r85161 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r85160‎ | r85161 | r85162 >
Date:22:39, 1 April 2011
Author:reedy
Status:deferred
Tags:
Comment:
Modified paths:
  • /trunk/extensions/MSSQLBackCompat/db (added) (history)

Diff [purge]

Index: trunk/extensions/MSSQLBackCompat/db/DatabaseMysql.php
@@ -0,0 +1,453 @@
 2+<?php
 3+/**
 4+ * Database abstraction object for mySQL
 5+ * Inherit all methods and properties of Database::Database()
 6+ *
 7+ * @ingroup Database
 8+ * @see Database
 9+ */
 10+class DatabaseMysql extends DatabaseBase {
 11+ function getType() {
 12+ return 'mysql';
 13+ }
 14+
 15+ /*private*/ function doQuery( $sql ) {
 16+ if( $this->bufferResults() ) {
 17+ $ret = mysql_query( $sql, $this->mConn );
 18+ } else {
 19+ $ret = mysql_unbuffered_query( $sql, $this->mConn );
 20+ }
 21+ return $ret;
 22+ }
 23+
 24+ function open( $server, $user, $password, $dbName ) {
 25+ global $wgAllDBsAreLocalhost;
 26+ wfProfileIn( __METHOD__ );
 27+
 28+ # Test for missing mysql.so
 29+ # First try to load it
 30+ if (!@extension_loaded('mysql')) {
 31+ @dl('mysql.so');
 32+ }
 33+
 34+ # Fail now
 35+ # Otherwise we get a suppressed fatal error, which is very hard to track down
 36+ if ( !function_exists( 'mysql_connect' ) ) {
 37+ throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
 38+ }
 39+
 40+ # Debugging hack -- fake cluster
 41+ if ( $wgAllDBsAreLocalhost ) {
 42+ $realServer = 'localhost';
 43+ } else {
 44+ $realServer = $server;
 45+ }
 46+ $this->close();
 47+ $this->mServer = $server;
 48+ $this->mUser = $user;
 49+ $this->mPassword = $password;
 50+ $this->mDBname = $dbName;
 51+
 52+ $success = false;
 53+
 54+ wfProfileIn("dbconnect-$server");
 55+
 56+ # The kernel's default SYN retransmission period is far too slow for us,
 57+ # so we use a short timeout plus a manual retry. Retrying means that a small
 58+ # but finite rate of SYN packet loss won't cause user-visible errors.
 59+ $this->mConn = false;
 60+ if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
 61+ $numAttempts = 2;
 62+ } else {
 63+ $numAttempts = 1;
 64+ }
 65+ $this->installErrorHandler();
 66+ for ( $i = 0; $i < $numAttempts && !$this->mConn; $i++ ) {
 67+ if ( $i > 1 ) {
 68+ usleep( 1000 );
 69+ }
 70+ if ( $this->mFlags & DBO_PERSISTENT ) {
 71+ $this->mConn = mysql_pconnect( $realServer, $user, $password );
 72+ } else {
 73+ # Create a new connection...
 74+ $this->mConn = mysql_connect( $realServer, $user, $password, true );
 75+ }
 76+ if ($this->mConn === false) {
 77+ #$iplus = $i + 1;
 78+ #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
 79+ }
 80+ }
 81+ $phpError = $this->restoreErrorHandler();
 82+ # Always log connection errors
 83+ if ( !$this->mConn ) {
 84+ $error = $this->lastError();
 85+ if ( !$error ) {
 86+ $error = $phpError;
 87+ }
 88+ wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
 89+ wfDebug( "DB connection error\n" );
 90+ wfDebug( "Server: $server, User: $user, Password: " .
 91+ substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
 92+ $success = false;
 93+ }
 94+
 95+ wfProfileOut("dbconnect-$server");
 96+
 97+ if ( $dbName != '' && $this->mConn !== false ) {
 98+ $success = @/**/mysql_select_db( $dbName, $this->mConn );
 99+ if ( !$success ) {
 100+ $error = "Error selecting database $dbName on server {$this->mServer} " .
 101+ "from client host " . wfHostname() . "\n";
 102+ wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
 103+ wfDebug( $error );
 104+ }
 105+ } else {
 106+ # Delay USE query
 107+ $success = (bool)$this->mConn;
 108+ }
 109+
 110+ if ( $success ) {
 111+ $version = $this->getServerVersion();
 112+ if ( version_compare( $version, '4.1' ) >= 0 ) {
 113+ // Tell the server we're communicating with it in UTF-8.
 114+ // This may engage various charset conversions.
 115+ global $wgDBmysql5;
 116+ if( $wgDBmysql5 ) {
 117+ $this->query( 'SET NAMES utf8', __METHOD__ );
 118+ }
 119+ // Turn off strict mode
 120+ $this->query( "SET sql_mode = ''", __METHOD__ );
 121+ }
 122+
 123+ // Turn off strict mode if it is on
 124+ } else {
 125+ $this->reportConnectionError( $phpError );
 126+ }
 127+
 128+ $this->mOpened = $success;
 129+ wfProfileOut( __METHOD__ );
 130+ return $success;
 131+ }
 132+
 133+ function close() {
 134+ $this->mOpened = false;
 135+ if ( $this->mConn ) {
 136+ if ( $this->trxLevel() ) {
 137+ $this->commit();
 138+ }
 139+ return mysql_close( $this->mConn );
 140+ } else {
 141+ return true;
 142+ }
 143+ }
 144+
 145+ function freeResult( $res ) {
 146+ if ( $res instanceof ResultWrapper ) {
 147+ $res = $res->result;
 148+ }
 149+ if ( !@/**/mysql_free_result( $res ) ) {
 150+ throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
 151+ }
 152+ }
 153+
 154+ function fetchObject( $res ) {
 155+ if ( $res instanceof ResultWrapper ) {
 156+ $res = $res->result;
 157+ }
 158+ @/**/$row = mysql_fetch_object( $res );
 159+ if( $this->lastErrno() ) {
 160+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
 161+ }
 162+ return $row;
 163+ }
 164+
 165+ function fetchRow( $res ) {
 166+ if ( $res instanceof ResultWrapper ) {
 167+ $res = $res->result;
 168+ }
 169+ @/**/$row = mysql_fetch_array( $res );
 170+ if ( $this->lastErrno() ) {
 171+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
 172+ }
 173+ return $row;
 174+ }
 175+
 176+ function numRows( $res ) {
 177+ if ( $res instanceof ResultWrapper ) {
 178+ $res = $res->result;
 179+ }
 180+ @/**/$n = mysql_num_rows( $res );
 181+ if( $this->lastErrno() ) {
 182+ throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
 183+ }
 184+ return $n;
 185+ }
 186+
 187+ function numFields( $res ) {
 188+ if ( $res instanceof ResultWrapper ) {
 189+ $res = $res->result;
 190+ }
 191+ return mysql_num_fields( $res );
 192+ }
 193+
 194+ function fieldName( $res, $n ) {
 195+ if ( $res instanceof ResultWrapper ) {
 196+ $res = $res->result;
 197+ }
 198+ return mysql_field_name( $res, $n );
 199+ }
 200+
 201+ function insertId() { return mysql_insert_id( $this->mConn ); }
 202+
 203+ function dataSeek( $res, $row ) {
 204+ if ( $res instanceof ResultWrapper ) {
 205+ $res = $res->result;
 206+ }
 207+ return mysql_data_seek( $res, $row );
 208+ }
 209+
 210+ function lastErrno() {
 211+ if ( $this->mConn ) {
 212+ return mysql_errno( $this->mConn );
 213+ } else {
 214+ return mysql_errno();
 215+ }
 216+ }
 217+
 218+ function lastError() {
 219+ if ( $this->mConn ) {
 220+ # Even if it's non-zero, it can still be invalid
 221+ wfSuppressWarnings();
 222+ $error = mysql_error( $this->mConn );
 223+ if ( !$error ) {
 224+ $error = mysql_error();
 225+ }
 226+ wfRestoreWarnings();
 227+ } else {
 228+ $error = mysql_error();
 229+ }
 230+ if( $error ) {
 231+ $error .= ' (' . $this->mServer . ')';
 232+ }
 233+ return $error;
 234+ }
 235+
 236+ function affectedRows() { return mysql_affected_rows( $this->mConn ); }
 237+
 238+ /**
 239+ * Estimate rows in dataset
 240+ * Returns estimated count, based on EXPLAIN output
 241+ * Takes same arguments as Database::select()
 242+ */
 243+ public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
 244+ $options['EXPLAIN'] = true;
 245+ $res = $this->select( $table, $vars, $conds, $fname, $options );
 246+ if ( $res === false )
 247+ return false;
 248+ if ( !$this->numRows( $res ) ) {
 249+ $this->freeResult($res);
 250+ return 0;
 251+ }
 252+
 253+ $rows = 1;
 254+ while( $plan = $this->fetchObject( $res ) ) {
 255+ $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
 256+ }
 257+
 258+ $this->freeResult($res);
 259+ return $rows;
 260+ }
 261+
 262+ function fieldInfo( $table, $field ) {
 263+ $table = $this->tableName( $table );
 264+ $res = $this->query( "SELECT * FROM $table LIMIT 1" );
 265+ $n = mysql_num_fields( $res->result );
 266+ for( $i = 0; $i < $n; $i++ ) {
 267+ $meta = mysql_fetch_field( $res->result, $i );
 268+ if( $field == $meta->name ) {
 269+ return new MySQLField($meta);
 270+ }
 271+ }
 272+ return false;
 273+ }
 274+
 275+ function selectDB( $db ) {
 276+ $this->mDBname = $db;
 277+ return mysql_select_db( $db, $this->mConn );
 278+ }
 279+
 280+ function strencode( $s ) {
 281+ return mysql_real_escape_string( $s, $this->mConn );
 282+ }
 283+
 284+ function ping() {
 285+ if( !function_exists( 'mysql_ping' ) ) {
 286+ wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
 287+ return true;
 288+ }
 289+ $ping = mysql_ping( $this->mConn );
 290+ if ( $ping ) {
 291+ return true;
 292+ }
 293+
 294+ // Need to reconnect manually in MySQL client 5.0.13+
 295+ if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
 296+ mysql_close( $this->mConn );
 297+ $this->mOpened = false;
 298+ $this->mConn = false;
 299+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
 300+ return true;
 301+ }
 302+ return false;
 303+ }
 304+
 305+ function getServerVersion() {
 306+ return mysql_get_server_info( $this->mConn );
 307+ }
 308+
 309+ function useIndexClause( $index ) {
 310+ return "FORCE INDEX (" . $this->indexName( $index ) . ")";
 311+ }
 312+
 313+ function lowPriorityOption() {
 314+ return 'LOW_PRIORITY';
 315+ }
 316+
 317+ function getSoftwareLink() {
 318+ return '[http://www.mysql.com/ MySQL]';
 319+ }
 320+
 321+ function standardSelectDistinct() {
 322+ return false;
 323+ }
 324+
 325+ public function setTimeout( $timeout ) {
 326+ $this->query( "SET net_read_timeout=$timeout" );
 327+ $this->query( "SET net_write_timeout=$timeout" );
 328+ }
 329+
 330+ public function lock( $lockName, $method, $timeout = 5 ) {
 331+ $lockName = $this->addQuotes( $lockName );
 332+ $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method );
 333+ $row = $this->fetchObject( $result );
 334+ $this->freeResult( $result );
 335+
 336+ if( $row->lockstatus == 1 ) {
 337+ return true;
 338+ } else {
 339+ wfDebug( __METHOD__." failed to acquire lock\n" );
 340+ return false;
 341+ }
 342+ }
 343+
 344+ public function unlock( $lockName, $method ) {
 345+ $lockName = $this->addQuotes( $lockName );
 346+ $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method );
 347+ $row = $this->fetchObject( $result );
 348+ return $row->lockstatus;
 349+ }
 350+
 351+ public function lockTables( $read, $write, $method, $lowPriority = true ) {
 352+ $items = array();
 353+
 354+ foreach( $write as $table ) {
 355+ $tbl = $this->tableName( $table ) .
 356+ ( $lowPriority ? ' LOW_PRIORITY' : '' ) .
 357+ ' WRITE';
 358+ $items[] = $tbl;
 359+ }
 360+ foreach( $read as $table ) {
 361+ $items[] = $this->tableName( $table ) . ' READ';
 362+ }
 363+ $sql = "LOCK TABLES " . implode( ',', $items );
 364+ $this->query( $sql, $method );
 365+ }
 366+
 367+ public function unlockTables( $method ) {
 368+ $this->query( "UNLOCK TABLES", $method );
 369+ }
 370+
 371+ public function setBigSelects( $value = true ) {
 372+ if ( $value === 'default' ) {
 373+ if ( $this->mDefaultBigSelects === null ) {
 374+ # Function hasn't been called before so it must already be set to the default
 375+ return;
 376+ } else {
 377+ $value = $this->mDefaultBigSelects;
 378+ }
 379+ } elseif ( $this->mDefaultBigSelects === null ) {
 380+ $this->mDefaultBigSelects = (bool)$this->selectField( false, '@@sql_big_selects' );
 381+ }
 382+ $encValue = $value ? '1' : '0';
 383+ $this->query( "SET sql_big_selects=$encValue", __METHOD__ );
 384+ }
 385+
 386+
 387+ /**
 388+ * Determines if the last failure was due to a deadlock
 389+ */
 390+ function wasDeadlock() {
 391+ return $this->lastErrno() == 1213;
 392+ }
 393+
 394+ /**
 395+ * Determines if the last query error was something that should be dealt
 396+ * with by pinging the connection and reissuing the query
 397+ */
 398+ function wasErrorReissuable() {
 399+ return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
 400+ }
 401+
 402+ /**
 403+ * Determines if the last failure was due to the database being read-only.
 404+ */
 405+ function wasReadOnlyError() {
 406+ return $this->lastErrno() == 1223 ||
 407+ ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
 408+ }
 409+
 410+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseMysql::duplicateTableStructure' ) {
 411+ $tmp = $temporary ? 'TEMPORARY ' : '';
 412+ if ( strcmp( $this->getServerVersion(), '4.1' ) < 0 ) {
 413+ # Hack for MySQL versions < 4.1, which don't support
 414+ # "CREATE TABLE ... LIKE". Note that
 415+ # "CREATE TEMPORARY TABLE ... SELECT * FROM ... LIMIT 0"
 416+ # would not create the indexes we need....
 417+ #
 418+ # Note that we don't bother changing around the prefixes here be-
 419+ # cause we know we're using MySQL anyway.
 420+
 421+ $res = $this->query( "SHOW CREATE TABLE $oldName" );
 422+ $row = $this->fetchRow( $res );
 423+ $oldQuery = $row[1];
 424+ $query = preg_replace( '/CREATE TABLE `(.*?)`/',
 425+ "CREATE $tmp TABLE `$newName`", $oldQuery );
 426+ if ($oldQuery === $query) {
 427+ # Couldn't do replacement
 428+ throw new MWException( "could not create temporary table $newName" );
 429+ }
 430+ } else {
 431+ $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
 432+ }
 433+ $this->query( $query, $fname );
 434+ }
 435+
 436+}
 437+
 438+/**
 439+ * Legacy support: Database == DatabaseMysql
 440+ */
 441+class Database extends DatabaseMysql {}
 442+
 443+class MySQLMasterPos {
 444+ var $file, $pos;
 445+
 446+ function __construct( $file, $pos ) {
 447+ $this->file = $file;
 448+ $this->pos = $pos;
 449+ }
 450+
 451+ function __toString() {
 452+ return "{$this->file}/{$this->pos}";
 453+ }
 454+}
Property changes on: trunk/extensions/MSSQLBackCompat/db/DatabaseMysql.php
___________________________________________________________________
Added: svn:eol-style
1455 + native
Index: trunk/extensions/MSSQLBackCompat/db/DatabaseOracle.php
@@ -0,0 +1,1201 @@
 2+<?php
 3+/**
 4+ * @ingroup Database
 5+ * @file
 6+ */
 7+
 8+/**
 9+ * This is the Oracle database abstraction layer.
 10+ * @ingroup Database
 11+ */
 12+class ORABlob {
 13+ var $mData;
 14+
 15+ function __construct( $data ) {
 16+ $this->mData = $data;
 17+ }
 18+
 19+ function getData() {
 20+ return $this->mData;
 21+ }
 22+}
 23+
 24+/**
 25+ * The oci8 extension is fairly weak and doesn't support oci_num_rows, among
 26+ * other things. We use a wrapper class to handle that and other
 27+ * Oracle-specific bits, like converting column names back to lowercase.
 28+ * @ingroup Database
 29+ */
 30+class ORAResult {
 31+ private $rows;
 32+ private $cursor;
 33+ private $stmt;
 34+ private $nrows;
 35+
 36+ private $unique;
 37+ private function array_unique_md( $array_in ) {
 38+ $array_out = array();
 39+ $array_hashes = array();
 40+
 41+ foreach ( $array_in as $key => $item ) {
 42+ $hash = md5( serialize( $item ) );
 43+ if ( !isset( $array_hashes[$hash] ) ) {
 44+ $array_hashes[$hash] = $hash;
 45+ $array_out[] = $item;
 46+ }
 47+ }
 48+
 49+ return $array_out;
 50+ }
 51+
 52+ function __construct( &$db, $stmt, $unique = false ) {
 53+ $this->db =& $db;
 54+
 55+ if ( ( $this->nrows = oci_fetch_all( $stmt, $this->rows, 0, - 1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM ) ) === false ) {
 56+ $e = oci_error( $stmt );
 57+ $db->reportQueryError( $e['message'], $e['code'], '', __FUNCTION__ );
 58+ return;
 59+ }
 60+
 61+ if ( $unique ) {
 62+ $this->rows = $this->array_unique_md( $this->rows );
 63+ $this->nrows = count( $this->rows );
 64+ }
 65+
 66+ $this->cursor = 0;
 67+ $this->stmt = $stmt;
 68+ }
 69+
 70+ public function free() {
 71+ oci_free_statement( $this->stmt );
 72+ }
 73+
 74+ public function seek( $row ) {
 75+ $this->cursor = min( $row, $this->nrows );
 76+ }
 77+
 78+ public function numRows() {
 79+ return $this->nrows;
 80+ }
 81+
 82+ public function numFields() {
 83+ return oci_num_fields( $this->stmt );
 84+ }
 85+
 86+ public function fetchObject() {
 87+ if ( $this->cursor >= $this->nrows ) {
 88+ return false;
 89+ }
 90+ $row = $this->rows[$this->cursor++];
 91+ $ret = new stdClass();
 92+ foreach ( $row as $k => $v ) {
 93+ $lc = strtolower( oci_field_name( $this->stmt, $k + 1 ) );
 94+ $ret->$lc = $v;
 95+ }
 96+
 97+ return $ret;
 98+ }
 99+
 100+ public function fetchRow() {
 101+ if ( $this->cursor >= $this->nrows ) {
 102+ return false;
 103+ }
 104+
 105+ $row = $this->rows[$this->cursor++];
 106+ $ret = array();
 107+ foreach ( $row as $k => $v ) {
 108+ $lc = strtolower( oci_field_name( $this->stmt, $k + 1 ) );
 109+ $ret[$lc] = $v;
 110+ $ret[$k] = $v;
 111+ }
 112+ return $ret;
 113+ }
 114+}
 115+
 116+/**
 117+ * Utility class.
 118+ * @ingroup Database
 119+ */
 120+class ORAField {
 121+ private $name, $tablename, $default, $max_length, $nullable,
 122+ $is_pk, $is_unique, $is_multiple, $is_key, $type;
 123+
 124+ function __construct( $info ) {
 125+ $this->name = $info['column_name'];
 126+ $this->tablename = $info['table_name'];
 127+ $this->default = $info['data_default'];
 128+ $this->max_length = $info['data_length'];
 129+ $this->nullable = $info['not_null'];
 130+ $this->is_pk = isset( $info['prim'] ) && $info['prim'] == 1 ? 1 : 0;
 131+ $this->is_unique = isset( $info['uniq'] ) && $info['uniq'] == 1 ? 1 : 0;
 132+ $this->is_multiple = isset( $info['nonuniq'] ) && $info['nonuniq'] == 1 ? 1 : 0;
 133+ $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple );
 134+ $this->type = $info['data_type'];
 135+ }
 136+
 137+ function name() {
 138+ return $this->name;
 139+ }
 140+
 141+ function tableName() {
 142+ return $this->tablename;
 143+ }
 144+
 145+ function defaultValue() {
 146+ return $this->default;
 147+ }
 148+
 149+ function maxLength() {
 150+ return $this->max_length;
 151+ }
 152+
 153+ function nullable() {
 154+ return $this->nullable;
 155+ }
 156+
 157+ function isKey() {
 158+ return $this->is_key;
 159+ }
 160+
 161+ function isMultipleKey() {
 162+ return $this->is_multiple;
 163+ }
 164+
 165+ function type() {
 166+ return $this->type;
 167+ }
 168+}
 169+
 170+/**
 171+ * @ingroup Database
 172+ */
 173+class DatabaseOracle extends DatabaseBase {
 174+ var $mInsertId = null;
 175+ var $mLastResult = null;
 176+ var $numeric_version = null;
 177+ var $lastResult = null;
 178+ var $cursor = 0;
 179+ var $mAffectedRows;
 180+
 181+ var $ignore_DUP_VAL_ON_INDEX = false;
 182+ var $sequenceData = null;
 183+
 184+ var $defaultCharset = 'AL32UTF8';
 185+
 186+ var $mFieldInfoCache = array();
 187+
 188+ function __construct( $server = false, $user = false, $password = false, $dbName = false,
 189+ $failFunction = false, $flags = 0, $tablePrefix = 'get from global' )
 190+ {
 191+ $tablePrefix = $tablePrefix == 'get from global' ? $tablePrefix : strtoupper( $tablePrefix );
 192+ parent::__construct( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix );
 193+ wfRunHooks( 'DatabaseOraclePostInit', array( &$this ) );
 194+ }
 195+
 196+ function getType() {
 197+ return 'oracle';
 198+ }
 199+
 200+ function cascadingDeletes() {
 201+ return true;
 202+ }
 203+ function cleanupTriggers() {
 204+ return true;
 205+ }
 206+ function strictIPs() {
 207+ return true;
 208+ }
 209+ function realTimestamps() {
 210+ return true;
 211+ }
 212+ function implicitGroupby() {
 213+ return false;
 214+ }
 215+ function implicitOrderby() {
 216+ return false;
 217+ }
 218+ function searchableIPs() {
 219+ return true;
 220+ }
 221+
 222+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
 223+ {
 224+ return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags );
 225+ }
 226+
 227+ /**
 228+ * Usually aborts on failure
 229+ * If the failFunction is set to a non-zero integer, returns success
 230+ */
 231+ function open( $server, $user, $password, $dbName ) {
 232+ if ( !function_exists( 'oci_connect' ) ) {
 233+ 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" );
 234+ }
 235+
 236+ $this->close();
 237+ $this->mServer = $server;
 238+ $this->mUser = $user;
 239+ $this->mPassword = $password;
 240+ $this->mDBname = $dbName;
 241+
 242+ if ( !strlen( $user ) ) { # e.g. the class is being loaded
 243+ return;
 244+ }
 245+
 246+ $session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT;
 247+ if ( $this->mFlags & DBO_DEFAULT ) {
 248+ $this->mConn = oci_new_connect( $user, $password, $dbName, $this->defaultCharset, $session_mode );
 249+ } else {
 250+ $this->mConn = oci_connect( $user, $password, $dbName, $this->defaultCharset, $session_mode );
 251+ }
 252+
 253+ if ( $this->mConn == false ) {
 254+ wfDebug( "DB connection error\n" );
 255+ wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
 256+ wfDebug( $this->lastError() . "\n" );
 257+ return false;
 258+ }
 259+
 260+ $this->mOpened = true;
 261+
 262+ # removed putenv calls because they interfere with the system globaly
 263+ $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
 264+ $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
 265+ return $this->mConn;
 266+ }
 267+
 268+ /**
 269+ * Closes a database connection, if it is open
 270+ * Returns success, true if already closed
 271+ */
 272+ function close() {
 273+ $this->mOpened = false;
 274+ if ( $this->mConn ) {
 275+ return oci_close( $this->mConn );
 276+ } else {
 277+ return true;
 278+ }
 279+ }
 280+
 281+ function execFlags() {
 282+ return $this->mTrxLevel ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS;
 283+ }
 284+
 285+ function doQuery( $sql ) {
 286+ wfDebug( "SQL: [$sql]\n" );
 287+ if ( !mb_check_encoding( $sql ) ) {
 288+ throw new MWException( "SQL encoding is invalid\n$sql" );
 289+ }
 290+
 291+ // handle some oracle specifics
 292+ // remove AS column/table/subquery namings
 293+ if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
 294+ $sql = preg_replace( '/ as /i', ' ', $sql );
 295+ }
 296+ // Oracle has issues with UNION clause if the statement includes LOB fields
 297+ // So we do a UNION ALL and then filter the results array with array_unique
 298+ $union_unique = ( preg_match( '/\/\* UNION_UNIQUE \*\/ /', $sql ) != 0 );
 299+ // EXPLAIN syntax in Oracle is EXPLAIN PLAN FOR and it return nothing
 300+ // you have to select data from plan table after explain
 301+ $olderr = error_reporting( E_ERROR );
 302+ $explain_id = date( 'dmYHis' );
 303+ error_reporting( $olderr );
 304+
 305+ $sql = preg_replace( '/^EXPLAIN /', 'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR', $sql, 1, $explain_count );
 306+
 307+
 308+ $olderr = error_reporting( E_ERROR );
 309+
 310+ if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) {
 311+ $e = oci_error( $this->mConn );
 312+ $this->reportQueryError( $e['message'], $e['code'], $sql, __FUNCTION__ );
 313+ }
 314+
 315+ $olderr = error_reporting( E_ERROR );
 316+ if ( oci_execute( $stmt, $this->execFlags() ) == false ) {
 317+ $e = oci_error( $stmt );
 318+ if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) {
 319+ $this->reportQueryError( $e['message'], $e['code'], $sql, __FUNCTION__ );
 320+ }
 321+ }
 322+ error_reporting( $olderr );
 323+
 324+ if ( $explain_count > 0 ) {
 325+ return $this->doQuery( 'SELECT id, cardinality "ROWS" FROM plan_table WHERE statement_id = \'' . $explain_id . '\'' );
 326+ } elseif ( oci_statement_type( $stmt ) == 'SELECT' ) {
 327+ return new ORAResult( $this, $stmt, $union_unique );
 328+ } else {
 329+ $this->mAffectedRows = oci_num_rows( $stmt );
 330+ return true;
 331+ }
 332+ }
 333+
 334+ function queryIgnore( $sql, $fname = '' ) {
 335+ return $this->query( $sql, $fname, true );
 336+ }
 337+
 338+ function freeResult( $res ) {
 339+ if ( $res instanceof ORAResult ) {
 340+ $res->free();
 341+ } else {
 342+ $res->result->free();
 343+ }
 344+ }
 345+
 346+ function fetchObject( $res ) {
 347+ if ( $res instanceof ORAResult ) {
 348+ return $res->numRows();
 349+ } else {
 350+ return $res->result->fetchObject();
 351+ }
 352+ }
 353+
 354+ function fetchRow( $res ) {
 355+ if ( $res instanceof ORAResult ) {
 356+ return $res->fetchRow();
 357+ } else {
 358+ return $res->result->fetchRow();
 359+ }
 360+ }
 361+
 362+ function numRows( $res ) {
 363+ if ( $res instanceof ORAResult ) {
 364+ return $res->numRows();
 365+ } else {
 366+ return $res->result->numRows();
 367+ }
 368+ }
 369+
 370+ function numFields( $res ) {
 371+ if ( $res instanceof ORAResult ) {
 372+ return $res->numFields();
 373+ } else {
 374+ return $res->result->numFields();
 375+ }
 376+ }
 377+
 378+ function fieldName( $stmt, $n ) {
 379+ return oci_field_name( $stmt, $n );
 380+ }
 381+
 382+ /**
 383+ * This must be called after nextSequenceVal
 384+ */
 385+ function insertId() {
 386+ return $this->mInsertId;
 387+ }
 388+
 389+ function dataSeek( $res, $row ) {
 390+ if ( $res instanceof ORAResult ) {
 391+ $res->seek( $row );
 392+ } else {
 393+ $res->result->seek( $row );
 394+ }
 395+ }
 396+
 397+ function lastError() {
 398+ if ( $this->mConn === false ) {
 399+ $e = oci_error();
 400+ } else {
 401+ $e = oci_error( $this->mConn );
 402+ }
 403+ return $e['message'];
 404+ }
 405+
 406+ function lastErrno() {
 407+ if ( $this->mConn === false ) {
 408+ $e = oci_error();
 409+ } else {
 410+ $e = oci_error( $this->mConn );
 411+ }
 412+ return $e['code'];
 413+ }
 414+
 415+ function affectedRows() {
 416+ return $this->mAffectedRows;
 417+ }
 418+
 419+ /**
 420+ * Returns information about an index
 421+ * If errors are explicitly ignored, returns NULL on failure
 422+ */
 423+ function indexInfo( $table, $index, $fname = 'DatabaseOracle::indexExists' ) {
 424+ return false;
 425+ }
 426+
 427+ function indexUnique( $table, $index, $fname = 'DatabaseOracle::indexUnique' ) {
 428+ return false;
 429+ }
 430+
 431+ function insert( $table, $a, $fname = 'DatabaseOracle::insert', $options = array() ) {
 432+ if ( !count( $a ) ) {
 433+ return true;
 434+ }
 435+
 436+ if ( !is_array( $options ) ) {
 437+ $options = array( $options );
 438+ }
 439+
 440+ if ( in_array( 'IGNORE', $options ) ) {
 441+ $this->ignore_DUP_VAL_ON_INDEX = true;
 442+ }
 443+
 444+ if ( !is_array( reset( $a ) ) ) {
 445+ $a = array( $a );
 446+ }
 447+
 448+ foreach ( $a as &$row ) {
 449+ $this->insertOneRow( $table, $row, $fname );
 450+ }
 451+ $retVal = true;
 452+
 453+ if ( in_array( 'IGNORE', $options ) ) {
 454+ $this->ignore_DUP_VAL_ON_INDEX = false;
 455+ }
 456+
 457+ return $retVal;
 458+ }
 459+
 460+ function insertOneRow( $table, $row, $fname ) {
 461+ global $wgLang;
 462+
 463+ // "INSERT INTO tables (a, b, c)"
 464+ $sql = "INSERT INTO " . $this->tableName( $table ) . " (" . join( ',', array_keys( $row ) ) . ')';
 465+ $sql .= " VALUES (";
 466+
 467+ // for each value, append ":key"
 468+ $first = true;
 469+ foreach ( $row as $col => $val ) {
 470+ if ( $first ) {
 471+ $sql .= $val !== null ? ':' . $col : 'NULL';
 472+ } else {
 473+ $sql .= $val !== null ? ', :' . $col : ', NULL';
 474+ }
 475+
 476+ $first = false;
 477+ }
 478+ $sql .= ')';
 479+
 480+ $stmt = oci_parse( $this->mConn, $sql );
 481+ foreach ( $row as $col => &$val ) {
 482+ $col_type = $this->fieldInfo( $this->tableName( $table ), $col )->type();
 483+
 484+ if ( $val === null ) {
 485+ // do nothing ... null was inserted in statement creation
 486+ } elseif ( $col_type != 'BLOB' && $col_type != 'CLOB' ) {
 487+ if ( is_object( $val ) ) {
 488+ $val = $val->getData();
 489+ }
 490+
 491+ if ( preg_match( '/^timestamp.*/i', $col_type ) == 1 && strtolower( $val ) == 'infinity' ) {
 492+ $val = '31-12-2030 12:00:00.000000';
 493+ }
 494+
 495+ $val = ( $wgLang != null ) ? $wgLang->checkTitleEncoding( $val ) : $val;
 496+ if ( oci_bind_by_name( $stmt, ":$col", $val ) === false ) {
 497+ $this->reportQueryError( $this->lastErrno(), $this->lastError(), $sql, __METHOD__ );
 498+ }
 499+ } else {
 500+ if ( ( $lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB ) ) === false ) {
 501+ $e = oci_error( $stmt );
 502+ throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] );
 503+ }
 504+
 505+ if ( $col_type == 'BLOB' ) { // is_object($val)) {
 506+ $lob[$col]->writeTemporary( $val ); // ->getData());
 507+ oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, SQLT_BLOB );
 508+ } else {
 509+ $lob[$col]->writeTemporary( $val );
 510+ oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, OCI_B_CLOB );
 511+ }
 512+ }
 513+ }
 514+
 515+ $olderr = error_reporting( E_ERROR );
 516+ if ( oci_execute( $stmt, OCI_DEFAULT ) === false ) {
 517+ $e = oci_error( $stmt );
 518+
 519+ if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) {
 520+ $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
 521+ } else {
 522+ $this->mAffectedRows = oci_num_rows( $stmt );
 523+ }
 524+ } else {
 525+ $this->mAffectedRows = oci_num_rows( $stmt );
 526+ }
 527+ error_reporting( $olderr );
 528+
 529+ if ( isset( $lob ) ) {
 530+ foreach ( $lob as $lob_i => $lob_v ) {
 531+ $lob_v->free();
 532+ }
 533+ }
 534+
 535+ if ( !$this->mTrxLevel ) {
 536+ oci_commit( $this->mConn );
 537+ }
 538+
 539+ oci_free_statement( $stmt );
 540+ }
 541+
 542+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseOracle::insertSelect',
 543+ $insertOptions = array(), $selectOptions = array() )
 544+ {
 545+ $destTable = $this->tableName( $destTable );
 546+ if ( !is_array( $selectOptions ) ) {
 547+ $selectOptions = array( $selectOptions );
 548+ }
 549+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
 550+ if ( is_array( $srcTable ) ) {
 551+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
 552+ } else {
 553+ $srcTable = $this->tableName( $srcTable );
 554+ }
 555+
 556+ if ( ( $sequenceData = $this->getSequenceData( $destTable ) ) !== false &&
 557+ !isset( $varMap[$sequenceData['column']] ) )
 558+ $varMap[$sequenceData['column']] = 'GET_SEQUENCE_VALUE(\'' . $sequenceData['sequence'] . '\')';
 559+
 560+ // count-alias subselect fields to avoid abigious definition errors
 561+ $i = 0;
 562+ foreach ( $varMap as $key => &$val ) {
 563+ $val = $val . ' field' . ( $i++ );
 564+ }
 565+
 566+ $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
 567+ " SELECT $startOpts " . implode( ',', $varMap ) .
 568+ " FROM $srcTable $useIndex ";
 569+ if ( $conds != '*' ) {
 570+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
 571+ }
 572+ $sql .= " $tailOpts";
 573+
 574+ if ( in_array( 'IGNORE', $insertOptions ) ) {
 575+ $this->ignore_DUP_VAL_ON_INDEX = true;
 576+ }
 577+
 578+ $retval = $this->query( $sql, $fname );
 579+
 580+ if ( in_array( 'IGNORE', $insertOptions ) ) {
 581+ $this->ignore_DUP_VAL_ON_INDEX = false;
 582+ }
 583+
 584+ return $retval;
 585+ }
 586+
 587+ function tableName( $name ) {
 588+ global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
 589+ /*
 590+ Replace reserved words with better ones
 591+ Using uppercase because that's the only way Oracle can handle
 592+ quoted tablenames
 593+ */
 594+ switch( $name ) {
 595+ case 'user':
 596+ $name = 'MWUSER';
 597+ break;
 598+ case 'text':
 599+ $name = 'PAGECONTENT';
 600+ break;
 601+ }
 602+
 603+ /*
 604+ The rest of procedure is equal to generic Databse class
 605+ except for the quoting style
 606+ */
 607+ if ( $name[0] == '"' && substr( $name, - 1, 1 ) == '"' ) {
 608+ return $name;
 609+ }
 610+
 611+ if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
 612+ return $name;
 613+ }
 614+ $dbDetails = array_reverse( explode( '.', $name, 2 ) );
 615+ if ( isset( $dbDetails[1] ) ) {
 616+ @list( $table, $database ) = $dbDetails;
 617+ } else {
 618+ @list( $table ) = $dbDetails;
 619+ }
 620+
 621+ $prefix = $this->mTablePrefix;
 622+
 623+ if ( isset( $database ) ) {
 624+ $table = ( $table[0] == '`' ? $table : "`{$table}`" );
 625+ }
 626+
 627+ if ( !isset( $database ) && isset( $wgSharedDB ) && $table[0] != '"'
 628+ && isset( $wgSharedTables )
 629+ && is_array( $wgSharedTables )
 630+ && in_array( $table, $wgSharedTables )
 631+ ) {
 632+ $database = $wgSharedDB;
 633+ $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix;
 634+ }
 635+
 636+ if ( isset( $database ) ) {
 637+ $database = ( $database[0] == '"' ? $database : "\"{$database}\"" );
 638+ }
 639+ $table = ( $table[0] == '"' ? $table : "\"{$prefix}{$table}\"" );
 640+
 641+ $tableName = ( isset( $database ) ? "{$database}.{$table}" : "{$table}" );
 642+
 643+ return strtoupper( $tableName );
 644+ }
 645+
 646+ /**
 647+ * Return the next in a sequence, save the value for retrieval via insertId()
 648+ */
 649+ function nextSequenceValue( $seqName ) {
 650+ $res = $this->query( "SELECT $seqName.nextval FROM dual" );
 651+ $row = $this->fetchRow( $res );
 652+ $this->mInsertId = $row[0];
 653+ $this->freeResult( $res );
 654+ return $this->mInsertId;
 655+ }
 656+
 657+ /**
 658+ * Return sequence_name if table has a sequence
 659+ */
 660+ function getSequenceData( $table ) {
 661+ if ( $this->sequenceData == null ) {
 662+ $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'" );
 663+
 664+ while ( ( $row = $result->fetchRow() ) !== false ) {
 665+ $this->sequenceData[$this->tableName( $row[1] )] = array(
 666+ 'sequence' => $row[0],
 667+ 'column' => $row[2]
 668+ );
 669+ }
 670+ }
 671+
 672+ return ( isset( $this->sequenceData[$table] ) ) ? $this->sequenceData[$table] : false;
 673+ }
 674+
 675+ # REPLACE query wrapper
 676+ # Oracle simulates this with a DELETE followed by INSERT
 677+ # $row is the row to insert, an associative array
 678+ # $uniqueIndexes is an array of indexes. Each element may be either a
 679+ # field name or an array of field names
 680+ #
 681+ # It may be more efficient to leave off unique indexes which are unlikely to collide.
 682+ # However if you do this, you run the risk of encountering errors which wouldn't have
 683+ # occurred in MySQL
 684+ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseOracle::replace' ) {
 685+ $table = $this->tableName( $table );
 686+
 687+ if ( count( $rows ) == 0 ) {
 688+ return;
 689+ }
 690+
 691+ # Single row case
 692+ if ( !is_array( reset( $rows ) ) ) {
 693+ $rows = array( $rows );
 694+ }
 695+
 696+ $sequenceData = $this->getSequenceData( $table );
 697+
 698+ foreach ( $rows as $row ) {
 699+ # Delete rows which collide
 700+ if ( $uniqueIndexes ) {
 701+ $condsDelete = array();
 702+ foreach ( $uniqueIndexes as $index )
 703+ $condsDelete[$index] = $row[$index];
 704+ $this->delete( $table, $condsDelete, $fname );
 705+/*
 706+ $sql = "DELETE FROM $table WHERE ";
 707+ $first = true;
 708+ foreach ( $uniqueIndexes as $index ) {
 709+ if ( $first ) {
 710+ $first = false;
 711+ $sql .= "(";
 712+ } else {
 713+ $sql .= ') OR (';
 714+ }
 715+ if ( is_array( $index ) ) {
 716+ $first2 = true;
 717+ foreach ( $index as $col ) {
 718+ if ( $first2 ) {
 719+ $first2 = false;
 720+ } else {
 721+ $sql .= ' AND ';
 722+ }
 723+ $sql .= $col.'=' . $this->addQuotes( $row[$col] );
 724+ }
 725+ } else {
 726+ $sql .= $index.'=' . $this->addQuotes( $row[$index] );
 727+ }
 728+ }
 729+ $sql .= ')';
 730+
 731+ $this->doQuery( $sql);//, $fname );
 732+*/
 733+ }
 734+
 735+ if ( $sequenceData !== false && !isset( $row[$sequenceData['column']] ) ) {
 736+ $row[$sequenceData['column']] = $this->nextSequenceValue( $sequenceData['sequence'] );
 737+ }
 738+
 739+ # Now insert the row
 740+ $this->insert( $table, $row, $fname );
 741+ }
 742+ }
 743+
 744+ # DELETE where the condition is a join
 745+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseOracle::deleteJoin" ) {
 746+ if ( !$conds ) {
 747+ throw new DBUnexpectedError( $this, 'DatabaseOracle::deleteJoin() called with empty $conds' );
 748+ }
 749+
 750+ $delTable = $this->tableName( $delTable );
 751+ $joinTable = $this->tableName( $joinTable );
 752+ $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
 753+ if ( $conds != '*' ) {
 754+ $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
 755+ }
 756+ $sql .= ')';
 757+
 758+ $this->query( $sql, $fname );
 759+ }
 760+
 761+ # Returns the size of a text field, or -1 for "unlimited"
 762+ function textFieldSize( $table, $field ) {
 763+ $table = $this->tableName( $table );
 764+ $sql = "SELECT t.typname as ftype,a.atttypmod as size
 765+ FROM pg_class c, pg_attribute a, pg_type t
 766+ WHERE relname='$table' AND a.attrelid=c.oid AND
 767+ a.atttypid=t.oid and a.attname='$field'";
 768+ $res = $this->query( $sql );
 769+ $row = $this->fetchObject( $res );
 770+ if ( $row->ftype == "varchar" ) {
 771+ $size = $row->size - 4;
 772+ } else {
 773+ $size = $row->size;
 774+ }
 775+ $this->freeResult( $res );
 776+ return $size;
 777+ }
 778+
 779+ function limitResult( $sql, $limit, $offset = false ) {
 780+ if ( $offset === false ) {
 781+ $offset = 0;
 782+ }
 783+ return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < (1 + $limit + $offset)";
 784+ }
 785+
 786+
 787+ function unionQueries( $sqls, $all ) {
 788+ $glue = ' UNION ALL ';
 789+ return 'SELECT * ' . ( $all ? '':'/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')' ;
 790+ }
 791+
 792+ function wasDeadlock() {
 793+ return $this->lastErrno() == 'OCI-00060';
 794+ }
 795+
 796+
 797+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseOracle::duplicateTableStructure' ) {
 798+ $temporary = $temporary ? 'TRUE' : 'FALSE';
 799+ return $this->query( 'BEGIN DUPLICATE_TABLE(\'' . $oldName . '\', \'' . $newName . '\', ' . $temporary . '); END;', $fname );
 800+ }
 801+
 802+ function timestamp( $ts = 0 ) {
 803+ return wfTimestamp( TS_ORACLE, $ts );
 804+ }
 805+
 806+ /**
 807+ * Return aggregated value function call
 808+ */
 809+ function aggregateValue ( $valuedata, $valuename = 'value' ) {
 810+ return $valuedata;
 811+ }
 812+
 813+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
 814+ # Ignore errors during error handling to avoid infinite
 815+ # recursion
 816+ $ignore = $this->ignoreErrors( true );
 817+ ++$this->mErrorCount;
 818+
 819+ if ( $ignore || $tempIgnore ) {
 820+ wfDebug( "SQL ERROR (ignored): $error\n" );
 821+ $this->ignoreErrors( $ignore );
 822+ } else {
 823+ throw new DBQueryError( $this, $error, $errno, $sql, $fname );
 824+ }
 825+ }
 826+
 827+ /**
 828+ * @return string wikitext of a link to the server software's web site
 829+ */
 830+ function getSoftwareLink() {
 831+ return '[http://www.oracle.com/ Oracle]';
 832+ }
 833+
 834+ /**
 835+ * @return string Version information from the database
 836+ */
 837+ function getServerVersion() {
 838+ return oci_server_version( $this->mConn );
 839+ }
 840+
 841+ /**
 842+ * Query whether a given table exists (in the given schema, or the default mw one if not given)
 843+ */
 844+ function tableExists( $table ) {
 845+ $SQL = "SELECT 1 FROM user_tables WHERE table_name='$table'";
 846+ $res = $this->doQuery( $SQL );
 847+ if ( $res ) {
 848+ $count = $res->numRows();
 849+ $res->free();
 850+ } else {
 851+ $count = 0;
 852+ }
 853+ return $count;
 854+ }
 855+
 856+ /**
 857+ * Query whether a given column exists in the mediawiki schema
 858+ * based on prebuilt table to simulate MySQL field info and keep query speed minimal
 859+ */
 860+ function fieldExists( $table, $field, $fname = 'DatabaseOracle::fieldExists' ) {
 861+ $table = trim( $table, '"' );
 862+
 863+ if (isset($this->mFieldInfoCache[$table.'.'.$field])) {
 864+ return true;
 865+ } elseif ( !isset( $this->fieldInfo_stmt ) ) {
 866+ $this->fieldInfo_stmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name = upper(:tab) and column_name = UPPER(:col)' );
 867+ }
 868+
 869+ oci_bind_by_name( $this->fieldInfo_stmt, ':tab', trim( $table, '"' ) );
 870+ oci_bind_by_name( $this->fieldInfo_stmt, ':col', $field );
 871+
 872+ if ( oci_execute( $this->fieldInfo_stmt, OCI_DEFAULT ) === false ) {
 873+ $e = oci_error( $this->fieldInfo_stmt );
 874+ $this->reportQueryError( $e['message'], $e['code'], 'fieldInfo QUERY', __METHOD__ );
 875+ return false;
 876+ }
 877+ $res = new ORAResult( $this, $this->fieldInfo_stmt );
 878+ if ($res->numRows() != 0) {
 879+ $this->mFieldInfoCache[$table.'.'.$field] = new ORAField( $res->fetchRow() );
 880+ return true;
 881+ } else {
 882+ return false;
 883+ }
 884+ }
 885+
 886+ function fieldInfo( $table, $field ) {
 887+ $table = trim( $table, '"' );
 888+
 889+ if (isset($this->mFieldInfoCache[$table.'.'.$field])) {
 890+ return $this->mFieldInfoCache[$table.'.'.$field];
 891+ } elseif ( !isset( $this->fieldInfo_stmt ) ) {
 892+ $this->fieldInfo_stmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name = upper(:tab) and column_name = UPPER(:col)' );
 893+ }
 894+
 895+ oci_bind_by_name( $this->fieldInfo_stmt, ':tab', $table );
 896+ oci_bind_by_name( $this->fieldInfo_stmt, ':col', $field );
 897+
 898+ if ( oci_execute( $this->fieldInfo_stmt, OCI_DEFAULT ) === false ) {
 899+ $e = oci_error( $this->fieldInfo_stmt );
 900+ $this->reportQueryError( $e['message'], $e['code'], 'fieldInfo QUERY', __METHOD__ );
 901+ return false;
 902+ }
 903+ $res = new ORAResult( $this, $this->fieldInfo_stmt );
 904+ $this->mFieldInfoCache[$table.'.'.$field] = new ORAField( $res->fetchRow() );
 905+ return $this->mFieldInfoCache[$table.'.'.$field];
 906+ }
 907+
 908+ function begin( $fname = '' ) {
 909+ $this->mTrxLevel = 1;
 910+ }
 911+
 912+ function immediateCommit( $fname = '' ) {
 913+ return true;
 914+ }
 915+
 916+ function commit( $fname = '' ) {
 917+ oci_commit( $this->mConn );
 918+ $this->mTrxLevel = 0;
 919+ }
 920+
 921+ /* Not even sure why this is used in the main codebase... */
 922+ function limitResultForUpdate( $sql, $num ) {
 923+ return $sql;
 924+ }
 925+
 926+ /* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */
 927+ function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
 928+ $cmd = '';
 929+ $done = false;
 930+ $dollarquote = false;
 931+
 932+ $replacements = array();
 933+
 934+ while ( ! feof( $fp ) ) {
 935+ if ( $lineCallback ) {
 936+ call_user_func( $lineCallback );
 937+ }
 938+ $line = trim( fgets( $fp, 1024 ) );
 939+ $sl = strlen( $line ) - 1;
 940+
 941+ if ( $sl < 0 ) {
 942+ continue;
 943+ }
 944+ if ( '-' == $line { 0 } && '-' == $line { 1 } ) {
 945+ continue;
 946+ }
 947+
 948+ // Allow dollar quoting for function declarations
 949+ if ( substr( $line, 0, 8 ) == '/*$mw$*/' ) {
 950+ if ( $dollarquote ) {
 951+ $dollarquote = false;
 952+ $done = true;
 953+ } else {
 954+ $dollarquote = true;
 955+ }
 956+ } elseif ( !$dollarquote ) {
 957+ if ( ';' == $line { $sl } && ( $sl < 2 || ';' != $line { $sl - 1 } ) ) {
 958+ $done = true;
 959+ $line = substr( $line, 0, $sl );
 960+ }
 961+ }
 962+
 963+ if ( $cmd != '' ) {
 964+ $cmd .= ' ';
 965+ }
 966+ $cmd .= "$line\n";
 967+
 968+ if ( $done ) {
 969+ $cmd = str_replace( ';;', ";", $cmd );
 970+ if ( strtolower( substr( $cmd, 0, 6 ) ) == 'define' ) {
 971+ if ( preg_match( '/^define\s*([^\s=]*)\s*=\s*\'\{\$([^\}]*)\}\'/', $cmd, $defines ) ) {
 972+ $replacements[$defines[2]] = $defines[1];
 973+ }
 974+ } else {
 975+ foreach ( $replacements as $mwVar => $scVar ) {
 976+ $cmd = str_replace( '&' . $scVar . '.', '{$' . $mwVar . '}', $cmd );
 977+ }
 978+
 979+ $cmd = $this->replaceVars( $cmd );
 980+ $res = $this->query( $cmd, __METHOD__ );
 981+ if ( $resultCallback ) {
 982+ call_user_func( $resultCallback, $res, $this );
 983+ }
 984+
 985+ if ( false === $res ) {
 986+ $err = $this->lastError();
 987+ return "Query \"{$cmd}\" failed with error code \"$err\".\n";
 988+ }
 989+ }
 990+
 991+ $cmd = '';
 992+ $done = false;
 993+ }
 994+ }
 995+ return true;
 996+ }
 997+
 998+ function setup_database() {
 999+ global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
 1000+
 1001+ echo "<li>Creating DB objects</li>\n";
 1002+ $res = $this->sourceFile( "../maintenance/ora/tables.sql" );
 1003+
 1004+ // Avoid the non-standard "REPLACE INTO" syntax
 1005+ echo "<li>Populating table interwiki</li>\n";
 1006+ $f = fopen( "../maintenance/interwiki.sql", 'r' );
 1007+ if ( $f == false ) {
 1008+ dieout( "<li>Could not find the interwiki.sql file</li>" );
 1009+ }
 1010+
 1011+ // do it like the postgres :D
 1012+ $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
 1013+ while ( !feof( $f ) ) {
 1014+ $line = fgets( $f, 1024 );
 1015+ $matches = array();
 1016+ if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) {
 1017+ continue;
 1018+ }
 1019+ $this->query( "$SQL $matches[1],$matches[2])" );
 1020+ }
 1021+
 1022+ echo "<li>Table interwiki successfully populated</li>\n";
 1023+ }
 1024+
 1025+ function strencode( $s ) {
 1026+ return str_replace( "'", "''", $s );
 1027+ }
 1028+
 1029+/*
 1030+ function encodeBlob($b) {
 1031+ return $b; //new ORABlob($b);
 1032+ }
 1033+ function decodeBlob($b) {
 1034+ return $b; //return $b->load();
 1035+ }
 1036+*/
 1037+ function addQuotes( $s ) {
 1038+ global $wgLang;
 1039+ if ( isset( $wgLang->mLoaded ) && $wgLang->mLoaded ) {
 1040+ $s = $wgLang->checkTitleEncoding( $s );
 1041+ }
 1042+ return "'" . $this->strencode( $s ) . "'";
 1043+ }
 1044+
 1045+ function quote_ident( $s ) {
 1046+ return $s;
 1047+ }
 1048+
 1049+ function selectRow( $table, $vars, $conds, $fname = 'DatabaseOracle::selectRow', $options = array(), $join_conds = array() ) {
 1050+ global $wgLang;
 1051+
 1052+ $conds2 = array();
 1053+ foreach ( $conds as $col => $val ) {
 1054+ $col_type = $this->fieldInfo( $this->tableName( $table ), $col )->type();
 1055+ if ( $col_type == 'CLOB' ) {
 1056+ $conds2['TO_CHAR(' . $col . ')'] = $wgLang->checkTitleEncoding( $val );
 1057+ } elseif ( $col_type == 'VARCHAR2' && !mb_check_encoding( $val ) ) {
 1058+ $conds2[$col] = $wgLang->checkTitleEncoding( $val );
 1059+ } else {
 1060+ $conds2[$col] = $val;
 1061+ }
 1062+ }
 1063+
 1064+ if ( is_array( $table ) ) {
 1065+ foreach ( $table as $tab ) {
 1066+ $tab = $this->tableName( $tab );
 1067+ }
 1068+ } else {
 1069+ $table = $this->tableName( $table );
 1070+ }
 1071+
 1072+ return parent::selectRow( $table, $vars, $conds2, $fname, $options, $join_conds );
 1073+ }
 1074+
 1075+ /**
 1076+ * Returns an optional USE INDEX clause to go after the table, and a
 1077+ * string to go at the end of the query
 1078+ *
 1079+ * @private
 1080+ *
 1081+ * @param $options Array: an associative array of options to be turned into
 1082+ * an SQL query, valid keys are listed in the function.
 1083+ * @return array
 1084+ */
 1085+ function makeSelectOptions( $options ) {
 1086+ $preLimitTail = $postLimitTail = '';
 1087+ $startOpts = '';
 1088+
 1089+ $noKeyOptions = array();
 1090+ foreach ( $options as $key => $option ) {
 1091+ if ( is_numeric( $key ) ) {
 1092+ $noKeyOptions[$option] = true;
 1093+ }
 1094+ }
 1095+
 1096+ if ( isset( $options['GROUP BY'] ) ) {
 1097+ $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
 1098+ }
 1099+ if ( isset( $options['ORDER BY'] ) ) {
 1100+ $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
 1101+ }
 1102+
 1103+ # if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
 1104+ # if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
 1105+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
 1106+ $startOpts .= 'DISTINCT';
 1107+ }
 1108+
 1109+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
 1110+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
 1111+ } else {
 1112+ $useIndex = '';
 1113+ }
 1114+
 1115+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
 1116+ }
 1117+
 1118+ public function delete( $table, $conds, $fname = 'DatabaseOracle::delete' ) {
 1119+ global $wgLang;
 1120+
 1121+ if ( $wgLang != null ) {
 1122+ $conds2 = array();
 1123+ foreach ( $conds as $col => $val ) {
 1124+ $col_type = $this->fieldInfo( $this->tableName( $table ), $col )->type();
 1125+ if ( $col_type == 'CLOB' ) {
 1126+ $conds2['TO_CHAR(' . $col . ')'] = $wgLang->checkTitleEncoding( $val );
 1127+ } else {
 1128+ if ( is_array( $val ) ) {
 1129+ $conds2[$col] = $val;
 1130+ foreach ( $conds2[$col] as &$val2 ) {
 1131+ $val2 = $wgLang->checkTitleEncoding( $val2 );
 1132+ }
 1133+ } else {
 1134+ $conds2[$col] = $wgLang->checkTitleEncoding( $val );
 1135+ }
 1136+ }
 1137+ }
 1138+
 1139+ return parent::delete( $table, $conds2, $fname );
 1140+ } else {
 1141+ return parent::delete( $table, $conds, $fname );
 1142+ }
 1143+ }
 1144+
 1145+ function bitNot( $field ) {
 1146+ // expecting bit-fields smaller than 4bytes
 1147+ return 'BITNOT(' . $bitField . ')';
 1148+ }
 1149+
 1150+ function bitAnd( $fieldLeft, $fieldRight ) {
 1151+ return 'BITAND(' . $fieldLeft . ', ' . $fieldRight . ')';
 1152+ }
 1153+
 1154+ function bitOr( $fieldLeft, $fieldRight ) {
 1155+ return 'BITOR(' . $fieldLeft . ', ' . $fieldRight . ')';
 1156+ }
 1157+
 1158+ /**
 1159+ * How lagged is this slave?
 1160+ *
 1161+ * @return int
 1162+ */
 1163+ public function getLag() {
 1164+ # Not implemented for Oracle
 1165+ return 0;
 1166+ }
 1167+
 1168+ function setFakeSlaveLag( $lag ) { }
 1169+ function setFakeMaster( $enabled = true ) { }
 1170+
 1171+ function getDBname() {
 1172+ return $this->mDBname;
 1173+ }
 1174+
 1175+ function getServer() {
 1176+ return $this->mServer;
 1177+ }
 1178+
 1179+ public function replaceVars( $ins ) {
 1180+ $varnames = array( 'wgDBprefix' );
 1181+ if ( $this->mFlags & DBO_SYSDBA ) {
 1182+ $varnames[] = 'wgDBOracleDefTS';
 1183+ $varnames[] = 'wgDBOracleTempTS';
 1184+ }
 1185+
 1186+ // Ordinary variables
 1187+ foreach ( $varnames as $var ) {
 1188+ if ( isset( $GLOBALS[$var] ) ) {
 1189+ $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
 1190+ $ins = str_replace( '{$' . $var . '}', $val, $ins );
 1191+ $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
 1192+ $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
 1193+ }
 1194+ }
 1195+
 1196+ return parent::replaceVars( $ins );
 1197+ }
 1198+
 1199+ public function getSearchEngine() {
 1200+ return 'SearchOracle';
 1201+ }
 1202+} // end DatabaseOracle class
Property changes on: trunk/extensions/MSSQLBackCompat/db/DatabaseOracle.php
___________________________________________________________________
Added: svn:eol-style
11203 + native
Index: trunk/extensions/MSSQLBackCompat/db/DatabasePostgres.php
@@ -0,0 +1,1476 @@
 2+<?php
 3+/**
 4+ * @ingroup Database
 5+ * @file
 6+ * This is the Postgres database abstraction layer.
 7+ *
 8+ */
 9+class PostgresField {
 10+ private $name, $tablename, $type, $nullable, $max_length;
 11+
 12+ static function fromText($db, $table, $field) {
 13+ global $wgDBmwschema;
 14+
 15+ $q = <<<SQL
 16+SELECT
 17+CASE WHEN typname = 'int2' THEN 'smallint'
 18+WHEN typname = 'int4' THEN 'integer'
 19+WHEN typname = 'int8' THEN 'bigint'
 20+WHEN typname = 'bpchar' THEN 'char'
 21+ELSE typname END AS typname,
 22+attnotnull, attlen
 23+FROM pg_class, pg_namespace, pg_attribute, pg_type
 24+WHERE relnamespace=pg_namespace.oid
 25+AND relkind='r'
 26+AND attrelid=pg_class.oid
 27+AND atttypid=pg_type.oid
 28+AND nspname=%s
 29+AND relname=%s
 30+AND attname=%s;
 31+SQL;
 32+ $res = $db->query(sprintf($q,
 33+ $db->addQuotes($wgDBmwschema),
 34+ $db->addQuotes($table),
 35+ $db->addQuotes($field)));
 36+ $row = $db->fetchObject($res);
 37+ if (!$row)
 38+ return null;
 39+ $n = new PostgresField;
 40+ $n->type = $row->typname;
 41+ $n->nullable = ($row->attnotnull == 'f');
 42+ $n->name = $field;
 43+ $n->tablename = $table;
 44+ $n->max_length = $row->attlen;
 45+ return $n;
 46+ }
 47+
 48+ function name() {
 49+ return $this->name;
 50+ }
 51+
 52+ function tableName() {
 53+ return $this->tablename;
 54+ }
 55+
 56+ function type() {
 57+ return $this->type;
 58+ }
 59+
 60+ function nullable() {
 61+ return $this->nullable;
 62+ }
 63+
 64+ function maxLength() {
 65+ return $this->max_length;
 66+ }
 67+}
 68+
 69+/**
 70+ * @ingroup Database
 71+ */
 72+class DatabasePostgres extends DatabaseBase {
 73+ var $mInsertId = null;
 74+ var $mLastResult = null;
 75+ var $numeric_version = null;
 76+ var $mAffectedRows = null;
 77+
 78+ function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false,
 79+ $failFunction = false, $flags = 0 )
 80+ {
 81+
 82+ $this->mFailFunction = $failFunction;
 83+ $this->mFlags = $flags;
 84+ $this->open( $server, $user, $password, $dbName);
 85+
 86+ }
 87+
 88+ function getType() {
 89+ return 'postgres';
 90+ }
 91+
 92+ function cascadingDeletes() {
 93+ return true;
 94+ }
 95+ function cleanupTriggers() {
 96+ return true;
 97+ }
 98+ function strictIPs() {
 99+ return true;
 100+ }
 101+ function realTimestamps() {
 102+ return true;
 103+ }
 104+ function implicitGroupby() {
 105+ return false;
 106+ }
 107+ function implicitOrderby() {
 108+ return false;
 109+ }
 110+ function searchableIPs() {
 111+ return true;
 112+ }
 113+ function functionalIndexes() {
 114+ return true;
 115+ }
 116+
 117+ function hasConstraint( $name ) {
 118+ global $wgDBmwschema;
 119+ $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) ."'";
 120+ return $this->numRows($res = $this->doQuery($SQL));
 121+ }
 122+
 123+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
 124+ {
 125+ return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags );
 126+ }
 127+
 128+ /**
 129+ * Usually aborts on failure
 130+ * If the failFunction is set to a non-zero integer, returns success
 131+ */
 132+ function open( $server, $user, $password, $dbName ) {
 133+ # Test for Postgres support, to avoid suppressed fatal error
 134+ if ( !function_exists( 'pg_connect' ) ) {
 135+ throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
 136+ }
 137+
 138+ global $wgDBport;
 139+
 140+ if (!strlen($user)) { ## e.g. the class is being loaded
 141+ return;
 142+ }
 143+ $this->close();
 144+ $this->mServer = $server;
 145+ $this->mPort = $port = $wgDBport;
 146+ $this->mUser = $user;
 147+ $this->mPassword = $password;
 148+ $this->mDBname = $dbName;
 149+
 150+ $connectVars = array(
 151+ 'dbname' => $dbName,
 152+ 'user' => $user,
 153+ 'password' => $password );
 154+ if ($server!=false && $server!="") {
 155+ $connectVars['host'] = $server;
 156+ }
 157+ if ($port!=false && $port!="") {
 158+ $connectVars['port'] = $port;
 159+ }
 160+ $connectString = $this->makeConnectionString( $connectVars );
 161+
 162+ $this->installErrorHandler();
 163+ $this->mConn = pg_connect( $connectString );
 164+ $phpError = $this->restoreErrorHandler();
 165+
 166+ if ( $this->mConn == false ) {
 167+ wfDebug( "DB connection error\n" );
 168+ wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
 169+ wfDebug( $this->lastError()."\n" );
 170+ if ( !$this->mFailFunction ) {
 171+ throw new DBConnectionError( $this, $phpError );
 172+ } else {
 173+ return false;
 174+ }
 175+ }
 176+
 177+ $this->mOpened = true;
 178+
 179+ global $wgCommandLineMode;
 180+ ## If called from the command-line (e.g. importDump), only show errors
 181+ if ($wgCommandLineMode) {
 182+ $this->doQuery( "SET client_min_messages = 'ERROR'" );
 183+ }
 184+
 185+ $this->doQuery( "SET client_encoding='UTF8'" );
 186+
 187+ global $wgDBmwschema, $wgDBts2schema;
 188+ if (isset( $wgDBmwschema ) && isset( $wgDBts2schema )
 189+ && $wgDBmwschema !== 'mediawiki'
 190+ && preg_match( '/^\w+$/', $wgDBmwschema )
 191+ && preg_match( '/^\w+$/', $wgDBts2schema )
 192+ ) {
 193+ $safeschema = $this->quote_ident($wgDBmwschema);
 194+ $safeschema2 = $this->quote_ident($wgDBts2schema);
 195+ $this->doQuery( "SET search_path = $safeschema, $wgDBts2schema, public" );
 196+ }
 197+
 198+ return $this->mConn;
 199+ }
 200+
 201+ function makeConnectionString( $vars ) {
 202+ $s = '';
 203+ foreach ( $vars as $name => $value ) {
 204+ $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' ";
 205+ }
 206+ return $s;
 207+ }
 208+
 209+
 210+ function initial_setup($password, $dbName) {
 211+ // If this is the initial connection, setup the schema stuff and possibly create the user
 212+ global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema;
 213+
 214+ print "<li>Checking the version of Postgres...";
 215+ $version = $this->getServerVersion();
 216+ $PGMINVER = '8.1';
 217+ if ($version < $PGMINVER) {
 218+ print "<b>FAILED</b>. Required version is $PGMINVER. You have " . htmlspecialchars( $version ) . "</li>\n";
 219+ dieout("</ul>");
 220+ }
 221+ print "version " . htmlspecialchars( $this->numeric_version ) . " is OK.</li>\n";
 222+
 223+ $safeuser = $this->quote_ident($wgDBuser);
 224+ // Are we connecting as a superuser for the first time?
 225+ if ($wgDBsuperuser) {
 226+ // Are we really a superuser? Check out our rights
 227+ $SQL = "SELECT
 228+ CASE WHEN usesuper IS TRUE THEN
 229+ CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END
 230+ ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END
 231+ END AS rights
 232+ FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser);
 233+ $rows = $this->numRows($res = $this->doQuery($SQL));
 234+ if (!$rows) {
 235+ print "<li>ERROR: Could not read permissions for user \"" . htmlspecialchars( $wgDBsuperuser ) . "\"</li>\n";
 236+ dieout('</ul>');
 237+ }
 238+ $perms = pg_fetch_result($res, 0, 0);
 239+
 240+ $SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser);
 241+ $rows = $this->numRows($this->doQuery($SQL));
 242+ if ($rows) {
 243+ print "<li>User \"" . htmlspecialchars( $wgDBuser ) . "\" already exists, skipping account creation.</li>";
 244+ }
 245+ else {
 246+ if ($perms != 1 and $perms != 3) {
 247+ print "<li>ERROR: the user \"" . htmlspecialchars( $wgDBsuperuser ) . "\" cannot create other users. ";
 248+ print 'Please use a different Postgres user.</li>';
 249+ dieout('</ul>');
 250+ }
 251+ print "<li>Creating user <b>" . htmlspecialchars( $wgDBuser ) . "</b>...";
 252+ $safepass = $this->addQuotes($wgDBpassword);
 253+ $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass";
 254+ $this->doQuery($SQL);
 255+ print "OK</li>\n";
 256+ }
 257+ // User now exists, check out the database
 258+ if ($dbName != $wgDBname) {
 259+ $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $this->addQuotes($wgDBname);
 260+ $rows = $this->numRows($this->doQuery($SQL));
 261+ if ($rows) {
 262+ print "<li>Database \"" . htmlspecialchars( $wgDBname ) . "\" already exists, skipping database creation.</li>";
 263+ }
 264+ else {
 265+ if ($perms < 1) {
 266+ print "<li>ERROR: the user \"" . htmlspecialchars( $wgDBsuperuser ) . "\" cannot create databases. ";
 267+ print 'Please use a different Postgres user.</li>';
 268+ dieout('</ul>');
 269+ }
 270+ print "<li>Creating database <b>" . htmlspecialchars( $wgDBname ) . "</b>...";
 271+ $safename = $this->quote_ident($wgDBname);
 272+ $SQL = "CREATE DATABASE $safename OWNER $safeuser ";
 273+ $this->doQuery($SQL);
 274+ print "OK</li>\n";
 275+ // Hopefully tsearch2 and plpgsql are in template1...
 276+ }
 277+
 278+ // Reconnect to check out tsearch2 rights for this user
 279+ print "<li>Connecting to \"" . htmlspecialchars( $wgDBname ) . "\" as superuser \"" .
 280+ htmlspecialchars( $wgDBsuperuser ) . "\" to check rights...";
 281+
 282+ $connectVars = array();
 283+ if ($this->mServer!=false && $this->mServer!="") {
 284+ $connectVars['host'] = $this->mServer;
 285+ }
 286+ if ($this->mPort!=false && $this->mPort!="") {
 287+ $connectVars['port'] = $this->mPort;
 288+ }
 289+ $connectVars['dbname'] = $wgDBname;
 290+ $connectVars['user'] = $wgDBsuperuser;
 291+ $connectVars['password'] = $password;
 292+
 293+ @$this->mConn = pg_connect( $this->makeConnectionString( $connectVars ) );
 294+ if ( $this->mConn == false ) {
 295+ print "<b>FAILED TO CONNECT!</b></li>";
 296+ dieout("</ul>");
 297+ }
 298+ print "OK</li>\n";
 299+ }
 300+
 301+ if ($this->numeric_version < 8.3) {
 302+ // Tsearch2 checks
 303+ print "<li>Checking that tsearch2 is installed in the database \"" .
 304+ htmlspecialchars( $wgDBname ) . "\"...";
 305+ if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) {
 306+ print "<b>FAILED</b>. tsearch2 must be installed in the database \"" .
 307+ htmlspecialchars( $wgDBname ) . "\".";
 308+ print "Please see <a href='http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
 309+ print " for instructions or ask on #postgresql on irc.freenode.net</li>\n";
 310+ dieout("</ul>");
 311+ }
 312+ print "OK</li>\n";
 313+ print "<li>Ensuring that user \"" . htmlspecialchars( $wgDBuser ) .
 314+ "\" has select rights on the tsearch2 tables...";
 315+ foreach (array('cfg','cfgmap','dict','parser') as $table) {
 316+ $SQL = "GRANT SELECT ON pg_ts_$table TO $safeuser";
 317+ $this->doQuery($SQL);
 318+ }
 319+ print "OK</li>\n";
 320+ }
 321+
 322+ // Setup the schema for this user if needed
 323+ $result = $this->schemaExists($wgDBmwschema);
 324+ $safeschema = $this->quote_ident($wgDBmwschema);
 325+ if (!$result) {
 326+ print "<li>Creating schema <b>" . htmlspecialchars( $wgDBmwschema ) . "</b> ...";
 327+ $result = $this->doQuery("CREATE SCHEMA $safeschema AUTHORIZATION $safeuser");
 328+ if (!$result) {
 329+ print "<b>FAILED</b>.</li>\n";
 330+ dieout("</ul>");
 331+ }
 332+ print "OK</li>\n";
 333+ }
 334+ else {
 335+ print "<li>Schema already exists, explicitly granting rights...\n";
 336+ $safeschema2 = $this->addQuotes($wgDBmwschema);
 337+ $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n".
 338+ "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n".
 339+ "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n".
 340+ "AND p.relkind IN ('r','S','v')\n";
 341+ $SQL .= "UNION\n";
 342+ $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n".
 343+ "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n".
 344+ "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n".
 345+ "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2";
 346+ $res = $this->doQuery($SQL);
 347+ if (!$res) {
 348+ print "<b>FAILED</b>. Could not set rights for the user.</li>\n";
 349+ dieout("</ul>");
 350+ }
 351+ $this->doQuery("SET search_path = $safeschema");
 352+ $rows = $this->numRows($res);
 353+ while ($rows) {
 354+ $rows--;
 355+ $this->doQuery(pg_fetch_result($res, $rows, 0));
 356+ }
 357+ print "OK</li>";
 358+ }
 359+
 360+ // Install plpgsql if needed
 361+ $this->setup_plpgsql();
 362+
 363+ $wgDBsuperuser = '';
 364+ return true; // Reconnect as regular user
 365+
 366+ } // end superuser
 367+
 368+ if (!defined('POSTGRES_SEARCHPATH')) {
 369+
 370+ if ($this->numeric_version < 8.3) {
 371+ // Do we have the basic tsearch2 table?
 372+ print "<li>Checking for tsearch2 in the schema \"" . htmlspecialchars( $wgDBts2schema ) . "\"...";
 373+ if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) {
 374+ print "<b>FAILED</b>. Make sure tsearch2 is installed. See <a href=";
 375+ print "'http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
 376+ print " for instructions.</li>\n";
 377+ dieout("</ul>");
 378+ }
 379+ print "OK</li>\n";
 380+
 381+ // Does this user have the rights to the tsearch2 tables?
 382+ $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
 383+ print "<li>Checking tsearch2 permissions...";
 384+ // Let's check all four, just to be safe
 385+ error_reporting( 0 );
 386+ $ts2tables = array('cfg','cfgmap','dict','parser');
 387+ $safetsschema = $this->quote_ident($wgDBts2schema);
 388+ foreach ( $ts2tables AS $tname ) {
 389+ $SQL = "SELECT count(*) FROM $safetsschema.pg_ts_$tname";
 390+ $res = $this->doQuery($SQL);
 391+ if (!$res) {
 392+ print "<b>FAILED</b> to access " . htmlspecialchars( "pg_ts_$tname" ) .
 393+ ". Make sure that the user \"". htmlspecialchars( $wgDBuser ) .
 394+ "\" has SELECT access to all four tsearch2 tables</li>\n";
 395+ dieout("</ul>");
 396+ }
 397+ }
 398+ $SQL = "SELECT ts_name FROM $safetsschema.pg_ts_cfg WHERE locale = " . $this->addQuotes( $ctype ) ;
 399+ $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END";
 400+ $res = $this->doQuery($SQL);
 401+ error_reporting( E_ALL );
 402+ if (!$res) {
 403+ print "<b>FAILED</b>. Could not determine the tsearch2 locale information</li>\n";
 404+ dieout("</ul>");
 405+ }
 406+ print "OK</li>";
 407+
 408+ // Will the current locale work? Can we force it to?
 409+ print "<li>Verifying tsearch2 locale with " . htmlspecialchars( $ctype ) . "...";
 410+ $rows = $this->numRows($res);
 411+ $resetlocale = 0;
 412+ if (!$rows) {
 413+ print "<b>not found</b></li>\n";
 414+ print "<li>Attempting to set default tsearch2 locale to \"" . htmlspecialchars( $ctype ) . "\"...";
 415+ $resetlocale = 1;
 416+ }
 417+ else {
 418+ $tsname = pg_fetch_result($res, 0, 0);
 419+ if ($tsname != 'default') {
 420+ print "<b>not set to default (" . htmlspecialchars( $tsname ) . ")</b>";
 421+ print "<li>Attempting to change tsearch2 default locale to \"" .
 422+ htmlspecialchars( $ctype ) . "\"...";
 423+ $resetlocale = 1;
 424+ }
 425+ }
 426+ if ($resetlocale) {
 427+ $SQL = "UPDATE $safetsschema.pg_ts_cfg SET locale = " . $this->addQuotes( $ctype ) . " WHERE ts_name = 'default'";
 428+ $res = $this->doQuery($SQL);
 429+ if (!$res) {
 430+ print "<b>FAILED</b>. ";
 431+ print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"" .
 432+ htmlspecialchars( $ctype ) . "\"</li>\n";
 433+ dieout("</ul>");
 434+ }
 435+ print "OK</li>";
 436+ }
 437+
 438+ // Final test: try out a simple tsearch2 query
 439+ $SQL = "SELECT $safetsschema.to_tsvector('default','MediaWiki tsearch2 testing')";
 440+ $res = $this->doQuery($SQL);
 441+ if (!$res) {
 442+ print "<b>FAILED</b>. Specifically, \"" . htmlspecialchars( $SQL ) . "\" did not work.</li>";
 443+ dieout("</ul>");
 444+ }
 445+ print "OK</li>";
 446+ }
 447+
 448+ // Install plpgsql if needed
 449+ $this->setup_plpgsql();
 450+
 451+ // Does the schema already exist? Who owns it?
 452+ $result = $this->schemaExists($wgDBmwschema);
 453+ if (!$result) {
 454+ print "<li>Creating schema <b>" . htmlspecialchars( $wgDBmwschema ) . "</b> ...";
 455+ error_reporting( 0 );
 456+ $safeschema = $this->quote_ident($wgDBmwschema);
 457+ $result = $this->doQuery("CREATE SCHEMA $safeschema");
 458+ error_reporting( E_ALL );
 459+ if (!$result) {
 460+ print "<b>FAILED</b>. The user \"" . htmlspecialchars( $wgDBuser ) .
 461+ "\" must be able to access the schema. ".
 462+ "You can try making them the owner of the database, or try creating the schema with a ".
 463+ "different user, and then grant access to the \"" .
 464+ htmlspecialchars( $wgDBuser ) . "\" user.</li>\n";
 465+ dieout("</ul>");
 466+ }
 467+ print "OK</li>\n";
 468+ }
 469+ else if ($result != $wgDBuser) {
 470+ print "<li>Schema \"" . htmlspecialchars( $wgDBmwschema ) . "\" exists but is not owned by \"" .
 471+ htmlspecialchars( $wgDBuser ) . "\". Not ideal.</li>\n";
 472+ }
 473+ else {
 474+ print "<li>Schema \"" . htmlspecialchars( $wgDBmwschema ) . "\" exists and is owned by \"" .
 475+ htmlspecialchars( $wgDBuser ) . "\". Excellent.</li>\n";
 476+ }
 477+
 478+ // Always return GMT time to accomodate the existing integer-based timestamp assumption
 479+ print "<li>Setting the timezone to GMT for user \"" . htmlspecialchars( $wgDBuser ) . "\" ...";
 480+ $SQL = "ALTER USER $safeuser SET timezone = 'GMT'";
 481+ $result = pg_query($this->mConn, $SQL);
 482+ if (!$result) {
 483+ print "<b>FAILED</b>.</li>\n";
 484+ dieout("</ul>");
 485+ }
 486+ print "OK</li>\n";
 487+ // Set for the rest of this session
 488+ $SQL = "SET timezone = 'GMT'";
 489+ $result = pg_query($this->mConn, $SQL);
 490+ if (!$result) {
 491+ print "<li>Failed to set timezone</li>\n";
 492+ dieout("</ul>");
 493+ }
 494+
 495+ print "<li>Setting the datestyle to ISO, YMD for user \"" . htmlspecialchars( $wgDBuser ) . "\" ...";
 496+ $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'";
 497+ $result = pg_query($this->mConn, $SQL);
 498+ if (!$result) {
 499+ print "<b>FAILED</b>.</li>\n";
 500+ dieout("</ul>");
 501+ }
 502+ print "OK</li>\n";
 503+ // Set for the rest of this session
 504+ $SQL = "SET datestyle = 'ISO, YMD'";
 505+ $result = pg_query($this->mConn, $SQL);
 506+ if (!$result) {
 507+ print "<li>Failed to set datestyle</li>\n";
 508+ dieout("</ul>");
 509+ }
 510+
 511+ // Fix up the search paths if needed
 512+ print "<li>Setting the search path for user \"" . htmlspecialchars( $wgDBuser ) . "\" ...";
 513+ $path = $this->quote_ident($wgDBmwschema);
 514+ if ($wgDBts2schema !== $wgDBmwschema)
 515+ $path .= ", ". $this->quote_ident($wgDBts2schema);
 516+ if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public')
 517+ $path .= ", public";
 518+ $SQL = "ALTER USER $safeuser SET search_path = $path";
 519+ $result = pg_query($this->mConn, $SQL);
 520+ if (!$result) {
 521+ print "<b>FAILED</b>.</li>\n";
 522+ dieout("</ul>");
 523+ }
 524+ print "OK</li>\n";
 525+ // Set for the rest of this session
 526+ $SQL = "SET search_path = $path";
 527+ $result = pg_query($this->mConn, $SQL);
 528+ if (!$result) {
 529+ print "<li>Failed to set search_path</li>\n";
 530+ dieout("</ul>");
 531+ }
 532+ define( "POSTGRES_SEARCHPATH", $path );
 533+ }
 534+ }
 535+
 536+
 537+ function setup_plpgsql() {
 538+ print "<li>Checking for Pl/Pgsql ...";
 539+ $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'";
 540+ $rows = $this->numRows($this->doQuery($SQL));
 541+ if ($rows < 1) {
 542+ // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it
 543+ print "not installed. Attempting to install Pl/Pgsql ...";
 544+ $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ".
 545+ "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'";
 546+ $rows = $this->numRows($this->doQuery($SQL));
 547+ if ($rows >= 1) {
 548+ $olde = error_reporting(0);
 549+ error_reporting($olde - E_WARNING);
 550+ $result = $this->doQuery("CREATE LANGUAGE plpgsql");
 551+ error_reporting($olde);
 552+ if (!$result) {
 553+ print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>" .
 554+ htmlspecialchars( $wgDBname ) . "</tt></li>";
 555+ dieout("</ul>");
 556+ }
 557+ }
 558+ else {
 559+ print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>" .
 560+ htmlspecialchars( $wgDBname ) . "</tt></li>";
 561+ dieout("</ul>");
 562+ }
 563+ }
 564+ print "OK</li>\n";
 565+ }
 566+
 567+
 568+ /**
 569+ * Closes a database connection, if it is open
 570+ * Returns success, true if already closed
 571+ */
 572+ function close() {
 573+ $this->mOpened = false;
 574+ if ( $this->mConn ) {
 575+ return pg_close( $this->mConn );
 576+ } else {
 577+ return true;
 578+ }
 579+ }
 580+
 581+ function doQuery( $sql ) {
 582+ if (function_exists('mb_convert_encoding')) {
 583+ $sql = mb_convert_encoding($sql,'UTF-8');
 584+ }
 585+ $this->mLastResult = pg_query( $this->mConn, $sql);
 586+ $this->mAffectedRows = null; // use pg_affected_rows(mLastResult)
 587+ return $this->mLastResult;
 588+ }
 589+
 590+ function queryIgnore( $sql, $fname = '' ) {
 591+ return $this->query( $sql, $fname, true );
 592+ }
 593+
 594+ function freeResult( $res ) {
 595+ if ( $res instanceof ResultWrapper ) {
 596+ $res = $res->result;
 597+ }
 598+ if ( !@pg_free_result( $res ) ) {
 599+ throw new DBUnexpectedError($this, "Unable to free Postgres result\n" );
 600+ }
 601+ }
 602+
 603+ function fetchObject( $res ) {
 604+ if ( $res instanceof ResultWrapper ) {
 605+ $res = $res->result;
 606+ }
 607+ @$row = pg_fetch_object( $res );
 608+ # FIXME: HACK HACK HACK HACK debug
 609+
 610+ # TODO:
 611+ # hashar : not sure if the following test really trigger if the object
 612+ # fetching failed.
 613+ if( pg_last_error($this->mConn) ) {
 614+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
 615+ }
 616+ return $row;
 617+ }
 618+
 619+ function fetchRow( $res ) {
 620+ if ( $res instanceof ResultWrapper ) {
 621+ $res = $res->result;
 622+ }
 623+ @$row = pg_fetch_array( $res );
 624+ if( pg_last_error($this->mConn) ) {
 625+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
 626+ }
 627+ return $row;
 628+ }
 629+
 630+ function numRows( $res ) {
 631+ if ( $res instanceof ResultWrapper ) {
 632+ $res = $res->result;
 633+ }
 634+ @$n = pg_num_rows( $res );
 635+ if( pg_last_error($this->mConn) ) {
 636+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
 637+ }
 638+ return $n;
 639+ }
 640+ function numFields( $res ) {
 641+ if ( $res instanceof ResultWrapper ) {
 642+ $res = $res->result;
 643+ }
 644+ return pg_num_fields( $res );
 645+ }
 646+ function fieldName( $res, $n ) {
 647+ if ( $res instanceof ResultWrapper ) {
 648+ $res = $res->result;
 649+ }
 650+ return pg_field_name( $res, $n );
 651+ }
 652+
 653+ /**
 654+ * This must be called after nextSequenceVal
 655+ */
 656+ function insertId() {
 657+ return $this->mInsertId;
 658+ }
 659+
 660+ function dataSeek( $res, $row ) {
 661+ if ( $res instanceof ResultWrapper ) {
 662+ $res = $res->result;
 663+ }
 664+ return pg_result_seek( $res, $row );
 665+ }
 666+
 667+ function lastError() {
 668+ if ( $this->mConn ) {
 669+ return pg_last_error();
 670+ }
 671+ else {
 672+ return "No database connection";
 673+ }
 674+ }
 675+ function lastErrno() {
 676+ return pg_last_error() ? 1 : 0;
 677+ }
 678+
 679+ function affectedRows() {
 680+ if ( !is_null( $this->mAffectedRows ) ) {
 681+ // Forced result for simulated queries
 682+ return $this->mAffectedRows;
 683+ }
 684+ if( empty( $this->mLastResult ) )
 685+ return 0;
 686+ return pg_affected_rows( $this->mLastResult );
 687+ }
 688+
 689+ /**
 690+ * Estimate rows in dataset
 691+ * Returns estimated count, based on EXPLAIN output
 692+ * This is not necessarily an accurate estimate, so use sparingly
 693+ * Returns -1 if count cannot be found
 694+ * Takes same arguments as Database::select()
 695+ */
 696+
 697+ function estimateRowCount( $table, $vars='*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) {
 698+ $options['EXPLAIN'] = true;
 699+ $res = $this->select( $table, $vars, $conds, $fname, $options );
 700+ $rows = -1;
 701+ if ( $res ) {
 702+ $row = $this->fetchRow( $res );
 703+ $count = array();
 704+ if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
 705+ $rows = $count[1];
 706+ }
 707+ $this->freeResult($res);
 708+ }
 709+ return $rows;
 710+ }
 711+
 712+
 713+ /**
 714+ * Returns information about an index
 715+ * If errors are explicitly ignored, returns NULL on failure
 716+ */
 717+ function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) {
 718+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
 719+ $res = $this->query( $sql, $fname );
 720+ if ( !$res ) {
 721+ return null;
 722+ }
 723+ while ( $row = $this->fetchObject( $res ) ) {
 724+ if ( $row->indexname == $this->indexName( $index ) ) {
 725+ return $row;
 726+ }
 727+ }
 728+ return false;
 729+ }
 730+
 731+ function indexUnique ($table, $index, $fname = 'DatabasePostgres::indexUnique' ) {
 732+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
 733+ " AND indexdef LIKE 'CREATE UNIQUE%(" .
 734+ $this->strencode( $this->indexName( $index ) ) .
 735+ ")'";
 736+ $res = $this->query( $sql, $fname );
 737+ if ( !$res )
 738+ return null;
 739+ while ($row = $this->fetchObject( $res ))
 740+ return true;
 741+ return false;
 742+
 743+ }
 744+
 745+ /**
 746+ * INSERT wrapper, inserts an array into a table
 747+ *
 748+ * $args may be a single associative array, or an array of these with numeric keys,
 749+ * for multi-row insert (Postgres version 8.2 and above only).
 750+ *
 751+ * @param $table String: Name of the table to insert to.
 752+ * @param $args Array: Items to insert into the table.
 753+ * @param $fname String: Name of the function, for profiling
 754+ * @param $options String or Array. Valid options: IGNORE
 755+ *
 756+ * @return bool Success of insert operation. IGNORE always returns true.
 757+ */
 758+ function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
 759+ global $wgDBversion;
 760+
 761+ if ( !count( $args ) ) {
 762+ return true;
 763+ }
 764+
 765+ $table = $this->tableName( $table );
 766+ if (! isset( $wgDBversion ) ) {
 767+ $wgDBversion = $this->getServerVersion();
 768+ }
 769+
 770+ if ( !is_array( $options ) )
 771+ $options = array( $options );
 772+
 773+ if ( isset( $args[0] ) && is_array( $args[0] ) ) {
 774+ $multi = true;
 775+ $keys = array_keys( $args[0] );
 776+ }
 777+ else {
 778+ $multi = false;
 779+ $keys = array_keys( $args );
 780+ }
 781+
 782+ // If IGNORE is set, we use savepoints to emulate mysql's behavior
 783+ $ignore = in_array( 'IGNORE', $options ) ? 'mw' : '';
 784+
 785+ // If we are not in a transaction, we need to be for savepoint trickery
 786+ $didbegin = 0;
 787+ if ( $ignore ) {
 788+ if (! $this->mTrxLevel) {
 789+ $this->begin();
 790+ $didbegin = 1;
 791+ }
 792+ $olde = error_reporting( 0 );
 793+ // For future use, we may want to track the number of actual inserts
 794+ // Right now, insert (all writes) simply return true/false
 795+ $numrowsinserted = 0;
 796+ }
 797+
 798+ $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
 799+
 800+ if ( $multi ) {
 801+ if ( $wgDBversion >= 8.2 && !$ignore ) {
 802+ $first = true;
 803+ foreach ( $args as $row ) {
 804+ if ( $first ) {
 805+ $first = false;
 806+ } else {
 807+ $sql .= ',';
 808+ }
 809+ $sql .= '(' . $this->makeList( $row ) . ')';
 810+ }
 811+ $res = (bool)$this->query( $sql, $fname, $ignore );
 812+ }
 813+ else {
 814+ $res = true;
 815+ $origsql = $sql;
 816+ foreach ( $args as $row ) {
 817+ $tempsql = $origsql;
 818+ $tempsql .= '(' . $this->makeList( $row ) . ')';
 819+
 820+ if ( $ignore ) {
 821+ pg_query($this->mConn, "SAVEPOINT $ignore");
 822+ }
 823+
 824+ $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
 825+
 826+ if ( $ignore ) {
 827+ $bar = pg_last_error();
 828+ if ($bar != false) {
 829+ pg_query( $this->mConn, "ROLLBACK TO $ignore" );
 830+ }
 831+ else {
 832+ pg_query( $this->mConn, "RELEASE $ignore" );
 833+ $numrowsinserted++;
 834+ }
 835+ }
 836+
 837+ // If any of them fail, we fail overall for this function call
 838+ // Note that this will be ignored if IGNORE is set
 839+ if (! $tempres)
 840+ $res = false;
 841+ }
 842+ }
 843+ }
 844+ else {
 845+ // Not multi, just a lone insert
 846+ if ( $ignore ) {
 847+ pg_query($this->mConn, "SAVEPOINT $ignore");
 848+ }
 849+
 850+ $sql .= '(' . $this->makeList( $args ) . ')';
 851+ $res = (bool)$this->query( $sql, $fname, $ignore );
 852+ if ( $ignore ) {
 853+ $bar = pg_last_error();
 854+ if ($bar != false) {
 855+ pg_query( $this->mConn, "ROLLBACK TO $ignore" );
 856+ }
 857+ else {
 858+ pg_query( $this->mConn, "RELEASE $ignore" );
 859+ $numrowsinserted++;
 860+ }
 861+ }
 862+ }
 863+ if ( $ignore ) {
 864+ $olde = error_reporting( $olde );
 865+ if ($didbegin) {
 866+ $this->commit();
 867+ }
 868+
 869+ // Set the affected row count for the whole operation
 870+ $this->mAffectedRows = $numrowsinserted;
 871+
 872+ // IGNORE always returns true
 873+ return true;
 874+ }
 875+
 876+
 877+ return $res;
 878+
 879+ }
 880+
 881+ /**
 882+ * INSERT SELECT wrapper
 883+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
 884+ * Source items may be literals rather then field names, but strings should be quoted with Database::addQuotes()
 885+ * $conds may be "*" to copy the whole table
 886+ * srcTable may be an array of tables.
 887+ * @todo FIXME: implement this a little better (seperate select/insert)?
 888+ */
 889+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect',
 890+ $insertOptions = array(), $selectOptions = array() )
 891+ {
 892+ $destTable = $this->tableName( $destTable );
 893+
 894+ // If IGNORE is set, we use savepoints to emulate mysql's behavior
 895+ $ignore = in_array( 'IGNORE', $insertOptions ) ? 'mw' : '';
 896+
 897+ if( is_array( $insertOptions ) ) {
 898+ $insertOptions = implode( ' ', $insertOptions );
 899+ }
 900+ if( !is_array( $selectOptions ) ) {
 901+ $selectOptions = array( $selectOptions );
 902+ }
 903+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
 904+ if( is_array( $srcTable ) ) {
 905+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
 906+ } else {
 907+ $srcTable = $this->tableName( $srcTable );
 908+ }
 909+
 910+ // If we are not in a transaction, we need to be for savepoint trickery
 911+ $didbegin = 0;
 912+ if ( $ignore ) {
 913+ if( !$this->mTrxLevel ) {
 914+ $this->begin();
 915+ $didbegin = 1;
 916+ }
 917+ $olde = error_reporting( 0 );
 918+ $numrowsinserted = 0;
 919+ pg_query( $this->mConn, "SAVEPOINT $ignore");
 920+ }
 921+
 922+ $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
 923+ " SELECT $startOpts " . implode( ',', $varMap ) .
 924+ " FROM $srcTable $useIndex";
 925+
 926+ if ( $conds != '*') {
 927+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
 928+ }
 929+
 930+ $sql .= " $tailOpts";
 931+
 932+ $res = (bool)$this->query( $sql, $fname, $ignore );
 933+ if( $ignore ) {
 934+ $bar = pg_last_error();
 935+ if( $bar != false ) {
 936+ pg_query( $this->mConn, "ROLLBACK TO $ignore" );
 937+ } else {
 938+ pg_query( $this->mConn, "RELEASE $ignore" );
 939+ $numrowsinserted++;
 940+ }
 941+ $olde = error_reporting( $olde );
 942+ if( $didbegin ) {
 943+ $this->commit();
 944+ }
 945+
 946+ // Set the affected row count for the whole operation
 947+ $this->mAffectedRows = $numrowsinserted;
 948+
 949+ // IGNORE always returns true
 950+ return true;
 951+ }
 952+
 953+ return $res;
 954+ }
 955+
 956+ function tableName( $name ) {
 957+ # Replace reserved words with better ones
 958+ switch( $name ) {
 959+ case 'user':
 960+ return 'mwuser';
 961+ case 'text':
 962+ return 'pagecontent';
 963+ default:
 964+ return $name;
 965+ }
 966+ }
 967+
 968+ /**
 969+ * Return the next in a sequence, save the value for retrieval via insertId()
 970+ */
 971+ function nextSequenceValue( $seqName ) {
 972+ $safeseq = preg_replace( "/'/", "''", $seqName );
 973+ $res = $this->query( "SELECT nextval('$safeseq')" );
 974+ $row = $this->fetchRow( $res );
 975+ $this->mInsertId = $row[0];
 976+ $this->freeResult( $res );
 977+ return $this->mInsertId;
 978+ }
 979+
 980+ /**
 981+ * Return the current value of a sequence. Assumes it has been nextval'ed in this session.
 982+ */
 983+ function currentSequenceValue( $seqName ) {
 984+ $safeseq = preg_replace( "/'/", "''", $seqName );
 985+ $res = $this->query( "SELECT currval('$safeseq')" );
 986+ $row = $this->fetchRow( $res );
 987+ $currval = $row[0];
 988+ $this->freeResult( $res );
 989+ return $currval;
 990+ }
 991+
 992+ # REPLACE query wrapper
 993+ # Postgres simulates this with a DELETE followed by INSERT
 994+ # $row is the row to insert, an associative array
 995+ # $uniqueIndexes is an array of indexes. Each element may be either a
 996+ # field name or an array of field names
 997+ #
 998+ # It may be more efficient to leave off unique indexes which are unlikely to collide.
 999+ # However if you do this, you run the risk of encountering errors which wouldn't have
 1000+ # occurred in MySQL
 1001+ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabasePostgres::replace' ) {
 1002+ $table = $this->tableName( $table );
 1003+
 1004+ if (count($rows)==0) {
 1005+ return;
 1006+ }
 1007+
 1008+ # Single row case
 1009+ if ( !is_array( reset( $rows ) ) ) {
 1010+ $rows = array( $rows );
 1011+ }
 1012+
 1013+ foreach( $rows as $row ) {
 1014+ # Delete rows which collide
 1015+ if ( $uniqueIndexes ) {
 1016+ $sql = "DELETE FROM $table WHERE ";
 1017+ $first = true;
 1018+ foreach ( $uniqueIndexes as $index ) {
 1019+ if ( $first ) {
 1020+ $first = false;
 1021+ $sql .= "(";
 1022+ } else {
 1023+ $sql .= ') OR (';
 1024+ }
 1025+ if ( is_array( $index ) ) {
 1026+ $first2 = true;
 1027+ foreach ( $index as $col ) {
 1028+ if ( $first2 ) {
 1029+ $first2 = false;
 1030+ } else {
 1031+ $sql .= ' AND ';
 1032+ }
 1033+ $sql .= $col.'=' . $this->addQuotes( $row[$col] );
 1034+ }
 1035+ } else {
 1036+ $sql .= $index.'=' . $this->addQuotes( $row[$index] );
 1037+ }
 1038+ }
 1039+ $sql .= ')';
 1040+ $this->query( $sql, $fname );
 1041+ }
 1042+
 1043+ # Now insert the row
 1044+ $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
 1045+ $this->makeList( $row, LIST_COMMA ) . ')';
 1046+ $this->query( $sql, $fname );
 1047+ }
 1048+ }
 1049+
 1050+ # DELETE where the condition is a join
 1051+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabasePostgres::deleteJoin' ) {
 1052+ if ( !$conds ) {
 1053+ throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
 1054+ }
 1055+
 1056+ $delTable = $this->tableName( $delTable );
 1057+ $joinTable = $this->tableName( $joinTable );
 1058+ $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
 1059+ if ( $conds != '*' ) {
 1060+ $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
 1061+ }
 1062+ $sql .= ')';
 1063+
 1064+ $this->query( $sql, $fname );
 1065+ }
 1066+
 1067+ # Returns the size of a text field, or -1 for "unlimited"
 1068+ function textFieldSize( $table, $field ) {
 1069+ $table = $this->tableName( $table );
 1070+ $sql = "SELECT t.typname as ftype,a.atttypmod as size
 1071+ FROM pg_class c, pg_attribute a, pg_type t
 1072+ WHERE relname='$table' AND a.attrelid=c.oid AND
 1073+ a.atttypid=t.oid and a.attname='$field'";
 1074+ $res =$this->query($sql);
 1075+ $row=$this->fetchObject($res);
 1076+ if ($row->ftype=="varchar") {
 1077+ $size=$row->size-4;
 1078+ } else {
 1079+ $size=$row->size;
 1080+ }
 1081+ $this->freeResult( $res );
 1082+ return $size;
 1083+ }
 1084+
 1085+ function limitResult($sql, $limit, $offset=false) {
 1086+ return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":"");
 1087+ }
 1088+
 1089+ function wasDeadlock() {
 1090+ return $this->lastErrno() == '40P01';
 1091+ }
 1092+
 1093+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabasePostgres::duplicateTableStructure' ) {
 1094+ return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname );
 1095+ }
 1096+
 1097+ function timestamp( $ts=0 ) {
 1098+ return wfTimestamp(TS_POSTGRES,$ts);
 1099+ }
 1100+
 1101+ /**
 1102+ * Return aggregated value function call
 1103+ */
 1104+ function aggregateValue ($valuedata,$valuename='value') {
 1105+ return $valuedata;
 1106+ }
 1107+
 1108+
 1109+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
 1110+ // Ignore errors during error handling to avoid infinite recursion
 1111+ $ignore = $this->ignoreErrors( true );
 1112+ $this->mErrorCount++;
 1113+
 1114+ if ($ignore || $tempIgnore) {
 1115+ wfDebug("SQL ERROR (ignored): $error\n");
 1116+ $this->ignoreErrors( $ignore );
 1117+ }
 1118+ else {
 1119+ $message = "A database error has occurred\n" .
 1120+ "Query: $sql\n" .
 1121+ "Function: $fname\n" .
 1122+ "Error: $errno $error\n";
 1123+ throw new DBUnexpectedError($this, $message);
 1124+ }
 1125+ }
 1126+
 1127+ /**
 1128+ * @return string wikitext of a link to the server software's web site
 1129+ */
 1130+ function getSoftwareLink() {
 1131+ return "[http://www.postgresql.org/ PostgreSQL]";
 1132+ }
 1133+
 1134+ /**
 1135+ * @return string Version information from the database
 1136+ */
 1137+ function getServerVersion() {
 1138+ $versionInfo = pg_version( $this->mConn );
 1139+ if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) {
 1140+ // Old client, abort install
 1141+ $this->numeric_version = '7.3 or earlier';
 1142+ } elseif ( isset( $versionInfo['server'] ) ) {
 1143+ // Normal client
 1144+ $this->numeric_version = $versionInfo['server'];
 1145+ } else {
 1146+ // Bug 16937: broken pgsql extension from PHP<5.3
 1147+ $this->numeric_version = pg_parameter_status( $this->mConn, 'server_version' );
 1148+ }
 1149+ return $this->numeric_version;
 1150+ }
 1151+
 1152+ /**
 1153+ * Query whether a given relation exists (in the given schema, or the
 1154+ * default mw one if not given)
 1155+ */
 1156+ function relationExists( $table, $types, $schema = false ) {
 1157+ global $wgDBmwschema;
 1158+ if ( !is_array( $types ) )
 1159+ $types = array( $types );
 1160+ if ( !$schema )
 1161+ $schema = $wgDBmwschema;
 1162+ $etable = $this->addQuotes( $table );
 1163+ $eschema = $this->addQuotes( $schema );
 1164+ $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
 1165+ . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
 1166+ . "AND c.relkind IN ('" . implode("','", $types) . "')";
 1167+ $res = $this->query( $SQL );
 1168+ $count = $res ? $res->numRows() : 0;
 1169+ if ($res)
 1170+ $this->freeResult( $res );
 1171+ return $count ? true : false;
 1172+ }
 1173+
 1174+ /*
 1175+ * For backward compatibility, this function checks both tables and
 1176+ * views.
 1177+ */
 1178+ function tableExists( $table, $schema = false ) {
 1179+ return $this->relationExists( $table, array( 'r', 'v' ), $schema );
 1180+ }
 1181+
 1182+ function sequenceExists( $sequence, $schema = false ) {
 1183+ return $this->relationExists( $sequence, 'S', $schema );
 1184+ }
 1185+
 1186+ function triggerExists( $table, $trigger ) {
 1187+ global $wgDBmwschema;
 1188+
 1189+ $q = <<<SQL
 1190+ SELECT 1 FROM pg_class, pg_namespace, pg_trigger
 1191+ WHERE relnamespace=pg_namespace.oid AND relkind='r'
 1192+ AND tgrelid=pg_class.oid
 1193+ AND nspname=%s AND relname=%s AND tgname=%s
 1194+SQL;
 1195+ $res = $this->query(sprintf($q,
 1196+ $this->addQuotes($wgDBmwschema),
 1197+ $this->addQuotes($table),
 1198+ $this->addQuotes($trigger)));
 1199+ if (!$res)
 1200+ return null;
 1201+ $rows = $res->numRows();
 1202+ $this->freeResult( $res );
 1203+ return $rows;
 1204+ }
 1205+
 1206+ function ruleExists( $table, $rule ) {
 1207+ global $wgDBmwschema;
 1208+ $exists = $this->selectField("pg_rules", "rulename",
 1209+ array( "rulename" => $rule,
 1210+ "tablename" => $table,
 1211+ "schemaname" => $wgDBmwschema ) );
 1212+ return $exists === $rule;
 1213+ }
 1214+
 1215+ function constraintExists( $table, $constraint ) {
 1216+ global $wgDBmwschema;
 1217+ $SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ".
 1218+ "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
 1219+ $this->addQuotes($wgDBmwschema),
 1220+ $this->addQuotes($table),
 1221+ $this->addQuotes($constraint));
 1222+ $res = $this->query($SQL);
 1223+ if (!$res)
 1224+ return null;
 1225+ $rows = $res->numRows();
 1226+ $this->freeResult($res);
 1227+ return $rows;
 1228+ }
 1229+
 1230+ /**
 1231+ * Query whether a given schema exists. Returns the name of the owner
 1232+ */
 1233+ function schemaExists( $schema ) {
 1234+ $eschema = preg_replace("/'/", "''", $schema);
 1235+ $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r "
 1236+ ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'";
 1237+ $res = $this->query( $SQL );
 1238+ if ( $res && $res->numRows() ) {
 1239+ $row = $res->fetchObject();
 1240+ $owner = $row->rolname;
 1241+ } else {
 1242+ $owner = false;
 1243+ }
 1244+ if ($res)
 1245+ $this->freeResult($res);
 1246+ return $owner;
 1247+ }
 1248+
 1249+ /**
 1250+ * Query whether a given column exists in the mediawiki schema
 1251+ */
 1252+ function fieldExists( $table, $field, $fname = 'DatabasePostgres::fieldExists' ) {
 1253+ global $wgDBmwschema;
 1254+ $etable = preg_replace("/'/", "''", $table);
 1255+ $eschema = preg_replace("/'/", "''", $wgDBmwschema);
 1256+ $ecol = preg_replace("/'/", "''", $field);
 1257+ $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a "
 1258+ . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' "
 1259+ . "AND a.attrelid = c.oid AND a.attname = '$ecol'";
 1260+ $res = $this->query( $SQL, $fname );
 1261+ $count = $res ? $res->numRows() : 0;
 1262+ if ($res)
 1263+ $this->freeResult( $res );
 1264+ return $count;
 1265+ }
 1266+
 1267+ function fieldInfo( $table, $field ) {
 1268+ return PostgresField::fromText($this, $table, $field);
 1269+ }
 1270+
 1271+ /**
 1272+ * pg_field_type() wrapper
 1273+ */
 1274+ function fieldType( $res, $index ) {
 1275+ if ( $res instanceof ResultWrapper ) {
 1276+ $res = $res->result;
 1277+ }
 1278+ return pg_field_type( $res, $index );
 1279+ }
 1280+
 1281+ function begin( $fname = 'DatabasePostgres::begin' ) {
 1282+ $this->query( 'BEGIN', $fname );
 1283+ $this->mTrxLevel = 1;
 1284+ }
 1285+ function immediateCommit( $fname = 'DatabasePostgres::immediateCommit' ) {
 1286+ return true;
 1287+ }
 1288+ function commit( $fname = 'DatabasePostgres::commit' ) {
 1289+ $this->query( 'COMMIT', $fname );
 1290+ $this->mTrxLevel = 0;
 1291+ }
 1292+
 1293+ /* Not even sure why this is used in the main codebase... */
 1294+ function limitResultForUpdate( $sql, $num ) {
 1295+ return $sql;
 1296+ }
 1297+
 1298+ function setup_database() {
 1299+ global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
 1300+
 1301+ // Make sure that we can write to the correct schema
 1302+ // If not, Postgres will happily and silently go to the next search_path item
 1303+ $ctest = "mediawiki_test_table";
 1304+ $safeschema = $this->quote_ident($wgDBmwschema);
 1305+ if ($this->tableExists($ctest, $wgDBmwschema)) {
 1306+ $this->doQuery("DROP TABLE $safeschema.$ctest");
 1307+ }
 1308+ $SQL = "CREATE TABLE $safeschema.$ctest(a int)";
 1309+ $olde = error_reporting( 0 );
 1310+ $res = $this->doQuery($SQL);
 1311+ error_reporting( $olde );
 1312+ if (!$res) {
 1313+ print "<b>FAILED</b>. Make sure that the user \"" . htmlspecialchars( $wgDBuser ) .
 1314+ "\" can write to the schema \"" . htmlspecialchars( $wgDBmwschema ) . "\"</li>\n";
 1315+ dieout("</ul>");
 1316+ }
 1317+ $this->doQuery("DROP TABLE $safeschema.$ctest");
 1318+
 1319+ $res = $this->sourceFile( "../maintenance/postgres/tables.sql" );
 1320+
 1321+ ## Update version information
 1322+ $mwv = $this->addQuotes($wgVersion);
 1323+ $pgv = $this->addQuotes($this->getServerVersion());
 1324+ $pgu = $this->addQuotes($this->mUser);
 1325+ $mws = $this->addQuotes($wgDBmwschema);
 1326+ $tss = $this->addQuotes($wgDBts2schema);
 1327+ $pgp = $this->addQuotes($wgDBport);
 1328+ $dbn = $this->addQuotes($this->mDBname);
 1329+ $ctype = $this->addQuotes( pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0) );
 1330+
 1331+ $SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ".
 1332+ "mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ".
 1333+ "ctype = $ctype ".
 1334+ "WHERE type = 'Creation'";
 1335+ $this->query($SQL);
 1336+
 1337+ ## Avoid the non-standard "REPLACE INTO" syntax
 1338+ $f = fopen( "../maintenance/interwiki.sql", 'r' );
 1339+ if ($f == false ) {
 1340+ dieout( "<li>Could not find the interwiki.sql file");
 1341+ }
 1342+ ## We simply assume it is already empty as we have just created it
 1343+ $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
 1344+ while ( ! feof( $f ) ) {
 1345+ $line = fgets($f,1024);
 1346+ $matches = array();
 1347+ if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) {
 1348+ continue;
 1349+ }
 1350+ $this->query("$SQL $matches[1],$matches[2])");
 1351+ }
 1352+ print " (table interwiki successfully populated)...\n";
 1353+
 1354+ $this->doQuery("COMMIT");
 1355+ }
 1356+
 1357+ function encodeBlob( $b ) {
 1358+ return new Blob ( pg_escape_bytea( $b ) ) ;
 1359+ }
 1360+
 1361+ function decodeBlob( $b ) {
 1362+ if ($b instanceof Blob) {
 1363+ $b = $b->fetch();
 1364+ }
 1365+ return pg_unescape_bytea( $b );
 1366+ }
 1367+
 1368+ function strencode( $s ) { ## Should not be called by us
 1369+ return pg_escape_string( $s );
 1370+ }
 1371+
 1372+ function addQuotes( $s ) {
 1373+ if ( is_null( $s ) ) {
 1374+ return 'NULL';
 1375+ } else if ( is_bool( $s ) ) {
 1376+ return intval( $s );
 1377+ } else if ($s instanceof Blob) {
 1378+ return "'".$s->fetch($s)."'";
 1379+ }
 1380+ return "'" . pg_escape_string($s) . "'";
 1381+ }
 1382+
 1383+ function quote_ident( $s ) {
 1384+ return '"' . preg_replace( '/"/', '""', $s) . '"';
 1385+ }
 1386+
 1387+ /**
 1388+ * Postgres specific version of replaceVars.
 1389+ * Calls the parent version in Database.php
 1390+ *
 1391+ * @private
 1392+ *
 1393+ * @param $ins String: SQL string, read from a stream (usually tables.sql)
 1394+ *
 1395+ * @return string SQL string
 1396+ */
 1397+ protected function replaceVars( $ins ) {
 1398+
 1399+ $ins = parent::replaceVars( $ins );
 1400+
 1401+ if ($this->numeric_version >= 8.3) {
 1402+ // Thanks for not providing backwards-compatibility, 8.3
 1403+ $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
 1404+ }
 1405+
 1406+ if ($this->numeric_version <= 8.1) { // Our minimum version
 1407+ $ins = str_replace( 'USING gin', 'USING gist', $ins );
 1408+ }
 1409+
 1410+ return $ins;
 1411+ }
 1412+
 1413+ /**
 1414+ * Various select options
 1415+ *
 1416+ * @private
 1417+ *
 1418+ * @param $options Array: an associative array of options to be turned into
 1419+ * an SQL query, valid keys are listed in the function.
 1420+ * @return array
 1421+ */
 1422+ function makeSelectOptions( $options ) {
 1423+ $preLimitTail = $postLimitTail = '';
 1424+ $startOpts = $useIndex = '';
 1425+
 1426+ $noKeyOptions = array();
 1427+ foreach ( $options as $key => $option ) {
 1428+ if ( is_numeric( $key ) ) {
 1429+ $noKeyOptions[$option] = true;
 1430+ }
 1431+ }
 1432+
 1433+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY'];
 1434+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
 1435+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY'];
 1436+
 1437+ //if (isset($options['LIMIT'])) {
 1438+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
 1439+ // isset($options['OFFSET']) ? $options['OFFSET']
 1440+ // : false);
 1441+ //}
 1442+
 1443+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
 1444+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
 1445+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
 1446+
 1447+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
 1448+ }
 1449+
 1450+ /**
 1451+ * How lagged is this slave?
 1452+ *
 1453+ */
 1454+ public function getLag() {
 1455+ # Not implemented for PostgreSQL
 1456+ return false;
 1457+ }
 1458+
 1459+ function setFakeSlaveLag( $lag ) {}
 1460+ function setFakeMaster( $enabled = true ) {}
 1461+
 1462+ function getDBname() {
 1463+ return $this->mDBname;
 1464+ }
 1465+
 1466+ function getServer() {
 1467+ return $this->mServer;
 1468+ }
 1469+
 1470+ function buildConcat( $stringList ) {
 1471+ return implode( ' || ', $stringList );
 1472+ }
 1473+
 1474+ public function getSearchEngine() {
 1475+ return "SearchPostgres";
 1476+ }
 1477+} // end DatabasePostgres class
Property changes on: trunk/extensions/MSSQLBackCompat/db/DatabasePostgres.php
___________________________________________________________________
Added: svn:keywords
11478 + Author Date Id Revision
Added: svn:eol-style
21479 + native
Index: trunk/extensions/MSSQLBackCompat/db/LBFactory_Multi.php
@@ -0,0 +1,243 @@
 2+<?php
 3+/**
 4+ * @file
 5+ * @ingroup Database
 6+ */
 7+
 8+
 9+/**
 10+ * A multi-wiki, multi-master factory for Wikimedia and similar installations.
 11+ * Ignores the old configuration globals
 12+ *
 13+ * Configuration:
 14+ * sectionsByDB A map of database names to section names
 15+ *
 16+ * sectionLoads A 2-d map. For each section, gives a map of server names to load ratios.
 17+ * For example: array( 'section1' => array( 'db1' => 100, 'db2' => 100 ) )
 18+ *
 19+ * serverTemplate A server info associative array as documented for $wgDBservers. The host,
 20+ * hostName and load entries will be overridden.
 21+ *
 22+ * groupLoadsBySection A 3-d map giving server load ratios for each section and group. For example:
 23+ * array( 'section1' => array( 'group1' => array( 'db1' => 100, 'db2' => 100 ) ) )
 24+ *
 25+ * groupLoadsByDB A 3-d map giving server load ratios by DB name.
 26+ *
 27+ * hostsByName A map of hostname to IP address.
 28+ *
 29+ * externalLoads A map of external storage cluster name to server load map
 30+ *
 31+ * externalTemplateOverrides A set of server info keys overriding serverTemplate for external storage
 32+ *
 33+ * templateOverridesByServer A 2-d map overriding serverTemplate and externalTemplateOverrides on a
 34+ * server-by-server basis. Applies to both core and external storage.
 35+ *
 36+ * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster
 37+ *
 38+ * masterTemplateOverrides An override array for all master servers.
 39+ *
 40+ * readOnlyBySection A map of section name to read-only message. Missing or false for read/write.
 41+ *
 42+ * @ingroup Database
 43+ */
 44+class LBFactory_Multi extends LBFactory {
 45+ // Required settings
 46+ var $sectionsByDB, $sectionLoads, $serverTemplate;
 47+ // Optional settings
 48+ var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array();
 49+ var $externalLoads = array(), $externalTemplateOverrides, $templateOverridesByServer;
 50+ var $templateOverridesByCluster, $masterTemplateOverrides, $readOnlyBySection = array();
 51+ // Other stuff
 52+ var $conf, $mainLBs = array(), $extLBs = array();
 53+ var $lastWiki, $lastSection;
 54+
 55+ function __construct( $conf ) {
 56+ $this->chronProt = new ChronologyProtector;
 57+ $this->conf = $conf;
 58+ $required = array( 'sectionsByDB', 'sectionLoads', 'serverTemplate' );
 59+ $optional = array( 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
 60+ 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
 61+ 'templateOverridesByCluster', 'masterTemplateOverrides',
 62+ 'readOnlyBySection' );
 63+
 64+ foreach ( $required as $key ) {
 65+ if ( !isset( $conf[$key] ) ) {
 66+ throw new MWException( __CLASS__.": $key is required in configuration" );
 67+ }
 68+ $this->$key = $conf[$key];
 69+ }
 70+
 71+ foreach ( $optional as $key ) {
 72+ if ( isset( $conf[$key] ) ) {
 73+ $this->$key = $conf[$key];
 74+ }
 75+ }
 76+
 77+ // Check for read-only mode
 78+ $section = $this->getSectionForWiki();
 79+ if ( !empty( $this->readOnlyBySection[$section] ) ) {
 80+ global $wgReadOnly;
 81+ $wgReadOnly = $this->readOnlyBySection[$section];
 82+ }
 83+ }
 84+
 85+ function getSectionForWiki( $wiki = false ) {
 86+ if ( $this->lastWiki === $wiki ) {
 87+ return $this->lastSection;
 88+ }
 89+ list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
 90+ if ( isset( $this->sectionsByDB[$dbName] ) ) {
 91+ $section = $this->sectionsByDB[$dbName];
 92+ } else {
 93+ $section = 'DEFAULT';
 94+ }
 95+ $this->lastSection = $section;
 96+ $this->lastWiki = $wiki;
 97+ return $section;
 98+ }
 99+
 100+ function newMainLB( $wiki = false ) {
 101+ list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
 102+ $section = $this->getSectionForWiki( $wiki );
 103+ $groupLoads = array();
 104+ if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
 105+ $groupLoads = $this->groupLoadsByDB[$dbName];
 106+ }
 107+ if ( isset( $this->groupLoadsBySection[$section] ) ) {
 108+ $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] );
 109+ }
 110+ return $this->newLoadBalancer( $this->serverTemplate, $this->sectionLoads[$section], $groupLoads );
 111+ }
 112+
 113+ function getMainLB( $wiki = false ) {
 114+ $section = $this->getSectionForWiki( $wiki );
 115+ if ( !isset( $this->mainLBs[$section] ) ) {
 116+ $lb = $this->newMainLB( $wiki, $section );
 117+ $this->chronProt->initLB( $lb );
 118+ $lb->parentInfo( array( 'id' => "main-$section" ) );
 119+ $this->mainLBs[$section] = $lb;
 120+ }
 121+ return $this->mainLBs[$section];
 122+ }
 123+
 124+ function newExternalLB( $cluster, $wiki = false ) {
 125+ if ( !isset( $this->externalLoads[$cluster] ) ) {
 126+ throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
 127+ }
 128+ $template = $this->serverTemplate;
 129+ if ( isset( $this->externalTemplateOverrides ) ) {
 130+ $template = $this->externalTemplateOverrides + $template;
 131+ }
 132+ if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
 133+ $template = $this->templateOverridesByCluster[$cluster] + $template;
 134+ }
 135+ return $this->newLoadBalancer( $template, $this->externalLoads[$cluster], array() );
 136+ }
 137+
 138+ function &getExternalLB( $cluster, $wiki = false ) {
 139+ if ( !isset( $this->extLBs[$cluster] ) ) {
 140+ $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
 141+ $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
 142+ }
 143+ return $this->extLBs[$cluster];
 144+ }
 145+
 146+ /**
 147+ * Make a new load balancer object based on template and load array
 148+ */
 149+ function newLoadBalancer( $template, $loads, $groupLoads ) {
 150+ global $wgMasterWaitTimeout;
 151+ $servers = $this->makeServerArray( $template, $loads, $groupLoads );
 152+ $lb = new LoadBalancer( array(
 153+ 'servers' => $servers,
 154+ 'masterWaitTimeout' => $wgMasterWaitTimeout
 155+ ));
 156+ return $lb;
 157+ }
 158+
 159+ /**
 160+ * Make a server array as expected by LoadBalancer::__construct, using a template and load array
 161+ */
 162+ function makeServerArray( $template, $loads, $groupLoads ) {
 163+ $servers = array();
 164+ $master = true;
 165+ $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
 166+ foreach ( $groupLoadsByServer as $server => $stuff ) {
 167+ if ( !isset( $loads[$server] ) ) {
 168+ $loads[$server] = 0;
 169+ }
 170+ }
 171+ foreach ( $loads as $serverName => $load ) {
 172+ $serverInfo = $template;
 173+ if ( $master ) {
 174+ $serverInfo['master'] = true;
 175+ if ( isset( $this->masterTemplateOverrides ) ) {
 176+ $serverInfo = $this->masterTemplateOverrides + $serverInfo;
 177+ }
 178+ $master = false;
 179+ }
 180+ if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
 181+ $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
 182+ }
 183+ if ( isset( $groupLoadsByServer[$serverName] ) ) {
 184+ $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
 185+ }
 186+ if ( isset( $this->hostsByName[$serverName] ) ) {
 187+ $serverInfo['host'] = $this->hostsByName[$serverName];
 188+ } else {
 189+ $serverInfo['host'] = $serverName;
 190+ }
 191+ $serverInfo['hostName'] = $serverName;
 192+ $serverInfo['load'] = $load;
 193+ $servers[] = $serverInfo;
 194+ }
 195+ return $servers;
 196+ }
 197+
 198+ /**
 199+ * Take a group load array indexed by group then server, and reindex it by server then group
 200+ */
 201+ function reindexGroupLoads( $groupLoads ) {
 202+ $reindexed = array();
 203+ foreach ( $groupLoads as $group => $loads ) {
 204+ foreach ( $loads as $server => $load ) {
 205+ $reindexed[$server][$group] = $load;
 206+ }
 207+ }
 208+ return $reindexed;
 209+ }
 210+
 211+ /**
 212+ * Get the database name and prefix based on the wiki ID
 213+ */
 214+ function getDBNameAndPrefix( $wiki = false ) {
 215+ if ( $wiki === false ) {
 216+ global $wgDBname, $wgDBprefix;
 217+ return array( $wgDBname, $wgDBprefix );
 218+ } else {
 219+ return wfSplitWikiID( $wiki );
 220+ }
 221+ }
 222+
 223+ /**
 224+ * Execute a function for each tracked load balancer
 225+ * The callback is called with the load balancer as the first parameter,
 226+ * and $params passed as the subsequent parameters.
 227+ */
 228+ function forEachLB( $callback, $params = array() ) {
 229+ foreach ( $this->mainLBs as $lb ) {
 230+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
 231+ }
 232+ foreach ( $this->extLBs as $lb ) {
 233+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
 234+ }
 235+ }
 236+
 237+ function shutdown() {
 238+ foreach ( $this->mainLBs as $lb ) {
 239+ $this->chronProt->shutdownLB( $lb );
 240+ }
 241+ $this->chronProt->shutdown();
 242+ $this->commitMasterChanges();
 243+ }
 244+}
Property changes on: trunk/extensions/MSSQLBackCompat/db/LBFactory_Multi.php
___________________________________________________________________
Added: svn:eol-style
1245 + native
Index: trunk/extensions/MSSQLBackCompat/db/DatabaseIbm_db2.php
@@ -0,0 +1,1837 @@
 2+<?php
 3+/**
 4+ * This script is the IBM DB2 database abstraction layer
 5+ *
 6+ * See maintenance/ibm_db2/README for development notes and other specific information
 7+ * @ingroup Database
 8+ * @file
 9+ * @author leo.petr+mediawiki@gmail.com
 10+ */
 11+
 12+/**
 13+ * This represents a column in a DB2 database
 14+ * @ingroup Database
 15+ */
 16+class IBM_DB2Field {
 17+ private $name = '';
 18+ private $tablename = '';
 19+ private $type = '';
 20+ private $nullable = false;
 21+ private $max_length = 0;
 22+
 23+ /**
 24+ * Builder method for the class
 25+ * @param DatabaseIbm_db2 $db Database interface
 26+ * @param string $table table name
 27+ * @param string $field column name
 28+ * @return IBM_DB2Field
 29+ */
 30+ static function fromText($db, $table, $field) {
 31+ global $wgDBmwschema;
 32+
 33+ $q = <<<SQL
 34+SELECT
 35+lcase(coltype) AS typname,
 36+nulls AS attnotnull, length AS attlen
 37+FROM sysibm.syscolumns
 38+WHERE tbcreator=%s AND tbname=%s AND name=%s;
 39+SQL;
 40+ $res = $db->query(sprintf($q,
 41+ $db->addQuotes($wgDBmwschema),
 42+ $db->addQuotes($table),
 43+ $db->addQuotes($field)));
 44+ $row = $db->fetchObject($res);
 45+ if (!$row)
 46+ return null;
 47+ $n = new IBM_DB2Field;
 48+ $n->type = $row->typname;
 49+ $n->nullable = ($row->attnotnull == 'N');
 50+ $n->name = $field;
 51+ $n->tablename = $table;
 52+ $n->max_length = $row->attlen;
 53+ return $n;
 54+ }
 55+ /**
 56+ * Get column name
 57+ * @return string column name
 58+ */
 59+ function name() { return $this->name; }
 60+ /**
 61+ * Get table name
 62+ * @return string table name
 63+ */
 64+ function tableName() { return $this->tablename; }
 65+ /**
 66+ * Get column type
 67+ * @return string column type
 68+ */
 69+ function type() { return $this->type; }
 70+ /**
 71+ * Can column be null?
 72+ * @return bool true or false
 73+ */
 74+ function nullable() { return $this->nullable; }
 75+ /**
 76+ * How much can you fit in the column per row?
 77+ * @return int length
 78+ */
 79+ function maxLength() { return $this->max_length; }
 80+}
 81+
 82+/**
 83+ * Wrapper around binary large objects
 84+ * @ingroup Database
 85+ */
 86+class IBM_DB2Blob {
 87+ private $mData;
 88+
 89+ public function __construct($data) {
 90+ $this->mData = $data;
 91+ }
 92+
 93+ public function getData() {
 94+ return $this->mData;
 95+ }
 96+
 97+ public function __toString()
 98+ {
 99+ return $this->mData;
 100+ }
 101+}
 102+
 103+/**
 104+ * Primary database interface
 105+ * @ingroup Database
 106+ */
 107+class DatabaseIbm_db2 extends DatabaseBase {
 108+ /*
 109+ * Inherited members
 110+ protected $mLastQuery = '';
 111+ protected $mPHPError = false;
 112+
 113+ protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
 114+ protected $mOut, $mOpened = false;
 115+
 116+ protected $mFailFunction;
 117+ protected $mTablePrefix;
 118+ protected $mFlags;
 119+ protected $mTrxLevel = 0;
 120+ protected $mErrorCount = 0;
 121+ protected $mLBInfo = array();
 122+ protected $mFakeSlaveLag = null, $mFakeMaster = false;
 123+ *
 124+ */
 125+
 126+ /// Server port for uncataloged connections
 127+ protected $mPort = null;
 128+ /// Whether connection is cataloged
 129+ protected $mCataloged = null;
 130+ /// Schema for tables, stored procedures, triggers
 131+ protected $mSchema = null;
 132+ /// Whether the schema has been applied in this session
 133+ protected $mSchemaSet = false;
 134+ /// Result of last query
 135+ protected $mLastResult = null;
 136+ /// Number of rows affected by last INSERT/UPDATE/DELETE
 137+ protected $mAffectedRows = null;
 138+ /// Number of rows returned by last SELECT
 139+ protected $mNumRows = null;
 140+
 141+ /// Connection config options - see constructor
 142+ public $mConnOptions = array();
 143+ /// Statement config options -- see constructor
 144+ public $mStmtOptions = array();
 145+
 146+
 147+ const CATALOGED = "cataloged";
 148+ const UNCATALOGED = "uncataloged";
 149+ const USE_GLOBAL = "get from global";
 150+
 151+ const NONE_OPTION = 0x00;
 152+ const CONN_OPTION = 0x01;
 153+ const STMT_OPTION = 0x02;
 154+
 155+ const REGULAR_MODE = 'regular';
 156+ const INSTALL_MODE = 'install';
 157+
 158+ // Whether this is regular operation or the initial installation
 159+ protected $mMode = self::REGULAR_MODE;
 160+
 161+ /// Last sequence value used for a primary key
 162+ protected $mInsertId = null;
 163+
 164+ /*
 165+ * These can be safely inherited
 166+ *
 167+ * Getter/Setter: (18)
 168+ * failFunction
 169+ * setOutputPage
 170+ * bufferResults
 171+ * ignoreErrors
 172+ * trxLevel
 173+ * errorCount
 174+ * getLBInfo
 175+ * setLBInfo
 176+ * lastQuery
 177+ * isOpen
 178+ * setFlag
 179+ * clearFlag
 180+ * getFlag
 181+ * getProperty
 182+ * getDBname
 183+ * getServer
 184+ * tableNameCallback
 185+ * tablePrefix
 186+ *
 187+ * Administrative: (8)
 188+ * debug
 189+ * installErrorHandler
 190+ * restoreErrorHandler
 191+ * connectionErrorHandler
 192+ * reportConnectionError
 193+ * sourceFile
 194+ * sourceStream
 195+ * replaceVars
 196+ *
 197+ * Database: (5)
 198+ * query
 199+ * set
 200+ * selectField
 201+ * generalizeSQL
 202+ * update
 203+ * strreplace
 204+ * deadlockLoop
 205+ *
 206+ * Prepared Statement: 6
 207+ * prepare
 208+ * freePrepared
 209+ * execute
 210+ * safeQuery
 211+ * fillPrepared
 212+ * fillPreparedArg
 213+ *
 214+ * Slave/Master: (4)
 215+ * masterPosWait
 216+ * getSlavePos
 217+ * getMasterPos
 218+ * getLag
 219+ *
 220+ * Generation: (9)
 221+ * tableNames
 222+ * tableNamesN
 223+ * tableNamesWithUseIndexOrJOIN
 224+ * escapeLike
 225+ * delete
 226+ * insertSelect
 227+ * timestampOrNull
 228+ * resultObject
 229+ * aggregateValue
 230+ * selectSQLText
 231+ * selectRow
 232+ * makeUpdateOptions
 233+ *
 234+ * Reflection: (1)
 235+ * indexExists
 236+ */
 237+
 238+ /*
 239+ * These have been implemented
 240+ *
 241+ * Administrative: 7 / 7
 242+ * constructor [Done]
 243+ * open [Done]
 244+ * openCataloged [Done]
 245+ * close [Done]
 246+ * newFromParams [Done]
 247+ * openUncataloged [Done]
 248+ * setup_database [Done]
 249+ *
 250+ * Getter/Setter: 13 / 13
 251+ * cascadingDeletes [Done]
 252+ * cleanupTriggers [Done]
 253+ * strictIPs [Done]
 254+ * realTimestamps [Done]
 255+ * impliciGroupby [Done]
 256+ * implicitOrderby [Done]
 257+ * searchableIPs [Done]
 258+ * functionalIndexes [Done]
 259+ * getWikiID [Done]
 260+ * isOpen [Done]
 261+ * getServerVersion [Done]
 262+ * getSoftwareLink [Done]
 263+ * getSearchEngine [Done]
 264+ *
 265+ * Database driver wrapper: 23 / 23
 266+ * lastError [Done]
 267+ * lastErrno [Done]
 268+ * doQuery [Done]
 269+ * tableExists [Done]
 270+ * fetchObject [Done]
 271+ * fetchRow [Done]
 272+ * freeResult [Done]
 273+ * numRows [Done]
 274+ * numFields [Done]
 275+ * fieldName [Done]
 276+ * insertId [Done]
 277+ * dataSeek [Done]
 278+ * affectedRows [Done]
 279+ * selectDB [Done]
 280+ * strencode [Done]
 281+ * conditional [Done]
 282+ * wasDeadlock [Done]
 283+ * ping [Done]
 284+ * getStatus [Done]
 285+ * setTimeout [Done]
 286+ * lock [Done]
 287+ * unlock [Done]
 288+ * insert [Done]
 289+ * select [Done]
 290+ *
 291+ * Slave/master: 2 / 2
 292+ * setFakeSlaveLag [Done]
 293+ * setFakeMaster [Done]
 294+ *
 295+ * Reflection: 6 / 6
 296+ * fieldExists [Done]
 297+ * indexInfo [Done]
 298+ * fieldInfo [Done]
 299+ * fieldType [Done]
 300+ * indexUnique [Done]
 301+ * textFieldSize [Done]
 302+ *
 303+ * Generation: 16 / 16
 304+ * tableName [Done]
 305+ * addQuotes [Done]
 306+ * makeList [Done]
 307+ * makeSelectOptions [Done]
 308+ * estimateRowCount [Done]
 309+ * nextSequenceValue [Done]
 310+ * useIndexClause [Done]
 311+ * replace [Done]
 312+ * deleteJoin [Done]
 313+ * lowPriorityOption [Done]
 314+ * limitResult [Done]
 315+ * limitResultForUpdate [Done]
 316+ * timestamp [Done]
 317+ * encodeBlob [Done]
 318+ * decodeBlob [Done]
 319+ * buildConcat [Done]
 320+ */
 321+
 322+ ######################################
 323+ # Getters and Setters
 324+ ######################################
 325+
 326+ /**
 327+ * Returns true if this database supports (and uses) cascading deletes
 328+ */
 329+ function cascadingDeletes() {
 330+ return true;
 331+ }
 332+
 333+ /**
 334+ * Returns true if this database supports (and uses) triggers (e.g. on the page table)
 335+ */
 336+ function cleanupTriggers() {
 337+ return true;
 338+ }
 339+
 340+ /**
 341+ * Returns true if this database is strict about what can be put into an IP field.
 342+ * Specifically, it uses a NULL value instead of an empty string.
 343+ */
 344+ function strictIPs() {
 345+ return true;
 346+ }
 347+
 348+ /**
 349+ * Returns true if this database uses timestamps rather than integers
 350+ */
 351+ function realTimestamps() {
 352+ return true;
 353+ }
 354+
 355+ /**
 356+ * Returns true if this database does an implicit sort when doing GROUP BY
 357+ */
 358+ function implicitGroupby() {
 359+ return false;
 360+ }
 361+
 362+ /**
 363+ * Returns true if this database does an implicit order by when the column has an index
 364+ * For example: SELECT page_title FROM page LIMIT 1
 365+ */
 366+ function implicitOrderby() {
 367+ return false;
 368+ }
 369+
 370+ /**
 371+ * Returns true if this database can do a native search on IP columns
 372+ * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
 373+ */
 374+ function searchableIPs() {
 375+ return true;
 376+ }
 377+
 378+ /**
 379+ * Returns true if this database can use functional indexes
 380+ */
 381+ function functionalIndexes() {
 382+ return true;
 383+ }
 384+
 385+ /**
 386+ * Returns a unique string representing the wiki on the server
 387+ */
 388+ function getWikiID() {
 389+ if( $this->mSchema ) {
 390+ return "{$this->mDBname}-{$this->mSchema}";
 391+ } else {
 392+ return $this->mDBname;
 393+ }
 394+ }
 395+
 396+ function getType() {
 397+ return 'ibm_db2';
 398+ }
 399+
 400+ ######################################
 401+ # Setup
 402+ ######################################
 403+
 404+
 405+ /**
 406+ *
 407+ * @param string $server hostname of database server
 408+ * @param string $user username
 409+ * @param string $password
 410+ * @param string $dbName database name on the server
 411+ * @param function $failFunction (optional)
 412+ * @param integer $flags database behaviour flags (optional, unused)
 413+ */
 414+ public function DatabaseIbm_db2($server = false, $user = false, $password = false,
 415+ $dbName = false, $failFunction = false, $flags = 0,
 416+ $schema = self::USE_GLOBAL )
 417+ {
 418+
 419+ global $wgOut, $wgDBmwschema;
 420+ # Can't get a reference if it hasn't been set yet
 421+ if ( !isset( $wgOut ) ) {
 422+ $wgOut = null;
 423+ }
 424+ $this->mOut =& $wgOut;
 425+ $this->mFailFunction = $failFunction;
 426+ $this->mFlags = DBO_TRX | $flags;
 427+
 428+ if ( $schema == self::USE_GLOBAL ) {
 429+ $this->mSchema = $wgDBmwschema;
 430+ }
 431+ else {
 432+ $this->mSchema = $schema;
 433+ }
 434+
 435+ // configure the connection and statement objects
 436+ $this->setDB2Option('db2_attr_case', 'DB2_CASE_LOWER', self::CONN_OPTION | self::STMT_OPTION);
 437+ $this->setDB2Option('deferred_prepare', 'DB2_DEFERRED_PREPARE_ON', self::STMT_OPTION);
 438+ $this->setDB2Option('rowcount', 'DB2_ROWCOUNT_PREFETCH_ON', self::STMT_OPTION);
 439+
 440+ $this->open( $server, $user, $password, $dbName);
 441+ }
 442+
 443+ /**
 444+ * Enables options only if the ibm_db2 extension version supports them
 445+ * @param string $name Name of the option in the options array
 446+ * @param string $const Name of the constant holding the right option value
 447+ * @param int $type Whether this is a Connection or Statement otion
 448+ */
 449+ private function setDB2Option($name, $const, $type) {
 450+ if (defined($const)) {
 451+ if ($type & self::CONN_OPTION) $this->mConnOptions[$name] = constant($const);
 452+ if ($type & self::STMT_OPTION) $this->mStmtOptions[$name] = constant($const);
 453+ }
 454+ else {
 455+ $this->installPrint("$const is not defined. ibm_db2 version is likely too low.");
 456+ }
 457+ }
 458+
 459+ /**
 460+ * Outputs debug information in the appropriate place
 461+ * @param string $string The relevant debug message
 462+ */
 463+ private function installPrint($string) {
 464+ wfDebug("$string");
 465+ if ($this->mMode == self::INSTALL_MODE) {
 466+ print "<li>$string</li>";
 467+ flush();
 468+ }
 469+ }
 470+
 471+ /**
 472+ * Opens a database connection and returns it
 473+ * Closes any existing connection
 474+ * @return a fresh connection
 475+ * @param string $server hostname
 476+ * @param string $user
 477+ * @param string $password
 478+ * @param string $dbName database name
 479+ */
 480+ public function open( $server, $user, $password, $dbName )
 481+ {
 482+ // Load the port number
 483+ global $wgDBport_db2, $wgDBcataloged;
 484+ wfProfileIn( __METHOD__ );
 485+
 486+ // Load IBM DB2 driver if missing
 487+ if (!@extension_loaded('ibm_db2')) {
 488+ @dl('ibm_db2.so');
 489+ }
 490+ // Test for IBM DB2 support, to avoid suppressed fatal error
 491+ if ( !function_exists( 'db2_connect' ) ) {
 492+ $error = "DB2 functions missing, have you enabled the ibm_db2 extension for PHP?\n";
 493+ $this->installPrint($error);
 494+ $this->reportConnectionError($error);
 495+ }
 496+
 497+ if (!strlen($user)) { // Copied from Postgres
 498+ return null;
 499+ }
 500+
 501+ // Close existing connection
 502+ $this->close();
 503+ // Cache conn info
 504+ $this->mServer = $server;
 505+ $this->mPort = $port = $wgDBport_db2;
 506+ $this->mUser = $user;
 507+ $this->mPassword = $password;
 508+ $this->mDBname = $dbName;
 509+ $this->mCataloged = $cataloged = $wgDBcataloged;
 510+
 511+ if ( $cataloged == self::CATALOGED ) {
 512+ $this->openCataloged($dbName, $user, $password);
 513+ }
 514+ elseif ( $cataloged == self::UNCATALOGED ) {
 515+ $this->openUncataloged($dbName, $user, $password, $server, $port);
 516+ }
 517+ // Apply connection config
 518+ db2_set_option($this->mConn, $this->mConnOptions, 1);
 519+ // Not all MediaWiki code is transactional
 520+ // Rather, turn autocommit off in the begin function and turn on after a commit
 521+ db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON);
 522+
 523+ if ( $this->mConn == false ) {
 524+ $this->installPrint( "DB connection error\n" );
 525+ $this->installPrint( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
 526+ $this->installPrint( $this->lastError()."\n" );
 527+ return null;
 528+ }
 529+
 530+ $this->mOpened = true;
 531+ $this->applySchema();
 532+
 533+ wfProfileOut( __METHOD__ );
 534+ return $this->mConn;
 535+ }
 536+
 537+ /**
 538+ * Opens a cataloged database connection, sets mConn
 539+ */
 540+ protected function openCataloged( $dbName, $user, $password )
 541+ {
 542+ @$this->mConn = db2_connect($dbName, $user, $password);
 543+ }
 544+
 545+ /**
 546+ * Opens an uncataloged database connection, sets mConn
 547+ */
 548+ protected function openUncataloged( $dbName, $user, $password, $server, $port )
 549+ {
 550+ $str = "DRIVER={IBM DB2 ODBC DRIVER};";
 551+ $str .= "DATABASE=$dbName;";
 552+ $str .= "HOSTNAME=$server;";
 553+ if ($port) $str .= "PORT=$port;";
 554+ $str .= "PROTOCOL=TCPIP;";
 555+ $str .= "UID=$user;";
 556+ $str .= "PWD=$password;";
 557+
 558+ @$this->mConn = db2_connect($str, $user, $password);
 559+ }
 560+
 561+ /**
 562+ * Closes a database connection, if it is open
 563+ * Returns success, true if already closed
 564+ */
 565+ public function close() {
 566+ $this->mOpened = false;
 567+ if ( $this->mConn ) {
 568+ if ($this->trxLevel() > 0) {
 569+ $this->commit();
 570+ }
 571+ return db2_close( $this->mConn );
 572+ }
 573+ else {
 574+ return true;
 575+ }
 576+ }
 577+
 578+ /**
 579+ * Returns a fresh instance of this class
 580+ * @static
 581+ * @return
 582+ * @param string $server hostname of database server
 583+ * @param string $user username
 584+ * @param string $password
 585+ * @param string $dbName database name on the server
 586+ * @param function $failFunction (optional)
 587+ * @param integer $flags database behaviour flags (optional, unused)
 588+ */
 589+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
 590+ {
 591+ return new DatabaseIbm_db2( $server, $user, $password, $dbName, $failFunction, $flags );
 592+ }
 593+
 594+ /**
 595+ * Retrieves the most current database error
 596+ * Forces a database rollback
 597+ */
 598+ public function lastError() {
 599+ $connerr = db2_conn_errormsg();
 600+ if ($connerr) {
 601+ //$this->rollback();
 602+ return $connerr;
 603+ }
 604+ $stmterr = db2_stmt_errormsg();
 605+ if ($stmterr) {
 606+ //$this->rollback();
 607+ return $stmterr;
 608+ }
 609+
 610+ return false;
 611+ }
 612+
 613+ /**
 614+ * Get the last error number
 615+ * Return 0 if no error
 616+ * @return integer
 617+ */
 618+ public function lastErrno() {
 619+ $connerr = db2_conn_error();
 620+ if ($connerr) return $connerr;
 621+ $stmterr = db2_stmt_error();
 622+ if ($stmterr) return $stmterr;
 623+ return 0;
 624+ }
 625+
 626+ /**
 627+ * Is a database connection open?
 628+ * @return
 629+ */
 630+ public function isOpen() { return $this->mOpened; }
 631+
 632+ /**
 633+ * The DBMS-dependent part of query()
 634+ * @param $sql String: SQL query.
 635+ * @return object Result object to feed to fetchObject, fetchRow, ...; or false on failure
 636+ * @access private
 637+ */
 638+ /*private*/
 639+ public function doQuery( $sql ) {
 640+ //print "<li><pre>$sql</pre></li>";
 641+ // Switch into the correct namespace
 642+ $this->applySchema();
 643+
 644+ $ret = db2_exec( $this->mConn, $sql, $this->mStmtOptions );
 645+ if( !$ret ) {
 646+ print "<br><pre>";
 647+ print $sql;
 648+ print "</pre><br>";
 649+ $error = db2_stmt_errormsg();
 650+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( $error ) );
 651+ }
 652+ $this->mLastResult = $ret;
 653+ $this->mAffectedRows = null; // Not calculated until asked for
 654+ return $ret;
 655+ }
 656+
 657+ /**
 658+ * @return string Version information from the database
 659+ */
 660+ public function getServerVersion() {
 661+ $info = db2_server_info( $this->mConn );
 662+ return $info->DBMS_VER;
 663+ }
 664+
 665+ /**
 666+ * Queries whether a given table exists
 667+ * @return boolean
 668+ */
 669+ public function tableExists( $table ) {
 670+ $schema = $this->mSchema;
 671+ $sql = <<< EOF
 672+SELECT COUNT(*) FROM SYSIBM.SYSTABLES ST
 673+WHERE ST.NAME = '$table' AND ST.CREATOR = '$schema'
 674+EOF;
 675+ $res = $this->query( $sql );
 676+ if (!$res) return false;
 677+
 678+ // If the table exists, there should be one of it
 679+ @$row = $this->fetchRow($res);
 680+ $count = $row[0];
 681+ if ($count == '1' or $count == 1) {
 682+ return true;
 683+ }
 684+
 685+ return false;
 686+ }
 687+
 688+ /**
 689+ * Fetch the next row from the given result object, in object form.
 690+ * Fields can be retrieved with $row->fieldname, with fields acting like
 691+ * member variables.
 692+ *
 693+ * @param $res SQL result object as returned from Database::query(), etc.
 694+ * @return DB2 row object
 695+ * @throws DBUnexpectedError Thrown if the database returns an error
 696+ */
 697+ public function fetchObject( $res ) {
 698+ if ( $res instanceof ResultWrapper ) {
 699+ $res = $res->result;
 700+ }
 701+ @$row = db2_fetch_object( $res );
 702+ if( $this->lastErrno() ) {
 703+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
 704+ }
 705+ return $row;
 706+ }
 707+
 708+ /**
 709+ * Fetch the next row from the given result object, in associative array
 710+ * form. Fields are retrieved with $row['fieldname'].
 711+ *
 712+ * @param $res SQL result object as returned from Database::query(), etc.
 713+ * @return DB2 row object
 714+ * @throws DBUnexpectedError Thrown if the database returns an error
 715+ */
 716+ public function fetchRow( $res ) {
 717+ if ( $res instanceof ResultWrapper ) {
 718+ $res = $res->result;
 719+ }
 720+ @$row = db2_fetch_array( $res );
 721+ if ( $this->lastErrno() ) {
 722+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
 723+ }
 724+ return $row;
 725+ }
 726+
 727+ /**
 728+ * Override if introduced to base Database class
 729+ */
 730+ public function initial_setup() {
 731+ // do nothing
 732+ }
 733+
 734+ /**
 735+ * Create tables, stored procedures, and so on
 736+ */
 737+ public function setup_database() {
 738+ // Timeout was being changed earlier due to mysterious crashes
 739+ // Changing it now may cause more problems than not changing it
 740+ //set_time_limit(240);
 741+ try {
 742+ // TODO: switch to root login if available
 743+
 744+ // Switch into the correct namespace
 745+ $this->applySchema();
 746+ $this->begin();
 747+
 748+ $res = $this->sourceFile( "../maintenance/ibm_db2/tables.sql" );
 749+ $res = null;
 750+
 751+ // TODO: update mediawiki_version table
 752+
 753+ // TODO: populate interwiki links
 754+
 755+ if ($this->lastError()) {
 756+ print "<li>Errors encountered during table creation -- rolled back</li>\n";
 757+ print "<li>Please install again</li>\n";
 758+ $this->rollback();
 759+ }
 760+ else {
 761+ $this->commit();
 762+ }
 763+ }
 764+ catch (MWException $mwe)
 765+ {
 766+ print "<br><pre>$mwe</pre><br>";
 767+ }
 768+ }
 769+
 770+ /**
 771+ * Escapes strings
 772+ * Doesn't escape numbers
 773+ * @param string s string to escape
 774+ * @return escaped string
 775+ */
 776+ public function addQuotes( $s ) {
 777+ //$this->installPrint("DB2::addQuotes($s)\n");
 778+ if ( is_null( $s ) ) {
 779+ return "NULL";
 780+ } else if ($s instanceof Blob) {
 781+ return "'".$s->fetch($s)."'";
 782+ } else if ($s instanceof IBM_DB2Blob) {
 783+ return "'".$this->decodeBlob($s)."'";
 784+ }
 785+ $s = $this->strencode($s);
 786+ if ( is_numeric($s) ) {
 787+ return $s;
 788+ }
 789+ else {
 790+ return "'$s'";
 791+ }
 792+ }
 793+
 794+ /**
 795+ * Verifies that a DB2 column/field type is numeric
 796+ * @return bool true if numeric
 797+ * @param string $type DB2 column type
 798+ */
 799+ public function is_numeric_type( $type ) {
 800+ switch (strtoupper($type)) {
 801+ case 'SMALLINT':
 802+ case 'INTEGER':
 803+ case 'INT':
 804+ case 'BIGINT':
 805+ case 'DECIMAL':
 806+ case 'REAL':
 807+ case 'DOUBLE':
 808+ case 'DECFLOAT':
 809+ return true;
 810+ }
 811+ return false;
 812+ }
 813+
 814+ /**
 815+ * Alias for addQuotes()
 816+ * @param string s string to escape
 817+ * @return escaped string
 818+ */
 819+ public function strencode( $s ) {
 820+ // Bloody useless function
 821+ // Prepends backslashes to \x00, \n, \r, \, ', " and \x1a.
 822+ // But also necessary
 823+ $s = db2_escape_string($s);
 824+ // Wide characters are evil -- some of them look like '
 825+ $s = utf8_encode($s);
 826+ // Fix its stupidity
 827+ $from = array("\\\\", "\\'", '\\n', '\\t', '\\"', '\\r');
 828+ $to = array("\\", "''", "\n", "\t", '"', "\r");
 829+ $s = str_replace($from, $to, $s); // DB2 expects '', not \' escaping
 830+ return $s;
 831+ }
 832+
 833+ /**
 834+ * Switch into the database schema
 835+ */
 836+ protected function applySchema() {
 837+ if ( !($this->mSchemaSet) ) {
 838+ $this->mSchemaSet = true;
 839+ $this->begin();
 840+ $this->doQuery("SET SCHEMA = $this->mSchema");
 841+ $this->commit();
 842+ }
 843+ }
 844+
 845+ /**
 846+ * Start a transaction (mandatory)
 847+ */
 848+ public function begin( $fname = 'DatabaseIbm_db2::begin' ) {
 849+ // turn off auto-commit
 850+ db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF);
 851+ $this->mTrxLevel = 1;
 852+ }
 853+
 854+ /**
 855+ * End a transaction
 856+ * Must have a preceding begin()
 857+ */
 858+ public function commit( $fname = 'DatabaseIbm_db2::commit' ) {
 859+ db2_commit($this->mConn);
 860+ // turn auto-commit back on
 861+ db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON);
 862+ $this->mTrxLevel = 0;
 863+ }
 864+
 865+ /**
 866+ * Cancel a transaction
 867+ */
 868+ public function rollback( $fname = 'DatabaseIbm_db2::rollback' ) {
 869+ db2_rollback($this->mConn);
 870+ // turn auto-commit back on
 871+ // not sure if this is appropriate
 872+ db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON);
 873+ $this->mTrxLevel = 0;
 874+ }
 875+
 876+ /**
 877+ * Makes an encoded list of strings from an array
 878+ * $mode:
 879+ * LIST_COMMA - comma separated, no field names
 880+ * LIST_AND - ANDed WHERE clause (without the WHERE)
 881+ * LIST_OR - ORed WHERE clause (without the WHERE)
 882+ * LIST_SET - comma separated with field names, like a SET clause
 883+ * LIST_NAMES - comma separated field names
 884+ */
 885+ public function makeList( $a, $mode = LIST_COMMA ) {
 886+ if ( !is_array( $a ) ) {
 887+ throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
 888+ }
 889+
 890+ $first = true;
 891+ $list = '';
 892+ foreach ( $a as $field => $value ) {
 893+ if ( !$first ) {
 894+ if ( $mode == LIST_AND ) {
 895+ $list .= ' AND ';
 896+ } elseif($mode == LIST_OR) {
 897+ $list .= ' OR ';
 898+ } else {
 899+ $list .= ',';
 900+ }
 901+ } else {
 902+ $first = false;
 903+ }
 904+ if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
 905+ $list .= "($value)";
 906+ } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
 907+ $list .= "$value";
 908+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
 909+ if( count( $value ) == 0 ) {
 910+ throw new MWException( __METHOD__.': empty input' );
 911+ } elseif( count( $value ) == 1 ) {
 912+ // Special-case single values, as IN isn't terribly efficient
 913+ // Don't necessarily assume the single key is 0; we don't
 914+ // enforce linear numeric ordering on other arrays here.
 915+ $value = array_values( $value );
 916+ $list .= $field." = ".$this->addQuotes( $value[0] );
 917+ } else {
 918+ $list .= $field." IN (".$this->makeList($value).") ";
 919+ }
 920+ } elseif( is_null($value) ) {
 921+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
 922+ $list .= "$field IS ";
 923+ } elseif ( $mode == LIST_SET ) {
 924+ $list .= "$field = ";
 925+ }
 926+ $list .= 'NULL';
 927+ } else {
 928+ if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
 929+ $list .= "$field = ";
 930+ }
 931+ if ( $mode == LIST_NAMES ) {
 932+ $list .= $value;
 933+ }
 934+ // Leo: Can't insert quoted numbers into numeric columns
 935+ // (?) Might cause other problems. May have to check column type before insertion.
 936+ else if ( is_numeric($value) ) {
 937+ $list .= $value;
 938+ }
 939+ else {
 940+ $list .= $this->addQuotes( $value );
 941+ }
 942+ }
 943+ }
 944+ return $list;
 945+ }
 946+
 947+ /**
 948+ * Construct a LIMIT query with optional offset
 949+ * This is used for query pages
 950+ * $sql string SQL query we will append the limit too
 951+ * $limit integer the SQL limit
 952+ * $offset integer the SQL offset (default false)
 953+ */
 954+ public function limitResult($sql, $limit, $offset=false) {
 955+ if( !is_numeric($limit) ) {
 956+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
 957+ }
 958+ if( $offset ) {
 959+ $this->installPrint("Offset parameter not supported in limitResult()\n");
 960+ }
 961+ // TODO implement proper offset handling
 962+ // idea: get all the rows between 0 and offset, advance cursor to offset
 963+ return "$sql FETCH FIRST $limit ROWS ONLY ";
 964+ }
 965+
 966+ /**
 967+ * Handle reserved keyword replacement in table names
 968+ * @return
 969+ * @param $name Object
 970+ */
 971+ public function tableName( $name ) {
 972+ # Replace reserved words with better ones
 973+// switch( $name ) {
 974+// case 'user':
 975+// return 'mwuser';
 976+// case 'text':
 977+// return 'pagecontent';
 978+// default:
 979+// return $name;
 980+// }
 981+ // we want maximum compatibility with MySQL schema
 982+ return $name;
 983+ }
 984+
 985+ /**
 986+ * Generates a timestamp in an insertable format
 987+ * @return string timestamp value
 988+ * @param timestamp $ts
 989+ */
 990+ public function timestamp( $ts=0 ) {
 991+ // TS_MW cannot be easily distinguished from an integer
 992+ return wfTimestamp(TS_DB2,$ts);
 993+ }
 994+
 995+ /**
 996+ * Return the next in a sequence, save the value for retrieval via insertId()
 997+ * @param string seqName Name of a defined sequence in the database
 998+ * @return next value in that sequence
 999+ */
 1000+ public function nextSequenceValue( $seqName ) {
 1001+ // Not using sequences in the primary schema to allow for easy third-party migration scripts
 1002+ // Emulating MySQL behaviour of using NULL to signal that sequences aren't used
 1003+ /*
 1004+ $safeseq = preg_replace( "/'/", "''", $seqName );
 1005+ $res = $this->query( "VALUES NEXTVAL FOR $safeseq" );
 1006+ $row = $this->fetchRow( $res );
 1007+ $this->mInsertId = $row[0];
 1008+ $this->freeResult( $res );
 1009+ return $this->mInsertId;
 1010+ */
 1011+ return null;
 1012+ }
 1013+
 1014+ /**
 1015+ * This must be called after nextSequenceVal
 1016+ * @return Last sequence value used as a primary key
 1017+ */
 1018+ public function insertId() {
 1019+ return $this->mInsertId;
 1020+ }
 1021+
 1022+ /**
 1023+ * Updates the mInsertId property with the value of the last insert into a generated column
 1024+ * @param string $table Sanitized table name
 1025+ * @param mixed $primaryKey String name of the primary key or a bool if this call is a do-nothing
 1026+ * @param resource $stmt Prepared statement resource
 1027+ * of the SELECT primary_key FROM FINAL TABLE ( INSERT ... ) form
 1028+ */
 1029+ private function calcInsertId($table, $primaryKey, $stmt) {
 1030+ if ($primaryKey) {
 1031+ $id_row = $this->fetchRow($stmt);
 1032+ $this->mInsertId = $id_row[0];
 1033+ }
 1034+ }
 1035+
 1036+ /**
 1037+ * INSERT wrapper, inserts an array into a table
 1038+ *
 1039+ * $args may be a single associative array, or an array of these with numeric keys,
 1040+ * for multi-row insert
 1041+ *
 1042+ * @param array $table String: Name of the table to insert to.
 1043+ * @param array $args Array: Items to insert into the table.
 1044+ * @param array $fname String: Name of the function, for profiling
 1045+ * @param mixed $options String or Array. Valid options: IGNORE
 1046+ *
 1047+ * @return bool Success of insert operation. IGNORE always returns true.
 1048+ */
 1049+ public function insert( $table, $args, $fname = 'DatabaseIbm_db2::insert', $options = array() ) {
 1050+ if ( !count( $args ) ) {
 1051+ return true;
 1052+ }
 1053+ // get database-specific table name (not used)
 1054+ $table = $this->tableName( $table );
 1055+ // format options as an array
 1056+ if ( !is_array( $options ) ) $options = array( $options );
 1057+ // format args as an array of arrays
 1058+ if ( !( isset( $args[0] ) && is_array( $args[0] ) ) ) {
 1059+ $args = array($args);
 1060+ }
 1061+ // prevent insertion of NULL into primary key columns
 1062+ list($args, $primaryKeys) = $this->removeNullPrimaryKeys($table, $args);
 1063+ // if there's only one primary key
 1064+ // we'll be able to read its value after insertion
 1065+ $primaryKey = false;
 1066+ if (count($primaryKeys) == 1) {
 1067+ $primaryKey = $primaryKeys[0];
 1068+ }
 1069+
 1070+ // get column names
 1071+ $keys = array_keys( $args[0] );
 1072+ $key_count = count($keys);
 1073+
 1074+ // If IGNORE is set, we use savepoints to emulate mysql's behavior
 1075+ $ignore = in_array( 'IGNORE', $options ) ? 'mw' : '';
 1076+
 1077+ // assume success
 1078+ $res = true;
 1079+ // If we are not in a transaction, we need to be for savepoint trickery
 1080+ $didbegin = 0;
 1081+ if (! $this->mTrxLevel) {
 1082+ $this->begin();
 1083+ $didbegin = 1;
 1084+ }
 1085+
 1086+ $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
 1087+ switch($key_count) {
 1088+ //case 0 impossible
 1089+ case 1:
 1090+ $sql .= '(?)';
 1091+ break;
 1092+ default:
 1093+ $sql .= '(?' . str_repeat(',?', $key_count-1) . ')';
 1094+ }
 1095+ // add logic to read back the new primary key value
 1096+ if ($primaryKey) {
 1097+ $sql = "SELECT $primaryKey FROM FINAL TABLE($sql)";
 1098+ }
 1099+ $stmt = $this->prepare($sql);
 1100+
 1101+ // start a transaction/enter transaction mode
 1102+ $this->begin();
 1103+
 1104+ if ( !$ignore ) {
 1105+ $first = true;
 1106+ foreach ( $args as $row ) {
 1107+ // insert each row into the database
 1108+ $res = $res & $this->execute($stmt, $row);
 1109+ // get the last inserted value into a generated column
 1110+ $this->calcInsertId($table, $primaryKey, $stmt);
 1111+ }
 1112+ }
 1113+ else {
 1114+ $olde = error_reporting( 0 );
 1115+ // For future use, we may want to track the number of actual inserts
 1116+ // Right now, insert (all writes) simply return true/false
 1117+ $numrowsinserted = 0;
 1118+
 1119+ // always return true
 1120+ $res = true;
 1121+
 1122+ foreach ( $args as $row ) {
 1123+ $overhead = "SAVEPOINT $ignore ON ROLLBACK RETAIN CURSORS";
 1124+ db2_exec($this->mConn, $overhead, $this->mStmtOptions);
 1125+
 1126+ $res2 = $this->execute($stmt, $row);
 1127+ // get the last inserted value into a generated column
 1128+ $this->calcInsertId($table, $primaryKey, $stmt);
 1129+
 1130+ $errNum = $this->lastErrno();
 1131+ if ($errNum) {
 1132+ db2_exec( $this->mConn, "ROLLBACK TO SAVEPOINT $ignore", $this->mStmtOptions );
 1133+ }
 1134+ else {
 1135+ db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore", $this->mStmtOptions );
 1136+ $numrowsinserted++;
 1137+ }
 1138+ }
 1139+
 1140+ $olde = error_reporting( $olde );
 1141+ // Set the affected row count for the whole operation
 1142+ $this->mAffectedRows = $numrowsinserted;
 1143+ }
 1144+ // commit either way
 1145+ $this->commit();
 1146+
 1147+ return $res;
 1148+ }
 1149+
 1150+ /**
 1151+ * Given a table name and a hash of columns with values
 1152+ * Removes primary key columns from the hash where the value is NULL
 1153+ *
 1154+ * @param string $table Name of the table
 1155+ * @param array $args Array of hashes of column names with values
 1156+ * @return array Tuple containing filtered array of columns, array of primary keys
 1157+ */
 1158+ private function removeNullPrimaryKeys($table, $args) {
 1159+ $schema = $this->mSchema;
 1160+ // find out the primary keys
 1161+ $keyres = db2_primary_keys($this->mConn, null, strtoupper($schema), strtoupper($table));
 1162+ $keys = array();
 1163+ for ($row = $this->fetchObject($keyres); $row != null; $row = $this->fetchRow($keyres)) {
 1164+ $keys[] = strtolower($row->column_name);
 1165+ }
 1166+ // remove primary keys
 1167+ foreach ($args as $ai => $row) {
 1168+ foreach ($keys as $ki => $key) {
 1169+ if ($row[$key] == null) {
 1170+ unset($row[$key]);
 1171+ }
 1172+ }
 1173+ $args[$ai] = $row;
 1174+ }
 1175+ // return modified hash
 1176+ return array($args, $keys);
 1177+ }
 1178+
 1179+ /**
 1180+ * UPDATE wrapper, takes a condition array and a SET array
 1181+ *
 1182+ * @param string $table The table to UPDATE
 1183+ * @param array $values An array of values to SET
 1184+ * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
 1185+ * @param string $fname The Class::Function calling this function
 1186+ * (for the log)
 1187+ * @param array $options An array of UPDATE options, can be one or
 1188+ * more of IGNORE, LOW_PRIORITY
 1189+ * @return bool
 1190+ */
 1191+ public function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
 1192+ $table = $this->tableName( $table );
 1193+ $opts = $this->makeUpdateOptions( $options );
 1194+ $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
 1195+ if ( $conds != '*' ) {
 1196+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
 1197+ }
 1198+ return $this->query( $sql, $fname );
 1199+ }
 1200+
 1201+ /**
 1202+ * DELETE query wrapper
 1203+ *
 1204+ * Use $conds == "*" to delete all rows
 1205+ */
 1206+ public function delete( $table, $conds, $fname = 'Database::delete' ) {
 1207+ if ( !$conds ) {
 1208+ throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
 1209+ }
 1210+ $table = $this->tableName( $table );
 1211+ $sql = "DELETE FROM $table";
 1212+ if ( $conds != '*' ) {
 1213+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
 1214+ }
 1215+ return $this->query( $sql, $fname );
 1216+ }
 1217+
 1218+ /**
 1219+ * Returns the number of rows affected by the last query or 0
 1220+ * @return int the number of rows affected by the last query
 1221+ */
 1222+ public function affectedRows() {
 1223+ if ( !is_null( $this->mAffectedRows ) ) {
 1224+ // Forced result for simulated queries
 1225+ return $this->mAffectedRows;
 1226+ }
 1227+ if( empty( $this->mLastResult ) )
 1228+ return 0;
 1229+ return db2_num_rows( $this->mLastResult );
 1230+ }
 1231+
 1232+ /**
 1233+ * Simulates REPLACE with a DELETE followed by INSERT
 1234+ * @param $table Object
 1235+ * @param array $uniqueIndexes array consisting of indexes and arrays of indexes
 1236+ * @param array $rows Rows to insert
 1237+ * @param string $fname Name of the function for profiling
 1238+ * @return nothing
 1239+ */
 1240+ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseIbm_db2::replace' ) {
 1241+ $table = $this->tableName( $table );
 1242+
 1243+ if (count($rows)==0) {
 1244+ return;
 1245+ }
 1246+
 1247+ # Single row case
 1248+ if ( !is_array( reset( $rows ) ) ) {
 1249+ $rows = array( $rows );
 1250+ }
 1251+
 1252+ foreach( $rows as $row ) {
 1253+ # Delete rows which collide
 1254+ if ( $uniqueIndexes ) {
 1255+ $sql = "DELETE FROM $table WHERE ";
 1256+ $first = true;
 1257+ foreach ( $uniqueIndexes as $index ) {
 1258+ if ( $first ) {
 1259+ $first = false;
 1260+ $sql .= "(";
 1261+ } else {
 1262+ $sql .= ') OR (';
 1263+ }
 1264+ if ( is_array( $index ) ) {
 1265+ $first2 = true;
 1266+ foreach ( $index as $col ) {
 1267+ if ( $first2 ) {
 1268+ $first2 = false;
 1269+ } else {
 1270+ $sql .= ' AND ';
 1271+ }
 1272+ $sql .= $col.'=' . $this->addQuotes( $row[$col] );
 1273+ }
 1274+ } else {
 1275+ $sql .= $index.'=' . $this->addQuotes( $row[$index] );
 1276+ }
 1277+ }
 1278+ $sql .= ')';
 1279+ $this->query( $sql, $fname );
 1280+ }
 1281+
 1282+ # Now insert the row
 1283+ $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
 1284+ $this->makeList( $row, LIST_COMMA ) . ')';
 1285+ $this->query( $sql, $fname );
 1286+ }
 1287+ }
 1288+
 1289+ /**
 1290+ * Returns the number of rows in the result set
 1291+ * Has to be called right after the corresponding select query
 1292+ * @param object $res result set
 1293+ * @return int number of rows
 1294+ */
 1295+ public function numRows( $res ) {
 1296+ if ( $res instanceof ResultWrapper ) {
 1297+ $res = $res->result;
 1298+ }
 1299+ if ( $this->mNumRows ) {
 1300+ return $this->mNumRows;
 1301+ }
 1302+ else {
 1303+ return 0;
 1304+ }
 1305+ }
 1306+
 1307+ /**
 1308+ * Moves the row pointer of the result set
 1309+ * @param object $res result set
 1310+ * @param int $row row number
 1311+ * @return success or failure
 1312+ */
 1313+ public function dataSeek( $res, $row ) {
 1314+ if ( $res instanceof ResultWrapper ) {
 1315+ $res = $res->result;
 1316+ }
 1317+ return db2_fetch_row( $res, $row );
 1318+ }
 1319+
 1320+ ###
 1321+ # Fix notices in Block.php
 1322+ ###
 1323+
 1324+ /**
 1325+ * Frees memory associated with a statement resource
 1326+ * @param object $res Statement resource to free
 1327+ * @return bool success or failure
 1328+ */
 1329+ public function freeResult( $res ) {
 1330+ if ( $res instanceof ResultWrapper ) {
 1331+ $res = $res->result;
 1332+ }
 1333+ if ( !@db2_free_result( $res ) ) {
 1334+ throw new DBUnexpectedError($this, "Unable to free DB2 result\n" );
 1335+ }
 1336+ }
 1337+
 1338+ /**
 1339+ * Returns the number of columns in a resource
 1340+ * @param object $res Statement resource
 1341+ * @return Number of fields/columns in resource
 1342+ */
 1343+ public function numFields( $res ) {
 1344+ if ( $res instanceof ResultWrapper ) {
 1345+ $res = $res->result;
 1346+ }
 1347+ return db2_num_fields( $res );
 1348+ }
 1349+
 1350+ /**
 1351+ * Returns the nth column name
 1352+ * @param object $res Statement resource
 1353+ * @param int $n Index of field or column
 1354+ * @return string name of nth column
 1355+ */
 1356+ public function fieldName( $res, $n ) {
 1357+ if ( $res instanceof ResultWrapper ) {
 1358+ $res = $res->result;
 1359+ }
 1360+ return db2_field_name( $res, $n );
 1361+ }
 1362+
 1363+ /**
 1364+ * SELECT wrapper
 1365+ *
 1366+ * @param mixed $table Array or string, table name(s) (prefix auto-added)
 1367+ * @param mixed $vars Array or string, field name(s) to be retrieved
 1368+ * @param mixed $conds Array or string, condition(s) for WHERE
 1369+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
 1370+ * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
 1371+ * see Database::makeSelectOptions code for list of supported stuff
 1372+ * @param array $join_conds Associative array of table join conditions (optional)
 1373+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
 1374+ * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
 1375+ */
 1376+ public function select( $table, $vars, $conds='', $fname = 'DatabaseIbm_db2::select', $options = array(), $join_conds = array() )
 1377+ {
 1378+ $res = parent::select( $table, $vars, $conds, $fname, $options, $join_conds );
 1379+
 1380+ // We must adjust for offset
 1381+ if ( isset( $options['LIMIT'] ) ) {
 1382+ if ( isset ($options['OFFSET'] ) ) {
 1383+ $limit = $options['LIMIT'];
 1384+ $offset = $options['OFFSET'];
 1385+ }
 1386+ }
 1387+
 1388+
 1389+ // DB2 does not have a proper num_rows() function yet, so we must emulate it
 1390+ // DB2 9.5.3/9.5.4 and the corresponding ibm_db2 driver will introduce a working one
 1391+ // Yay!
 1392+
 1393+ // we want the count
 1394+ $vars2 = array('count(*) as num_rows');
 1395+ // respecting just the limit option
 1396+ $options2 = array();
 1397+ if ( isset( $options['LIMIT'] ) ) $options2['LIMIT'] = $options['LIMIT'];
 1398+ // but don't try to emulate for GROUP BY
 1399+ if ( isset( $options['GROUP BY'] ) ) return $res;
 1400+
 1401+ $res2 = parent::select( $table, $vars2, $conds, $fname, $options2, $join_conds );
 1402+ $obj = $this->fetchObject($res2);
 1403+ $this->mNumRows = $obj->num_rows;
 1404+
 1405+
 1406+ return $res;
 1407+ }
 1408+
 1409+ /**
 1410+ * Handles ordering, grouping, and having options ('GROUP BY' => colname)
 1411+ * Has limited support for per-column options (colnum => 'DISTINCT')
 1412+ *
 1413+ * @private
 1414+ *
 1415+ * @param array $options an associative array of options to be turned into
 1416+ * an SQL query, valid keys are listed in the function.
 1417+ * @return array
 1418+ */
 1419+ function makeSelectOptions( $options ) {
 1420+ $preLimitTail = $postLimitTail = '';
 1421+ $startOpts = '';
 1422+
 1423+ $noKeyOptions = array();
 1424+ foreach ( $options as $key => $option ) {
 1425+ if ( is_numeric( $key ) ) {
 1426+ $noKeyOptions[$option] = true;
 1427+ }
 1428+ }
 1429+
 1430+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
 1431+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
 1432+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
 1433+
 1434+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
 1435+
 1436+ return array( $startOpts, '', $preLimitTail, $postLimitTail );
 1437+ }
 1438+
 1439+ /**
 1440+ * Returns link to IBM DB2 free download
 1441+ * @return string wikitext of a link to the server software's web site
 1442+ */
 1443+ public function getSoftwareLink() {
 1444+ return "[http://www.ibm.com/software/data/db2/express/?s_cmp=ECDDWW01&s_tact=MediaWiki IBM DB2]";
 1445+ }
 1446+
 1447+ /**
 1448+ * Get search engine class. All subclasses of this
 1449+ * need to implement this if they wish to use searching.
 1450+ *
 1451+ * @return string
 1452+ */
 1453+ public function getSearchEngine() {
 1454+ return "SearchIBM_DB2";
 1455+ }
 1456+
 1457+ /**
 1458+ * Did the last database access fail because of deadlock?
 1459+ * @return bool
 1460+ */
 1461+ public function wasDeadlock() {
 1462+ // get SQLSTATE
 1463+ $err = $this->lastErrno();
 1464+ switch($err) {
 1465+ case '40001': // sql0911n, Deadlock or timeout, rollback
 1466+ case '57011': // sql0904n, Resource unavailable, no rollback
 1467+ case '57033': // sql0913n, Deadlock or timeout, no rollback
 1468+ $this->installPrint("In a deadlock because of SQLSTATE $err");
 1469+ return true;
 1470+ }
 1471+ return false;
 1472+ }
 1473+
 1474+ /**
 1475+ * Ping the server and try to reconnect if it there is no connection
 1476+ * The connection may be closed and reopened while this happens
 1477+ * @return bool whether the connection exists
 1478+ */
 1479+ public function ping() {
 1480+ // db2_ping() doesn't exist
 1481+ // Emulate
 1482+ $this->close();
 1483+ if ($this->mCataloged == null) {
 1484+ return false;
 1485+ }
 1486+ else if ($this->mCataloged) {
 1487+ $this->mConn = $this->openCataloged($this->mDBName, $this->mUser, $this->mPassword);
 1488+ }
 1489+ else if (!$this->mCataloged) {
 1490+ $this->mConn = $this->openUncataloged($this->mDBName, $this->mUser, $this->mPassword, $this->mServer, $this->mPort);
 1491+ }
 1492+ return false;
 1493+ }
 1494+ ######################################
 1495+ # Unimplemented and not applicable
 1496+ ######################################
 1497+ /**
 1498+ * Not implemented
 1499+ * @return string ''
 1500+ * @deprecated
 1501+ */
 1502+ public function getStatus( $which="%" ) { $this->installPrint('Not implemented for DB2: getStatus()'); return ''; }
 1503+ /**
 1504+ * Not implemented
 1505+ * TODO
 1506+ * @return bool true
 1507+ */
 1508+ /**
 1509+ * Not implemented
 1510+ * @deprecated
 1511+ */
 1512+ public function setFakeSlaveLag( $lag ) { $this->installPrint('Not implemented for DB2: setFakeSlaveLag()'); }
 1513+ /**
 1514+ * Not implemented
 1515+ * @deprecated
 1516+ */
 1517+ public function setFakeMaster( $enabled = true ) { $this->installPrint('Not implemented for DB2: setFakeMaster()'); }
 1518+ /**
 1519+ * Not implemented
 1520+ * @return string $sql
 1521+ * @deprecated
 1522+ */
 1523+ public function limitResultForUpdate($sql, $num) { $this->installPrint('Not implemented for DB2: limitResultForUpdate()'); return $sql; }
 1524+
 1525+ /**
 1526+ * Only useful with fake prepare like in base Database class
 1527+ * @return string
 1528+ */
 1529+ public function fillPreparedArg( $matches ) { $this->installPrint('Not useful for DB2: fillPreparedArg()'); return ''; }
 1530+
 1531+ ######################################
 1532+ # Reflection
 1533+ ######################################
 1534+
 1535+ /**
 1536+ * Query whether a given column exists in the mediawiki schema
 1537+ * @param string $table name of the table
 1538+ * @param string $field name of the column
 1539+ * @param string $fname function name for logging and profiling
 1540+ */
 1541+ public function fieldExists( $table, $field, $fname = 'DatabaseIbm_db2::fieldExists' ) {
 1542+ $table = $this->tableName( $table );
 1543+ $schema = $this->mSchema;
 1544+ $etable = preg_replace("/'/", "''", $table);
 1545+ $eschema = preg_replace("/'/", "''", $schema);
 1546+ $ecol = preg_replace("/'/", "''", $field);
 1547+ $sql = <<<SQL
 1548+SELECT 1 as fieldexists
 1549+FROM sysibm.syscolumns sc
 1550+WHERE sc.name='$ecol' AND sc.tbname='$etable' AND sc.tbcreator='$eschema'
 1551+SQL;
 1552+ $res = $this->query( $sql, $fname );
 1553+ $count = $res ? $this->numRows($res) : 0;
 1554+ if ($res)
 1555+ $this->freeResult( $res );
 1556+ return $count;
 1557+ }
 1558+
 1559+ /**
 1560+ * Returns information about an index
 1561+ * If errors are explicitly ignored, returns NULL on failure
 1562+ * @param string $table table name
 1563+ * @param string $index index name
 1564+ * @param string
 1565+ * @return object query row in object form
 1566+ */
 1567+ public function indexInfo( $table, $index, $fname = 'DatabaseIbm_db2::indexExists' ) {
 1568+ $table = $this->tableName( $table );
 1569+ $sql = <<<SQL
 1570+SELECT name as indexname
 1571+FROM sysibm.sysindexes si
 1572+WHERE si.name='$index' AND si.tbname='$table' AND sc.tbcreator='$this->mSchema'
 1573+SQL;
 1574+ $res = $this->query( $sql, $fname );
 1575+ if ( !$res ) {
 1576+ return null;
 1577+ }
 1578+ $row = $this->fetchObject( $res );
 1579+ if ($row != null) return $row;
 1580+ else return false;
 1581+ }
 1582+
 1583+ /**
 1584+ * Returns an information object on a table column
 1585+ * @param string $table table name
 1586+ * @param string $field column name
 1587+ * @return IBM_DB2Field
 1588+ */
 1589+ public function fieldInfo( $table, $field ) {
 1590+ return IBM_DB2Field::fromText($this, $table, $field);
 1591+ }
 1592+
 1593+ /**
 1594+ * db2_field_type() wrapper
 1595+ * @param object $res Result of executed statement
 1596+ * @param mixed $index number or name of the column
 1597+ * @return string column type
 1598+ */
 1599+ public function fieldType( $res, $index ) {
 1600+ if ( $res instanceof ResultWrapper ) {
 1601+ $res = $res->result;
 1602+ }
 1603+ return db2_field_type( $res, $index );
 1604+ }
 1605+
 1606+ /**
 1607+ * Verifies that an index was created as unique
 1608+ * @param string $table table name
 1609+ * @param string $index index name
 1610+ * @param string $fnam function name for profiling
 1611+ * @return bool
 1612+ */
 1613+ public function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
 1614+ $table = $this->tableName( $table );
 1615+ $sql = <<<SQL
 1616+SELECT si.name as indexname
 1617+FROM sysibm.sysindexes si
 1618+WHERE si.name='$index' AND si.tbname='$table' AND sc.tbcreator='$this->mSchema'
 1619+AND si.uniquerule IN ('U', 'P')
 1620+SQL;
 1621+ $res = $this->query( $sql, $fname );
 1622+ if ( !$res ) {
 1623+ return null;
 1624+ }
 1625+ if ($this->fetchObject( $res )) {
 1626+ return true;
 1627+ }
 1628+ return false;
 1629+
 1630+ }
 1631+
 1632+ /**
 1633+ * Returns the size of a text field, or -1 for "unlimited"
 1634+ * @param string $table table name
 1635+ * @param string $field column name
 1636+ * @return int length or -1 for unlimited
 1637+ */
 1638+ public function textFieldSize( $table, $field ) {
 1639+ $table = $this->tableName( $table );
 1640+ $sql = <<<SQL
 1641+SELECT length as size
 1642+FROM sysibm.syscolumns sc
 1643+WHERE sc.name='$field' AND sc.tbname='$table' AND sc.tbcreator='$this->mSchema'
 1644+SQL;
 1645+ $res = $this->query($sql);
 1646+ $row = $this->fetchObject($res);
 1647+ $size = $row->size;
 1648+ $this->freeResult( $res );
 1649+ return $size;
 1650+ }
 1651+
 1652+ /**
 1653+ * DELETE where the condition is a join
 1654+ * @param string $delTable deleting from this table
 1655+ * @param string $joinTable using data from this table
 1656+ * @param string $delVar variable in deleteable table
 1657+ * @param string $joinVar variable in data table
 1658+ * @param array $conds conditionals for join table
 1659+ * @param string $fname function name for profiling
 1660+ */
 1661+ public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseIbm_db2::deleteJoin" ) {
 1662+ if ( !$conds ) {
 1663+ throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
 1664+ }
 1665+
 1666+ $delTable = $this->tableName( $delTable );
 1667+ $joinTable = $this->tableName( $joinTable );
 1668+ $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
 1669+ if ( $conds != '*' ) {
 1670+ $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
 1671+ }
 1672+ $sql .= ')';
 1673+
 1674+ $this->query( $sql, $fname );
 1675+ }
 1676+
 1677+ /**
 1678+ * Description is left as an exercise for the reader
 1679+ * @param mixed $b data to be encoded
 1680+ * @return IBM_DB2Blob
 1681+ */
 1682+ public function encodeBlob($b) {
 1683+ return new IBM_DB2Blob($b);
 1684+ }
 1685+
 1686+ /**
 1687+ * Description is left as an exercise for the reader
 1688+ * @param IBM_DB2Blob $b data to be decoded
 1689+ * @return mixed
 1690+ */
 1691+ public function decodeBlob($b) {
 1692+ return $b->getData();
 1693+ }
 1694+
 1695+ /**
 1696+ * Convert into a list of string being concatenated
 1697+ * @param array $stringList strings that need to be joined together by the SQL engine
 1698+ * @return string joined by the concatenation operator
 1699+ */
 1700+ public function buildConcat( $stringList ) {
 1701+ // || is equivalent to CONCAT
 1702+ // Sample query: VALUES 'foo' CONCAT 'bar' CONCAT 'baz'
 1703+ return implode( ' || ', $stringList );
 1704+ }
 1705+
 1706+ /**
 1707+ * Generates the SQL required to convert a DB2 timestamp into a Unix epoch
 1708+ * @param string $column name of timestamp column
 1709+ * @return string SQL code
 1710+ */
 1711+ public function extractUnixEpoch( $column ) {
 1712+ // TODO
 1713+ // see SpecialAncientpages
 1714+ }
 1715+
 1716+ ######################################
 1717+ # Prepared statements
 1718+ ######################################
 1719+
 1720+ /**
 1721+ * Intended to be compatible with the PEAR::DB wrapper functions.
 1722+ * http://pear.php.net/manual/en/package.database.db.intro-execute.php
 1723+ *
 1724+ * ? = scalar value, quoted as necessary
 1725+ * ! = raw SQL bit (a function for instance)
 1726+ * & = filename; reads the file and inserts as a blob
 1727+ * (we don't use this though...)
 1728+ * @param string $sql SQL statement with appropriate markers
 1729+ * @return resource a prepared DB2 SQL statement
 1730+ */
 1731+ public function prepare( $sql, $func = 'DB2::prepare' ) {
 1732+ $stmt = db2_prepare($this->mConn, $sql, $this->mStmtOptions);
 1733+ return $stmt;
 1734+ }
 1735+
 1736+ /**
 1737+ * Frees resources associated with a prepared statement
 1738+ * @return bool success or failure
 1739+ */
 1740+ public function freePrepared( $prepared ) {
 1741+ return db2_free_stmt($prepared);
 1742+ }
 1743+
 1744+ /**
 1745+ * Execute a prepared query with the various arguments
 1746+ * @param string $prepared the prepared sql
 1747+ * @param mixed $args Either an array here, or put scalars as varargs
 1748+ * @return resource Results object
 1749+ */
 1750+ public function execute( $prepared, $args = null ) {
 1751+ if( !is_array( $args ) ) {
 1752+ # Pull the var args
 1753+ $args = func_get_args();
 1754+ array_shift( $args );
 1755+ }
 1756+ $res = db2_execute($prepared, $args);
 1757+ return $res;
 1758+ }
 1759+
 1760+ /**
 1761+ * Prepare & execute an SQL statement, quoting and inserting arguments
 1762+ * in the appropriate places.
 1763+ * @param $query String
 1764+ * @param $args ...
 1765+ */
 1766+ public function safeQuery( $query, $args = null ) {
 1767+ // copied verbatim from Database.php
 1768+ $prepared = $this->prepare( $query, 'DB2::safeQuery' );
 1769+ if( !is_array( $args ) ) {
 1770+ # Pull the var args
 1771+ $args = func_get_args();
 1772+ array_shift( $args );
 1773+ }
 1774+ $retval = $this->execute( $prepared, $args );
 1775+ $this->freePrepared( $prepared );
 1776+ return $retval;
 1777+ }
 1778+
 1779+ /**
 1780+ * For faking prepared SQL statements on DBs that don't support
 1781+ * it directly.
 1782+ * @param resource $preparedQuery String: a 'preparable' SQL statement
 1783+ * @param array $args Array of arguments to fill it with
 1784+ * @return string executable statement
 1785+ */
 1786+ public function fillPrepared( $preparedQuery, $args ) {
 1787+ reset( $args );
 1788+ $this->preparedArgs =& $args;
 1789+
 1790+ foreach ($args as $i => $arg) {
 1791+ db2_bind_param($preparedQuery, $i+1, $args[$i]);
 1792+ }
 1793+
 1794+ return $preparedQuery;
 1795+ }
 1796+
 1797+ /**
 1798+ * Switches module between regular and install modes
 1799+ */
 1800+ public function setMode($mode) {
 1801+ $old = $this->mMode;
 1802+ $this->mMode = $mode;
 1803+ return $old;
 1804+ }
 1805+
 1806+ /**
 1807+ * Bitwise negation of a column or value in SQL
 1808+ * Same as (~field) in C
 1809+ * @param string $field
 1810+ * @return string
 1811+ */
 1812+ function bitNot($field) {
 1813+ //expecting bit-fields smaller than 4bytes
 1814+ return 'BITNOT('.$bitField.')';
 1815+ }
 1816+
 1817+ /**
 1818+ * Bitwise AND of two columns or values in SQL
 1819+ * Same as (fieldLeft & fieldRight) in C
 1820+ * @param string $fieldLeft
 1821+ * @param string $fieldRight
 1822+ * @return string
 1823+ */
 1824+ function bitAnd($fieldLeft, $fieldRight) {
 1825+ return 'BITAND('.$fieldLeft.', '.$fieldRight.')';
 1826+ }
 1827+
 1828+ /**
 1829+ * Bitwise OR of two columns or values in SQL
 1830+ * Same as (fieldLeft | fieldRight) in C
 1831+ * @param string $fieldLeft
 1832+ * @param string $fieldRight
 1833+ * @return string
 1834+ */
 1835+ function bitOr($fieldLeft, $fieldRight) {
 1836+ return 'BITOR('.$fieldLeft.', '.$fieldRight.')';
 1837+ }
 1838+}
Property changes on: trunk/extensions/MSSQLBackCompat/db/DatabaseIbm_db2.php
___________________________________________________________________
Added: svn:eol-style
11839 + native
Index: trunk/extensions/MSSQLBackCompat/db/Database.php
@@ -0,0 +1,2861 @@
 2+<?php
 3+/**
 4+ * @defgroup Database Database
 5+ *
 6+ * @file
 7+ * @ingroup Database
 8+ * This file deals with MySQL interface functions
 9+ * and query specifics/optimisations
 10+ */
 11+
 12+/** Number of times to re-try an operation in case of deadlock */
 13+define( 'DEADLOCK_TRIES', 4 );
 14+/** Minimum time to wait before retry, in microseconds */
 15+define( 'DEADLOCK_DELAY_MIN', 500000 );
 16+/** Maximum time to wait before retry */
 17+define( 'DEADLOCK_DELAY_MAX', 1500000 );
 18+
 19+/**
 20+ * Database abstraction object
 21+ * @ingroup Database
 22+ */
 23+abstract class DatabaseBase {
 24+
 25+#------------------------------------------------------------------------------
 26+# Variables
 27+#------------------------------------------------------------------------------
 28+
 29+ protected $mLastQuery = '';
 30+ protected $mDoneWrites = false;
 31+ protected $mPHPError = false;
 32+
 33+ protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
 34+ protected $mOpened = false;
 35+
 36+ protected $mFailFunction;
 37+ protected $mTablePrefix;
 38+ protected $mFlags;
 39+ protected $mTrxLevel = 0;
 40+ protected $mErrorCount = 0;
 41+ protected $mLBInfo = array();
 42+ protected $mFakeSlaveLag = null, $mFakeMaster = false;
 43+ protected $mDefaultBigSelects = null;
 44+
 45+#------------------------------------------------------------------------------
 46+# Accessors
 47+#------------------------------------------------------------------------------
 48+ # These optionally set a variable and return the previous state
 49+
 50+ /**
 51+ * Fail function, takes a Database as a parameter
 52+ * Set to false for default, 1 for ignore errors
 53+ */
 54+ function failFunction( $function = null ) {
 55+ return wfSetVar( $this->mFailFunction, $function );
 56+ }
 57+
 58+ /**
 59+ * Output page, used for reporting errors
 60+ * FALSE means discard output
 61+ */
 62+ function setOutputPage( $out ) {
 63+ wfDeprecated( __METHOD__ );
 64+ }
 65+
 66+ /**
 67+ * Boolean, controls output of large amounts of debug information
 68+ */
 69+ function debug( $debug = null ) {
 70+ return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
 71+ }
 72+
 73+ /**
 74+ * Turns buffering of SQL result sets on (true) or off (false).
 75+ * Default is "on" and it should not be changed without good reasons.
 76+ */
 77+ function bufferResults( $buffer = null ) {
 78+ if ( is_null( $buffer ) ) {
 79+ return !(bool)( $this->mFlags & DBO_NOBUFFER );
 80+ } else {
 81+ return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
 82+ }
 83+ }
 84+
 85+ /**
 86+ * Turns on (false) or off (true) the automatic generation and sending
 87+ * of a "we're sorry, but there has been a database error" page on
 88+ * database errors. Default is on (false). When turned off, the
 89+ * code should use lastErrno() and lastError() to handle the
 90+ * situation as appropriate.
 91+ */
 92+ function ignoreErrors( $ignoreErrors = null ) {
 93+ return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
 94+ }
 95+
 96+ /**
 97+ * The current depth of nested transactions
 98+ * @param $level Integer: , default NULL.
 99+ */
 100+ function trxLevel( $level = null ) {
 101+ return wfSetVar( $this->mTrxLevel, $level );
 102+ }
 103+
 104+ /**
 105+ * Number of errors logged, only useful when errors are ignored
 106+ */
 107+ function errorCount( $count = null ) {
 108+ return wfSetVar( $this->mErrorCount, $count );
 109+ }
 110+
 111+ function tablePrefix( $prefix = null ) {
 112+ return wfSetVar( $this->mTablePrefix, $prefix );
 113+ }
 114+
 115+ /**
 116+ * Properties passed down from the server info array of the load balancer
 117+ */
 118+ function getLBInfo( $name = null ) {
 119+ if ( is_null( $name ) ) {
 120+ return $this->mLBInfo;
 121+ } else {
 122+ if ( array_key_exists( $name, $this->mLBInfo ) ) {
 123+ return $this->mLBInfo[$name];
 124+ } else {
 125+ return null;
 126+ }
 127+ }
 128+ }
 129+
 130+ function setLBInfo( $name, $value = null ) {
 131+ if ( is_null( $value ) ) {
 132+ $this->mLBInfo = $name;
 133+ } else {
 134+ $this->mLBInfo[$name] = $value;
 135+ }
 136+ }
 137+
 138+ /**
 139+ * Set lag time in seconds for a fake slave
 140+ */
 141+ function setFakeSlaveLag( $lag ) {
 142+ $this->mFakeSlaveLag = $lag;
 143+ }
 144+
 145+ /**
 146+ * Make this connection a fake master
 147+ */
 148+ function setFakeMaster( $enabled = true ) {
 149+ $this->mFakeMaster = $enabled;
 150+ }
 151+
 152+ /**
 153+ * Returns true if this database supports (and uses) cascading deletes
 154+ */
 155+ function cascadingDeletes() {
 156+ return false;
 157+ }
 158+
 159+ /**
 160+ * Returns true if this database supports (and uses) triggers (e.g. on the page table)
 161+ */
 162+ function cleanupTriggers() {
 163+ return false;
 164+ }
 165+
 166+ /**
 167+ * Returns true if this database is strict about what can be put into an IP field.
 168+ * Specifically, it uses a NULL value instead of an empty string.
 169+ */
 170+ function strictIPs() {
 171+ return false;
 172+ }
 173+
 174+ /**
 175+ * Returns true if this database uses timestamps rather than integers
 176+ */
 177+ function realTimestamps() {
 178+ return false;
 179+ }
 180+
 181+ /**
 182+ * Returns true if this database does an implicit sort when doing GROUP BY
 183+ */
 184+ function implicitGroupby() {
 185+ return true;
 186+ }
 187+
 188+ /**
 189+ * Returns true if this database does an implicit order by when the column has an index
 190+ * For example: SELECT page_title FROM page LIMIT 1
 191+ */
 192+ function implicitOrderby() {
 193+ return true;
 194+ }
 195+
 196+ /**
 197+ * Returns true if this database requires that SELECT DISTINCT queries require that all
 198+ ORDER BY expressions occur in the SELECT list per the SQL92 standard
 199+ */
 200+ function standardSelectDistinct() {
 201+ return true;
 202+ }
 203+
 204+ /**
 205+ * Returns true if this database can do a native search on IP columns
 206+ * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
 207+ */
 208+ function searchableIPs() {
 209+ return false;
 210+ }
 211+
 212+ /**
 213+ * Returns true if this database can use functional indexes
 214+ */
 215+ function functionalIndexes() {
 216+ return false;
 217+ }
 218+
 219+ /**
 220+ * Return the last query that went through Database::query()
 221+ * @return String
 222+ */
 223+ function lastQuery() { return $this->mLastQuery; }
 224+
 225+
 226+ /**
 227+ * Returns true if the connection may have been used for write queries.
 228+ * Should return true if unsure.
 229+ */
 230+ function doneWrites() { return $this->mDoneWrites; }
 231+
 232+ /**
 233+ * Is a connection to the database open?
 234+ * @return Boolean
 235+ */
 236+ function isOpen() { return $this->mOpened; }
 237+
 238+ /**
 239+ * Set a flag for this connection
 240+ *
 241+ * @param $flag Integer: DBO_* constants from Defines.php:
 242+ * - DBO_DEBUG: output some debug info (same as debug())
 243+ * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
 244+ * - DBO_IGNORE: ignore errors (same as ignoreErrors())
 245+ * - DBO_TRX: automatically start transactions
 246+ * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode
 247+ * and removes it in command line mode
 248+ * - DBO_PERSISTENT: use persistant database connection
 249+ */
 250+ function setFlag( $flag ) {
 251+ $this->mFlags |= $flag;
 252+ }
 253+
 254+ /**
 255+ * Clear a flag for this connection
 256+ *
 257+ * @param $flag: same as setFlag()'s $flag param
 258+ */
 259+ function clearFlag( $flag ) {
 260+ $this->mFlags &= ~$flag;
 261+ }
 262+
 263+ /**
 264+ * Returns a boolean whether the flag $flag is set for this connection
 265+ *
 266+ * @param $flag: same as setFlag()'s $flag param
 267+ * @return Boolean
 268+ */
 269+ function getFlag( $flag ) {
 270+ return !!($this->mFlags & $flag);
 271+ }
 272+
 273+ /**
 274+ * General read-only accessor
 275+ */
 276+ function getProperty( $name ) {
 277+ return $this->$name;
 278+ }
 279+
 280+ function getWikiID() {
 281+ if( $this->mTablePrefix ) {
 282+ return "{$this->mDBname}-{$this->mTablePrefix}";
 283+ } else {
 284+ return $this->mDBname;
 285+ }
 286+ }
 287+
 288+ /**
 289+ * Get the type of the DBMS, as it appears in $wgDBtype.
 290+ */
 291+ abstract function getType();
 292+
 293+#------------------------------------------------------------------------------
 294+# Other functions
 295+#------------------------------------------------------------------------------
 296+
 297+ /**
 298+ * Constructor.
 299+ * @param $server String: database server host
 300+ * @param $user String: database user name
 301+ * @param $password String: database user password
 302+ * @param $dbName String: database name
 303+ * @param $failFunction
 304+ * @param $flags
 305+ * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
 306+ */
 307+ function __construct( $server = false, $user = false, $password = false, $dbName = false,
 308+ $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
 309+
 310+ global $wgOut, $wgDBprefix, $wgCommandLineMode;
 311+ # Can't get a reference if it hasn't been set yet
 312+ if ( !isset( $wgOut ) ) {
 313+ $wgOut = null;
 314+ }
 315+
 316+ $this->mFailFunction = $failFunction;
 317+ $this->mFlags = $flags;
 318+
 319+ if ( $this->mFlags & DBO_DEFAULT ) {
 320+ if ( $wgCommandLineMode ) {
 321+ $this->mFlags &= ~DBO_TRX;
 322+ } else {
 323+ $this->mFlags |= DBO_TRX;
 324+ }
 325+ }
 326+
 327+ /*
 328+ // Faster read-only access
 329+ if ( wfReadOnly() ) {
 330+ $this->mFlags |= DBO_PERSISTENT;
 331+ $this->mFlags &= ~DBO_TRX;
 332+ }*/
 333+
 334+ /** Get the default table prefix*/
 335+ if ( $tablePrefix == 'get from global' ) {
 336+ $this->mTablePrefix = $wgDBprefix;
 337+ } else {
 338+ $this->mTablePrefix = $tablePrefix;
 339+ }
 340+
 341+ if ( $server ) {
 342+ $this->open( $server, $user, $password, $dbName );
 343+ }
 344+ }
 345+
 346+ /**
 347+ * Same as new DatabaseMysql( ... ), kept for backward compatibility
 348+ * @param $server String: database server host
 349+ * @param $user String: database user name
 350+ * @param $password String: database user password
 351+ * @param $dbName String: database name
 352+ * @param failFunction
 353+ * @param $flags
 354+ */
 355+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
 356+ {
 357+ return new DatabaseMysql( $server, $user, $password, $dbName, $failFunction, $flags );
 358+ }
 359+
 360+ /**
 361+ * Usually aborts on failure
 362+ * If the failFunction is set to a non-zero integer, returns success
 363+ * @param $server String: database server host
 364+ * @param $user String: database user name
 365+ * @param $password String: database user password
 366+ * @param $dbName String: database name
 367+ */
 368+ abstract function open( $server, $user, $password, $dbName );
 369+
 370+ protected function installErrorHandler() {
 371+ $this->mPHPError = false;
 372+ $this->htmlErrors = ini_set( 'html_errors', '0' );
 373+ set_error_handler( array( $this, 'connectionErrorHandler' ) );
 374+ }
 375+
 376+ protected function restoreErrorHandler() {
 377+ restore_error_handler();
 378+ if ( $this->htmlErrors !== false ) {
 379+ ini_set( 'html_errors', $this->htmlErrors );
 380+ }
 381+ if ( $this->mPHPError ) {
 382+ $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
 383+ $error = preg_replace( '!^.*?:(.*)$!', '$1', $error );
 384+ return $error;
 385+ } else {
 386+ return false;
 387+ }
 388+ }
 389+
 390+ protected function connectionErrorHandler( $errno, $errstr ) {
 391+ $this->mPHPError = $errstr;
 392+ }
 393+
 394+ /**
 395+ * Closes a database connection.
 396+ * if it is open : commits any open transactions
 397+ *
 398+ * @return Bool operation success. true if already closed.
 399+ */
 400+ function close() {
 401+ # Stub, should probably be overridden
 402+ return true;
 403+ }
 404+
 405+ /**
 406+ * @param $error String: fallback error message, used if none is given by MySQL
 407+ */
 408+ function reportConnectionError( $error = 'Unknown error' ) {
 409+ $myError = $this->lastError();
 410+ if ( $myError ) {
 411+ $error = $myError;
 412+ }
 413+
 414+ if ( $this->mFailFunction ) {
 415+ # Legacy error handling method
 416+ if ( !is_int( $this->mFailFunction ) ) {
 417+ $ff = $this->mFailFunction;
 418+ $ff( $this, $error );
 419+ }
 420+ } else {
 421+ # New method
 422+ throw new DBConnectionError( $this, $error );
 423+ }
 424+ }
 425+
 426+ /**
 427+ * Determine whether a query writes to the DB.
 428+ * Should return true if unsure.
 429+ */
 430+ function isWriteQuery( $sql ) {
 431+ return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW|\(SELECT)\b/i', $sql );
 432+ }
 433+
 434+ /**
 435+ * Usually aborts on failure. If errors are explicitly ignored, returns success.
 436+ *
 437+ * @param $sql String: SQL query
 438+ * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
 439+ * comment (you can use __METHOD__ or add some extra info)
 440+ * @param $tempIgnore Boolean: Whether to avoid throwing an exception on errors...
 441+ * maybe best to catch the exception instead?
 442+ * @return true for a successful write query, ResultWrapper object for a successful read query,
 443+ * or false on failure if $tempIgnore set
 444+ * @throws DBQueryError Thrown when the database returns an error of any kind
 445+ */
 446+ public function query( $sql, $fname = '', $tempIgnore = false ) {
 447+ global $wgProfiler;
 448+
 449+ $isMaster = !is_null( $this->getLBInfo( 'master' ) );
 450+ if ( isset( $wgProfiler ) ) {
 451+ # generalizeSQL will probably cut down the query to reasonable
 452+ # logging size most of the time. The substr is really just a sanity check.
 453+
 454+ # Who's been wasting my precious column space? -- TS
 455+ #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
 456+
 457+ if ( $isMaster ) {
 458+ $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
 459+ $totalProf = 'Database::query-master';
 460+ } else {
 461+ $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
 462+ $totalProf = 'Database::query';
 463+ }
 464+ wfProfileIn( $totalProf );
 465+ wfProfileIn( $queryProf );
 466+ }
 467+
 468+ $this->mLastQuery = $sql;
 469+ if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) {
 470+ // Set a flag indicating that writes have been done
 471+ wfDebug( __METHOD__.": Writes done: $sql\n" );
 472+ $this->mDoneWrites = true;
 473+ }
 474+
 475+ # Add a comment for easy SHOW PROCESSLIST interpretation
 476+ #if ( $fname ) {
 477+ global $wgUser;
 478+ if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
 479+ $userName = $wgUser->getName();
 480+ if ( mb_strlen( $userName ) > 15 ) {
 481+ $userName = mb_substr( $userName, 0, 15 ) . '...';
 482+ }
 483+ $userName = str_replace( '/', '', $userName );
 484+ } else {
 485+ $userName = '';
 486+ }
 487+ $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
 488+ #} else {
 489+ # $commentedSql = $sql;
 490+ #}
 491+
 492+ # If DBO_TRX is set, start a transaction
 493+ if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
 494+ $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
 495+ // avoid establishing transactions for SHOW and SET statements too -
 496+ // that would delay transaction initializations to once connection
 497+ // is really used by application
 498+ $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
 499+ if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
 500+ $this->begin();
 501+ }
 502+
 503+ if ( $this->debug() ) {
 504+ $sqlx = substr( $commentedSql, 0, 500 );
 505+ $sqlx = strtr( $sqlx, "\t\n", ' ' );
 506+ if ( $isMaster ) {
 507+ wfDebug( "SQL-master: $sqlx\n" );
 508+ } else {
 509+ wfDebug( "SQL: $sqlx\n" );
 510+ }
 511+ }
 512+
 513+ if ( istainted( $sql ) & TC_MYSQL ) {
 514+ throw new MWException( 'Tainted query found' );
 515+ }
 516+
 517+ # Do the query and handle errors
 518+ $ret = $this->doQuery( $commentedSql );
 519+
 520+ # Try reconnecting if the connection was lost
 521+ if ( false === $ret && $this->wasErrorReissuable() ) {
 522+ # Transaction is gone, like it or not
 523+ $this->mTrxLevel = 0;
 524+ wfDebug( "Connection lost, reconnecting...\n" );
 525+ if ( $this->ping() ) {
 526+ wfDebug( "Reconnected\n" );
 527+ $sqlx = substr( $commentedSql, 0, 500 );
 528+ $sqlx = strtr( $sqlx, "\t\n", ' ' );
 529+ global $wgRequestTime;
 530+ $elapsed = round( microtime(true) - $wgRequestTime, 3 );
 531+ wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
 532+ $ret = $this->doQuery( $commentedSql );
 533+ } else {
 534+ wfDebug( "Failed\n" );
 535+ }
 536+ }
 537+
 538+ if ( false === $ret ) {
 539+ $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
 540+ }
 541+
 542+ if ( isset( $wgProfiler ) ) {
 543+ wfProfileOut( $queryProf );
 544+ wfProfileOut( $totalProf );
 545+ }
 546+ return $this->resultObject( $ret );
 547+ }
 548+
 549+ /**
 550+ * The DBMS-dependent part of query()
 551+ * @param $sql String: SQL query.
 552+ * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
 553+ * @private
 554+ */
 555+ /*private*/ abstract function doQuery( $sql );
 556+
 557+ /**
 558+ * @param $error String
 559+ * @param $errno Integer
 560+ * @param $sql String
 561+ * @param $fname String
 562+ * @param $tempIgnore Boolean
 563+ */
 564+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
 565+ global $wgCommandLineMode;
 566+ # Ignore errors during error handling to avoid infinite recursion
 567+ $ignore = $this->ignoreErrors( true );
 568+ ++$this->mErrorCount;
 569+
 570+ if( $ignore || $tempIgnore ) {
 571+ wfDebug("SQL ERROR (ignored): $error\n");
 572+ $this->ignoreErrors( $ignore );
 573+ } else {
 574+ $sql1line = str_replace( "\n", "\\n", $sql );
 575+ wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
 576+ wfDebug("SQL ERROR: " . $error . "\n");
 577+ throw new DBQueryError( $this, $error, $errno, $sql, $fname );
 578+ }
 579+ }
 580+
 581+
 582+ /**
 583+ * Intended to be compatible with the PEAR::DB wrapper functions.
 584+ * http://pear.php.net/manual/en/package.database.db.intro-execute.php
 585+ *
 586+ * ? = scalar value, quoted as necessary
 587+ * ! = raw SQL bit (a function for instance)
 588+ * & = filename; reads the file and inserts as a blob
 589+ * (we don't use this though...)
 590+ */
 591+ function prepare( $sql, $func = 'Database::prepare' ) {
 592+ /* MySQL doesn't support prepared statements (yet), so just
 593+ pack up the query for reference. We'll manually replace
 594+ the bits later. */
 595+ return array( 'query' => $sql, 'func' => $func );
 596+ }
 597+
 598+ function freePrepared( $prepared ) {
 599+ /* No-op for MySQL */
 600+ }
 601+
 602+ /**
 603+ * Execute a prepared query with the various arguments
 604+ * @param $prepared String: the prepared sql
 605+ * @param $args Mixed: Either an array here, or put scalars as varargs
 606+ */
 607+ function execute( $prepared, $args = null ) {
 608+ if( !is_array( $args ) ) {
 609+ # Pull the var args
 610+ $args = func_get_args();
 611+ array_shift( $args );
 612+ }
 613+ $sql = $this->fillPrepared( $prepared['query'], $args );
 614+ return $this->query( $sql, $prepared['func'] );
 615+ }
 616+
 617+ /**
 618+ * Prepare & execute an SQL statement, quoting and inserting arguments
 619+ * in the appropriate places.
 620+ * @param $query String
 621+ * @param $args ...
 622+ */
 623+ function safeQuery( $query, $args = null ) {
 624+ $prepared = $this->prepare( $query, 'Database::safeQuery' );
 625+ if( !is_array( $args ) ) {
 626+ # Pull the var args
 627+ $args = func_get_args();
 628+ array_shift( $args );
 629+ }
 630+ $retval = $this->execute( $prepared, $args );
 631+ $this->freePrepared( $prepared );
 632+ return $retval;
 633+ }
 634+
 635+ /**
 636+ * For faking prepared SQL statements on DBs that don't support
 637+ * it directly.
 638+ * @param $preparedQuery String: a 'preparable' SQL statement
 639+ * @param $args Array of arguments to fill it with
 640+ * @return string executable SQL
 641+ */
 642+ function fillPrepared( $preparedQuery, $args ) {
 643+ reset( $args );
 644+ $this->preparedArgs =& $args;
 645+ return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
 646+ array( &$this, 'fillPreparedArg' ), $preparedQuery );
 647+ }
 648+
 649+ /**
 650+ * preg_callback func for fillPrepared()
 651+ * The arguments should be in $this->preparedArgs and must not be touched
 652+ * while we're doing this.
 653+ *
 654+ * @param $matches Array
 655+ * @return String
 656+ * @private
 657+ */
 658+ function fillPreparedArg( $matches ) {
 659+ switch( $matches[1] ) {
 660+ case '\\?': return '?';
 661+ case '\\!': return '!';
 662+ case '\\&': return '&';
 663+ }
 664+ list( /* $n */ , $arg ) = each( $this->preparedArgs );
 665+ switch( $matches[1] ) {
 666+ case '?': return $this->addQuotes( $arg );
 667+ case '!': return $arg;
 668+ case '&':
 669+ # return $this->addQuotes( file_get_contents( $arg ) );
 670+ throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
 671+ default:
 672+ throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
 673+ }
 674+ }
 675+
 676+ /**
 677+ * Free a result object
 678+ * @param $res Mixed: A SQL result
 679+ */
 680+ function freeResult( $res ) {
 681+ # Stub. Might not really need to be overridden, since results should
 682+ # be freed by PHP when the variable goes out of scope anyway.
 683+ }
 684+
 685+ /**
 686+ * Fetch the next row from the given result object, in object form.
 687+ * Fields can be retrieved with $row->fieldname, with fields acting like
 688+ * member variables.
 689+ *
 690+ * @param $res SQL result object as returned from Database::query(), etc.
 691+ * @return MySQL row object
 692+ * @throws DBUnexpectedError Thrown if the database returns an error
 693+ */
 694+ abstract function fetchObject( $res );
 695+
 696+ /**
 697+ * Fetch the next row from the given result object, in associative array
 698+ * form. Fields are retrieved with $row['fieldname'].
 699+ *
 700+ * @param $res SQL result object as returned from Database::query(), etc.
 701+ * @return MySQL row object
 702+ * @throws DBUnexpectedError Thrown if the database returns an error
 703+ */
 704+ abstract function fetchRow( $res );
 705+
 706+ /**
 707+ * Get the number of rows in a result object
 708+ * @param $res Mixed: A SQL result
 709+ */
 710+ abstract function numRows( $res );
 711+
 712+ /**
 713+ * Get the number of fields in a result object
 714+ * See documentation for mysql_num_fields()
 715+ * @param $res Mixed: A SQL result
 716+ */
 717+ abstract function numFields( $res );
 718+
 719+ /**
 720+ * Get a field name in a result object
 721+ * See documentation for mysql_field_name():
 722+ * http://www.php.net/mysql_field_name
 723+ * @param $res Mixed: A SQL result
 724+ * @param $n Integer
 725+ */
 726+ abstract function fieldName( $res, $n );
 727+
 728+ /**
 729+ * Get the inserted value of an auto-increment row
 730+ *
 731+ * The value inserted should be fetched from nextSequenceValue()
 732+ *
 733+ * Example:
 734+ * $id = $dbw->nextSequenceValue('page_page_id_seq');
 735+ * $dbw->insert('page',array('page_id' => $id));
 736+ * $id = $dbw->insertId();
 737+ */
 738+ abstract function insertId();
 739+
 740+ /**
 741+ * Change the position of the cursor in a result object
 742+ * See mysql_data_seek()
 743+ * @param $res Mixed: A SQL result
 744+ * @param $row Mixed: Either MySQL row or ResultWrapper
 745+ */
 746+ abstract function dataSeek( $res, $row );
 747+
 748+ /**
 749+ * Get the last error number
 750+ * See mysql_errno()
 751+ */
 752+ abstract function lastErrno();
 753+
 754+ /**
 755+ * Get a description of the last error
 756+ * See mysql_error() for more details
 757+ */
 758+ abstract function lastError();
 759+
 760+ /**
 761+ * Get the number of rows affected by the last write query
 762+ * See mysql_affected_rows() for more details
 763+ */
 764+ abstract function affectedRows();
 765+
 766+ /**
 767+ * Simple UPDATE wrapper
 768+ * Usually aborts on failure
 769+ * If errors are explicitly ignored, returns success
 770+ *
 771+ * This function exists for historical reasons, Database::update() has a more standard
 772+ * calling convention and feature set
 773+ */
 774+ function set( $table, $var, $value, $cond, $fname = 'Database::set' ) {
 775+ $table = $this->tableName( $table );
 776+ $sql = "UPDATE $table SET $var = '" .
 777+ $this->strencode( $value ) . "' WHERE ($cond)";
 778+ return (bool)$this->query( $sql, $fname );
 779+ }
 780+
 781+ /**
 782+ * Simple SELECT wrapper, returns a single field, input must be encoded
 783+ * Usually aborts on failure
 784+ * If errors are explicitly ignored, returns FALSE on failure
 785+ */
 786+ function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
 787+ if ( !is_array( $options ) ) {
 788+ $options = array( $options );
 789+ }
 790+ $options['LIMIT'] = 1;
 791+
 792+ $res = $this->select( $table, $var, $cond, $fname, $options );
 793+ if ( $res === false || !$this->numRows( $res ) ) {
 794+ return false;
 795+ }
 796+ $row = $this->fetchRow( $res );
 797+ if ( $row !== false ) {
 798+ $this->freeResult( $res );
 799+ return reset( $row );
 800+ } else {
 801+ return false;
 802+ }
 803+ }
 804+
 805+ /**
 806+ * Returns an optional USE INDEX clause to go after the table, and a
 807+ * string to go at the end of the query
 808+ *
 809+ * @private
 810+ *
 811+ * @param $options Array: associative array of options to be turned into
 812+ * an SQL query, valid keys are listed in the function.
 813+ * @return Array
 814+ */
 815+ function makeSelectOptions( $options ) {
 816+ $preLimitTail = $postLimitTail = '';
 817+ $startOpts = '';
 818+
 819+ $noKeyOptions = array();
 820+ foreach ( $options as $key => $option ) {
 821+ if ( is_numeric( $key ) ) {
 822+ $noKeyOptions[$option] = true;
 823+ }
 824+ }
 825+
 826+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
 827+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
 828+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
 829+
 830+ //if (isset($options['LIMIT'])) {
 831+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
 832+ // isset($options['OFFSET']) ? $options['OFFSET']
 833+ // : false);
 834+ //}
 835+
 836+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
 837+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
 838+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
 839+
 840+ # Various MySQL extensions
 841+ if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
 842+ if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
 843+ if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
 844+ if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
 845+ if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
 846+ if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
 847+ if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
 848+ if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
 849+
 850+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
 851+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
 852+ } else {
 853+ $useIndex = '';
 854+ }
 855+
 856+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
 857+ }
 858+
 859+ /**
 860+ * SELECT wrapper
 861+ *
 862+ * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
 863+ * @param $vars Mixed: Array or string, field name(s) to be retrieved
 864+ * @param $conds Mixed: Array or string, condition(s) for WHERE
 865+ * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
 866+ * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
 867+ * see Database::makeSelectOptions code for list of supported stuff
 868+ * @param $join_conds Array: Associative array of table join conditions (optional)
 869+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
 870+ * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
 871+ */
 872+ function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
 873+ {
 874+ $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
 875+ return $this->query( $sql, $fname );
 876+ }
 877+
 878+ /**
 879+ * SELECT wrapper
 880+ *
 881+ * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
 882+ * @param $vars Mixed: Array or string, field name(s) to be retrieved
 883+ * @param $conds Mixed: Array or string, condition(s) for WHERE
 884+ * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
 885+ * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
 886+ * see Database::makeSelectOptions code for list of supported stuff
 887+ * @param $join_conds Array: Associative array of table join conditions (optional)
 888+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
 889+ * @return string, the SQL text
 890+ */
 891+ function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
 892+ if( is_array( $vars ) ) {
 893+ $vars = implode( ',', $vars );
 894+ }
 895+ if( !is_array( $options ) ) {
 896+ $options = array( $options );
 897+ }
 898+ if( is_array( $table ) ) {
 899+ if ( !empty($join_conds) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) )
 900+ $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
 901+ else
 902+ $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
 903+ } elseif ($table!='') {
 904+ if ($table{0}==' ') {
 905+ $from = ' FROM ' . $table;
 906+ } else {
 907+ $from = ' FROM ' . $this->tableName( $table );
 908+ }
 909+ } else {
 910+ $from = '';
 911+ }
 912+
 913+ list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
 914+
 915+ if( !empty( $conds ) ) {
 916+ if ( is_array( $conds ) ) {
 917+ $conds = $this->makeList( $conds, LIST_AND );
 918+ }
 919+ $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
 920+ } else {
 921+ $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
 922+ }
 923+
 924+ if (isset($options['LIMIT']))
 925+ $sql = $this->limitResult($sql, $options['LIMIT'],
 926+ isset($options['OFFSET']) ? $options['OFFSET'] : false);
 927+ $sql = "$sql $postLimitTail";
 928+
 929+ if (isset($options['EXPLAIN'])) {
 930+ $sql = 'EXPLAIN ' . $sql;
 931+ }
 932+ return $sql;
 933+ }
 934+
 935+ /**
 936+ * Single row SELECT wrapper
 937+ * Aborts or returns FALSE on error
 938+ *
 939+ * @param $table String: table name
 940+ * @param $vars String: the selected variables
 941+ * @param $conds Array: a condition map, terms are ANDed together.
 942+ * Items with numeric keys are taken to be literal conditions
 943+ * Takes an array of selected variables, and a condition map, which is ANDed
 944+ * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
 945+ * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
 946+ * $obj- >page_id is the ID of the Astronomy article
 947+ * @param $fname String: Calling function name
 948+ * @param $options Array
 949+ * @param $join_conds Array
 950+ *
 951+ * @todo migrate documentation to phpdocumentor format
 952+ */
 953+ function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
 954+ $options['LIMIT'] = 1;
 955+ $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
 956+ if ( $res === false )
 957+ return false;
 958+ if ( !$this->numRows($res) ) {
 959+ $this->freeResult($res);
 960+ return false;
 961+ }
 962+ $obj = $this->fetchObject( $res );
 963+ $this->freeResult( $res );
 964+ return $obj;
 965+
 966+ }
 967+
 968+ /**
 969+ * Estimate rows in dataset
 970+ * Returns estimated count - not necessarily an accurate estimate across different databases,
 971+ * so use sparingly
 972+ * Takes same arguments as Database::select()
 973+ *
 974+ * @param string $table table name
 975+ * @param array $vars unused
 976+ * @param array $conds filters on the table
 977+ * @param string $fname function name for profiling
 978+ * @param array $options options for select
 979+ * @return int row count
 980+ */
 981+ public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
 982+ $rows = 0;
 983+ $res = $this->select ( $table, 'COUNT(*) AS rowcount', $conds, $fname, $options );
 984+ if ( $res ) {
 985+ $row = $this->fetchRow( $res );
 986+ $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0;
 987+ }
 988+ $this->freeResult( $res );
 989+ return $rows;
 990+ }
 991+
 992+ /**
 993+ * Removes most variables from an SQL query and replaces them with X or N for numbers.
 994+ * It's only slightly flawed. Don't use for anything important.
 995+ *
 996+ * @param $sql String: A SQL Query
 997+ */
 998+ static function generalizeSQL( $sql ) {
 999+ # This does the same as the regexp below would do, but in such a way
 1000+ # as to avoid crashing php on some large strings.
 1001+ # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
 1002+
 1003+ $sql = str_replace ( "\\\\", '', $sql);
 1004+ $sql = str_replace ( "\\'", '', $sql);
 1005+ $sql = str_replace ( "\\\"", '', $sql);
 1006+ $sql = preg_replace ("/'.*'/s", "'X'", $sql);
 1007+ $sql = preg_replace ('/".*"/s', "'X'", $sql);
 1008+
 1009+ # All newlines, tabs, etc replaced by single space
 1010+ $sql = preg_replace ( '/\s+/', ' ', $sql);
 1011+
 1012+ # All numbers => N
 1013+ $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
 1014+
 1015+ return $sql;
 1016+ }
 1017+
 1018+ /**
 1019+ * Determines whether a field exists in a table
 1020+ * Usually aborts on failure
 1021+ * If errors are explicitly ignored, returns NULL on failure
 1022+ */
 1023+ function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
 1024+ $table = $this->tableName( $table );
 1025+ $res = $this->query( 'DESCRIBE '.$table, $fname );
 1026+ if ( !$res ) {
 1027+ return null;
 1028+ }
 1029+
 1030+ $found = false;
 1031+
 1032+ while ( $row = $this->fetchObject( $res ) ) {
 1033+ if ( $row->Field == $field ) {
 1034+ $found = true;
 1035+ break;
 1036+ }
 1037+ }
 1038+ return $found;
 1039+ }
 1040+
 1041+ /**
 1042+ * Determines whether an index exists
 1043+ * Usually aborts on failure
 1044+ * If errors are explicitly ignored, returns NULL on failure
 1045+ */
 1046+ function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
 1047+ $info = $this->indexInfo( $table, $index, $fname );
 1048+ if ( is_null( $info ) ) {
 1049+ return null;
 1050+ } else {
 1051+ return $info !== false;
 1052+ }
 1053+ }
 1054+
 1055+
 1056+ /**
 1057+ * Get information about an index into an object
 1058+ * Returns false if the index does not exist
 1059+ */
 1060+ function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
 1061+ # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
 1062+ # SHOW INDEX should work for 3.x and up:
 1063+ # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
 1064+ $table = $this->tableName( $table );
 1065+ $index = $this->indexName( $index );
 1066+ $sql = 'SHOW INDEX FROM '.$table;
 1067+ $res = $this->query( $sql, $fname );
 1068+ if ( !$res ) {
 1069+ return null;
 1070+ }
 1071+
 1072+ $result = array();
 1073+ while ( $row = $this->fetchObject( $res ) ) {
 1074+ if ( $row->Key_name == $index ) {
 1075+ $result[] = $row;
 1076+ }
 1077+ }
 1078+ $this->freeResult($res);
 1079+
 1080+ return empty($result) ? false : $result;
 1081+ }
 1082+
 1083+ /**
 1084+ * Query whether a given table exists
 1085+ */
 1086+ function tableExists( $table ) {
 1087+ $table = $this->tableName( $table );
 1088+ $old = $this->ignoreErrors( true );
 1089+ $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
 1090+ $this->ignoreErrors( $old );
 1091+ if( $res ) {
 1092+ $this->freeResult( $res );
 1093+ return true;
 1094+ } else {
 1095+ return false;
 1096+ }
 1097+ }
 1098+
 1099+ /**
 1100+ * mysql_fetch_field() wrapper
 1101+ * Returns false if the field doesn't exist
 1102+ *
 1103+ * @param $table
 1104+ * @param $field
 1105+ */
 1106+ abstract function fieldInfo( $table, $field );
 1107+
 1108+ /**
 1109+ * mysql_field_type() wrapper
 1110+ */
 1111+ function fieldType( $res, $index ) {
 1112+ if ( $res instanceof ResultWrapper ) {
 1113+ $res = $res->result;
 1114+ }
 1115+ return mysql_field_type( $res, $index );
 1116+ }
 1117+
 1118+ /**
 1119+ * Determines if a given index is unique
 1120+ */
 1121+ function indexUnique( $table, $index ) {
 1122+ $indexInfo = $this->indexInfo( $table, $index );
 1123+ if ( !$indexInfo ) {
 1124+ return null;
 1125+ }
 1126+ return !$indexInfo[0]->Non_unique;
 1127+ }
 1128+
 1129+ /**
 1130+ * INSERT wrapper, inserts an array into a table
 1131+ *
 1132+ * $a may be a single associative array, or an array of these with numeric keys, for
 1133+ * multi-row insert.
 1134+ *
 1135+ * Usually aborts on failure
 1136+ * If errors are explicitly ignored, returns success
 1137+ */
 1138+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
 1139+ # No rows to insert, easy just return now
 1140+ if ( !count( $a ) ) {
 1141+ return true;
 1142+ }
 1143+
 1144+ $table = $this->tableName( $table );
 1145+ if ( !is_array( $options ) ) {
 1146+ $options = array( $options );
 1147+ }
 1148+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
 1149+ $multi = true;
 1150+ $keys = array_keys( $a[0] );
 1151+ } else {
 1152+ $multi = false;
 1153+ $keys = array_keys( $a );
 1154+ }
 1155+
 1156+ $sql = 'INSERT ' . implode( ' ', $options ) .
 1157+ " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
 1158+
 1159+ if ( $multi ) {
 1160+ $first = true;
 1161+ foreach ( $a as $row ) {
 1162+ if ( $first ) {
 1163+ $first = false;
 1164+ } else {
 1165+ $sql .= ',';
 1166+ }
 1167+ $sql .= '(' . $this->makeList( $row ) . ')';
 1168+ }
 1169+ } else {
 1170+ $sql .= '(' . $this->makeList( $a ) . ')';
 1171+ }
 1172+ return (bool)$this->query( $sql, $fname );
 1173+ }
 1174+
 1175+ /**
 1176+ * Make UPDATE options for the Database::update function
 1177+ *
 1178+ * @private
 1179+ * @param $options Array: The options passed to Database::update
 1180+ * @return string
 1181+ */
 1182+ function makeUpdateOptions( $options ) {
 1183+ if( !is_array( $options ) ) {
 1184+ $options = array( $options );
 1185+ }
 1186+ $opts = array();
 1187+ if ( in_array( 'LOW_PRIORITY', $options ) )
 1188+ $opts[] = $this->lowPriorityOption();
 1189+ if ( in_array( 'IGNORE', $options ) )
 1190+ $opts[] = 'IGNORE';
 1191+ return implode(' ', $opts);
 1192+ }
 1193+
 1194+ /**
 1195+ * UPDATE wrapper, takes a condition array and a SET array
 1196+ *
 1197+ * @param $table String: The table to UPDATE
 1198+ * @param $values Array: An array of values to SET
 1199+ * @param $conds Array: An array of conditions (WHERE). Use '*' to update all rows.
 1200+ * @param $fname String: The Class::Function calling this function
 1201+ * (for the log)
 1202+ * @param $options Array: An array of UPDATE options, can be one or
 1203+ * more of IGNORE, LOW_PRIORITY
 1204+ * @return Boolean
 1205+ */
 1206+ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
 1207+ $table = $this->tableName( $table );
 1208+ $opts = $this->makeUpdateOptions( $options );
 1209+ $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
 1210+ if ( $conds != '*' ) {
 1211+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
 1212+ }
 1213+ return $this->query( $sql, $fname );
 1214+ }
 1215+
 1216+ /**
 1217+ * Makes an encoded list of strings from an array
 1218+ * $mode:
 1219+ * LIST_COMMA - comma separated, no field names
 1220+ * LIST_AND - ANDed WHERE clause (without the WHERE)
 1221+ * LIST_OR - ORed WHERE clause (without the WHERE)
 1222+ * LIST_SET - comma separated with field names, like a SET clause
 1223+ * LIST_NAMES - comma separated field names
 1224+ */
 1225+ function makeList( $a, $mode = LIST_COMMA ) {
 1226+ if ( !is_array( $a ) ) {
 1227+ throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
 1228+ }
 1229+
 1230+ $first = true;
 1231+ $list = '';
 1232+ foreach ( $a as $field => $value ) {
 1233+ if ( !$first ) {
 1234+ if ( $mode == LIST_AND ) {
 1235+ $list .= ' AND ';
 1236+ } elseif($mode == LIST_OR) {
 1237+ $list .= ' OR ';
 1238+ } else {
 1239+ $list .= ',';
 1240+ }
 1241+ } else {
 1242+ $first = false;
 1243+ }
 1244+ if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
 1245+ $list .= "($value)";
 1246+ } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
 1247+ $list .= "$value";
 1248+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
 1249+ if( count( $value ) == 0 ) {
 1250+ throw new MWException( __METHOD__.': empty input' );
 1251+ } elseif( count( $value ) == 1 ) {
 1252+ // Special-case single values, as IN isn't terribly efficient
 1253+ // Don't necessarily assume the single key is 0; we don't
 1254+ // enforce linear numeric ordering on other arrays here.
 1255+ $value = array_values( $value );
 1256+ $list .= $field." = ".$this->addQuotes( $value[0] );
 1257+ } else {
 1258+ $list .= $field." IN (".$this->makeList($value).") ";
 1259+ }
 1260+ } elseif( $value === null ) {
 1261+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
 1262+ $list .= "$field IS ";
 1263+ } elseif ( $mode == LIST_SET ) {
 1264+ $list .= "$field = ";
 1265+ }
 1266+ $list .= 'NULL';
 1267+ } else {
 1268+ if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
 1269+ $list .= "$field = ";
 1270+ }
 1271+ $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
 1272+ }
 1273+ }
 1274+ return $list;
 1275+ }
 1276+
 1277+ /**
 1278+ * Bitwise operations
 1279+ */
 1280+
 1281+ function bitNot($field) {
 1282+ return "(~$bitField)";
 1283+ }
 1284+
 1285+ function bitAnd($fieldLeft, $fieldRight) {
 1286+ return "($fieldLeft & $fieldRight)";
 1287+ }
 1288+
 1289+ function bitOr($fieldLeft, $fieldRight) {
 1290+ return "($fieldLeft | $fieldRight)";
 1291+ }
 1292+
 1293+ /**
 1294+ * Change the current database
 1295+ *
 1296+ * @return bool Success or failure
 1297+ */
 1298+ function selectDB( $db ) {
 1299+ # Stub. Shouldn't cause serious problems if it's not overridden, but
 1300+ # if your database engine supports a concept similar to MySQL's
 1301+ # databases you may as well. TODO: explain what exactly will fail if
 1302+ # this is not overridden.
 1303+ return true;
 1304+ }
 1305+
 1306+ /**
 1307+ * Get the current DB name
 1308+ */
 1309+ function getDBname() {
 1310+ return $this->mDBname;
 1311+ }
 1312+
 1313+ /**
 1314+ * Get the server hostname or IP address
 1315+ */
 1316+ function getServer() {
 1317+ return $this->mServer;
 1318+ }
 1319+
 1320+ /**
 1321+ * Format a table name ready for use in constructing an SQL query
 1322+ *
 1323+ * This does two important things: it quotes the table names to clean them up,
 1324+ * and it adds a table prefix if only given a table name with no quotes.
 1325+ *
 1326+ * All functions of this object which require a table name call this function
 1327+ * themselves. Pass the canonical name to such functions. This is only needed
 1328+ * when calling query() directly.
 1329+ *
 1330+ * @param $name String: database table name
 1331+ * @return String: full database name
 1332+ */
 1333+ function tableName( $name ) {
 1334+ global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
 1335+ # Skip the entire process when we have a string quoted on both ends.
 1336+ # Note that we check the end so that we will still quote any use of
 1337+ # use of `database`.table. But won't break things if someone wants
 1338+ # to query a database table with a dot in the name.
 1339+ if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
 1340+
 1341+ # Lets test for any bits of text that should never show up in a table
 1342+ # name. Basically anything like JOIN or ON which are actually part of
 1343+ # SQL queries, but may end up inside of the table value to combine
 1344+ # sql. Such as how the API is doing.
 1345+ # Note that we use a whitespace test rather than a \b test to avoid
 1346+ # any remote case where a word like on may be inside of a table name
 1347+ # surrounded by symbols which may be considered word breaks.
 1348+ if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
 1349+
 1350+ # Split database and table into proper variables.
 1351+ # We reverse the explode so that database.table and table both output
 1352+ # the correct table.
 1353+ $dbDetails = array_reverse( explode( '.', $name, 2 ) );
 1354+ if( isset( $dbDetails[1] ) ) @list( $table, $database ) = $dbDetails;
 1355+ else @list( $table ) = $dbDetails;
 1356+ $prefix = $this->mTablePrefix; # Default prefix
 1357+
 1358+ # A database name has been specified in input. Quote the table name
 1359+ # because we don't want any prefixes added.
 1360+ if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
 1361+
 1362+ # Note that we use the long format because php will complain in in_array if
 1363+ # the input is not an array, and will complain in is_array if it is not set.
 1364+ if( !isset( $database ) # Don't use shared database if pre selected.
 1365+ && isset( $wgSharedDB ) # We have a shared database
 1366+ && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
 1367+ && isset( $wgSharedTables )
 1368+ && is_array( $wgSharedTables )
 1369+ && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
 1370+ $database = $wgSharedDB;
 1371+ $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix;
 1372+ }
 1373+
 1374+ # Quote the $database and $table and apply the prefix if not quoted.
 1375+ if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
 1376+ $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
 1377+
 1378+ # Merge our database and table into our final table name.
 1379+ $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
 1380+
 1381+ # We're finished, return.
 1382+ return $tableName;
 1383+ }
 1384+
 1385+ /**
 1386+ * Fetch a number of table names into an array
 1387+ * This is handy when you need to construct SQL for joins
 1388+ *
 1389+ * Example:
 1390+ * extract($dbr->tableNames('user','watchlist'));
 1391+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
 1392+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
 1393+ */
 1394+ public function tableNames() {
 1395+ $inArray = func_get_args();
 1396+ $retVal = array();
 1397+ foreach ( $inArray as $name ) {
 1398+ $retVal[$name] = $this->tableName( $name );
 1399+ }
 1400+ return $retVal;
 1401+ }
 1402+
 1403+ /**
 1404+ * Fetch a number of table names into an zero-indexed numerical array
 1405+ * This is handy when you need to construct SQL for joins
 1406+ *
 1407+ * Example:
 1408+ * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
 1409+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
 1410+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
 1411+ */
 1412+ public function tableNamesN() {
 1413+ $inArray = func_get_args();
 1414+ $retVal = array();
 1415+ foreach ( $inArray as $name ) {
 1416+ $retVal[] = $this->tableName( $name );
 1417+ }
 1418+ return $retVal;
 1419+ }
 1420+
 1421+ /**
 1422+ * @private
 1423+ */
 1424+ function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
 1425+ $ret = array();
 1426+ $retJOIN = array();
 1427+ $use_index_safe = is_array($use_index) ? $use_index : array();
 1428+ $join_conds_safe = is_array($join_conds) ? $join_conds : array();
 1429+ foreach ( $tables as $table ) {
 1430+ // Is there a JOIN and INDEX clause for this table?
 1431+ if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
 1432+ $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
 1433+ $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
 1434+ $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
 1435+ $retJOIN[] = $tableClause;
 1436+ // Is there an INDEX clause?
 1437+ } else if ( isset($use_index_safe[$table]) ) {
 1438+ $tableClause = $this->tableName( $table );
 1439+ $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
 1440+ $ret[] = $tableClause;
 1441+ // Is there a JOIN clause?
 1442+ } else if ( isset($join_conds_safe[$table]) ) {
 1443+ $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
 1444+ $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
 1445+ $retJOIN[] = $tableClause;
 1446+ } else {
 1447+ $tableClause = $this->tableName( $table );
 1448+ $ret[] = $tableClause;
 1449+ }
 1450+ }
 1451+ // We can't separate explicit JOIN clauses with ',', use ' ' for those
 1452+ $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
 1453+ $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
 1454+ // Compile our final table clause
 1455+ return implode(' ',array($straightJoins,$otherJoins) );
 1456+ }
 1457+
 1458+ /**
 1459+ * Get the name of an index in a given table
 1460+ */
 1461+ function indexName( $index ) {
 1462+ // Backwards-compatibility hack
 1463+ $renamed = array(
 1464+ 'ar_usertext_timestamp' => 'usertext_timestamp',
 1465+ 'un_user_id' => 'user_id',
 1466+ 'un_user_ip' => 'user_ip',
 1467+ );
 1468+ if( isset( $renamed[$index] ) ) {
 1469+ return $renamed[$index];
 1470+ } else {
 1471+ return $index;
 1472+ }
 1473+ }
 1474+
 1475+ /**
 1476+ * Wrapper for addslashes()
 1477+ * @param $s String: to be slashed.
 1478+ * @return String: slashed string.
 1479+ */
 1480+ abstract function strencode( $s );
 1481+
 1482+ /**
 1483+ * If it's a string, adds quotes and backslashes
 1484+ * Otherwise returns as-is
 1485+ */
 1486+ function addQuotes( $s ) {
 1487+ if ( $s === null ) {
 1488+ return 'NULL';
 1489+ } else {
 1490+ # This will also quote numeric values. This should be harmless,
 1491+ # and protects against weird problems that occur when they really
 1492+ # _are_ strings such as article titles and string->number->string
 1493+ # conversion is not 1:1.
 1494+ return "'" . $this->strencode( $s ) . "'";
 1495+ }
 1496+ }
 1497+
 1498+ /**
 1499+ * Escape string for safe LIKE usage.
 1500+ * WARNING: you should almost never use this function directly,
 1501+ * instead use buildLike() that escapes everything automatically
 1502+ */
 1503+ function escapeLike( $s ) {
 1504+ $s = str_replace( '\\', '\\\\', $s );
 1505+ $s = $this->strencode( $s );
 1506+ $s = str_replace( array( '%', '_' ), array( '\%', '\_' ), $s );
 1507+ return $s;
 1508+ }
 1509+
 1510+ /**
 1511+ * LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match
 1512+ * containing either string literals that will be escaped or tokens returned by anyChar() or anyString().
 1513+ * Alternatively, the function could be provided with an array of aforementioned parameters.
 1514+ *
 1515+ * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns a LIKE clause that searches
 1516+ * for subpages of 'My page title'.
 1517+ * Alternatively: $pattern = array( 'My_page_title/', $dbr->anyString() ); $query .= $dbr->buildLike( $pattern );
 1518+ *
 1519+ * @ return String: fully built LIKE statement
 1520+ */
 1521+ function buildLike() {
 1522+ $params = func_get_args();
 1523+ if (count($params) > 0 && is_array($params[0])) {
 1524+ $params = $params[0];
 1525+ }
 1526+
 1527+ $s = '';
 1528+ foreach( $params as $value) {
 1529+ if( $value instanceof LikeMatch ) {
 1530+ $s .= $value->toString();
 1531+ } else {
 1532+ $s .= $this->escapeLike( $value );
 1533+ }
 1534+ }
 1535+ return " LIKE '" . $s . "' ";
 1536+ }
 1537+
 1538+ /**
 1539+ * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query
 1540+ */
 1541+ function anyChar() {
 1542+ return new LikeMatch( '_' );
 1543+ }
 1544+
 1545+ /**
 1546+ * Returns a token for buildLike() that denotes a '%' to be used in a LIKE query
 1547+ */
 1548+ function anyString() {
 1549+ return new LikeMatch( '%' );
 1550+ }
 1551+
 1552+ /**
 1553+ * Returns an appropriately quoted sequence value for inserting a new row.
 1554+ * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
 1555+ * subclass will return an integer, and save the value for insertId()
 1556+ */
 1557+ function nextSequenceValue( $seqName ) {
 1558+ return null;
 1559+ }
 1560+
 1561+ /**
 1562+ * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
 1563+ * is only needed because a) MySQL must be as efficient as possible due to
 1564+ * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
 1565+ * which index to pick. Anyway, other databases might have different
 1566+ * indexes on a given table. So don't bother overriding this unless you're
 1567+ * MySQL.
 1568+ */
 1569+ function useIndexClause( $index ) {
 1570+ return '';
 1571+ }
 1572+
 1573+ /**
 1574+ * REPLACE query wrapper
 1575+ * PostgreSQL simulates this with a DELETE followed by INSERT
 1576+ * $row is the row to insert, an associative array
 1577+ * $uniqueIndexes is an array of indexes. Each element may be either a
 1578+ * field name or an array of field names
 1579+ *
 1580+ * It may be more efficient to leave off unique indexes which are unlikely to collide.
 1581+ * However if you do this, you run the risk of encountering errors which wouldn't have
 1582+ * occurred in MySQL
 1583+ *
 1584+ * @todo migrate comment to phodocumentor format
 1585+ */
 1586+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
 1587+ $table = $this->tableName( $table );
 1588+
 1589+ # Single row case
 1590+ if ( !is_array( reset( $rows ) ) ) {
 1591+ $rows = array( $rows );
 1592+ }
 1593+
 1594+ $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
 1595+ $first = true;
 1596+ foreach ( $rows as $row ) {
 1597+ if ( $first ) {
 1598+ $first = false;
 1599+ } else {
 1600+ $sql .= ',';
 1601+ }
 1602+ $sql .= '(' . $this->makeList( $row ) . ')';
 1603+ }
 1604+ return $this->query( $sql, $fname );
 1605+ }
 1606+
 1607+ /**
 1608+ * DELETE where the condition is a join
 1609+ * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
 1610+ *
 1611+ * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
 1612+ * join condition matches, set $conds='*'
 1613+ *
 1614+ * DO NOT put the join condition in $conds
 1615+ *
 1616+ * @param $delTable String: The table to delete from.
 1617+ * @param $joinTable String: The other table.
 1618+ * @param $delVar String: The variable to join on, in the first table.
 1619+ * @param $joinVar String: The variable to join on, in the second table.
 1620+ * @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause
 1621+ * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
 1622+ */
 1623+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
 1624+ if ( !$conds ) {
 1625+ throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
 1626+ }
 1627+
 1628+ $delTable = $this->tableName( $delTable );
 1629+ $joinTable = $this->tableName( $joinTable );
 1630+ $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
 1631+ if ( $conds != '*' ) {
 1632+ $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
 1633+ }
 1634+
 1635+ return $this->query( $sql, $fname );
 1636+ }
 1637+
 1638+ /**
 1639+ * Returns the size of a text field, or -1 for "unlimited"
 1640+ */
 1641+ function textFieldSize( $table, $field ) {
 1642+ $table = $this->tableName( $table );
 1643+ $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
 1644+ $res = $this->query( $sql, 'Database::textFieldSize' );
 1645+ $row = $this->fetchObject( $res );
 1646+ $this->freeResult( $res );
 1647+
 1648+ $m = array();
 1649+ if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
 1650+ $size = $m[1];
 1651+ } else {
 1652+ $size = -1;
 1653+ }
 1654+ return $size;
 1655+ }
 1656+
 1657+ /**
 1658+ * A string to insert into queries to show that they're low-priority, like
 1659+ * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
 1660+ * string and nothing bad should happen.
 1661+ *
 1662+ * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
 1663+ */
 1664+ function lowPriorityOption() {
 1665+ return '';
 1666+ }
 1667+
 1668+ /**
 1669+ * DELETE query wrapper
 1670+ *
 1671+ * Use $conds == "*" to delete all rows
 1672+ */
 1673+ function delete( $table, $conds, $fname = 'Database::delete' ) {
 1674+ if ( !$conds ) {
 1675+ throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
 1676+ }
 1677+ $table = $this->tableName( $table );
 1678+ $sql = "DELETE FROM $table";
 1679+ if ( $conds != '*' ) {
 1680+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
 1681+ }
 1682+ return $this->query( $sql, $fname );
 1683+ }
 1684+
 1685+ /**
 1686+ * INSERT SELECT wrapper
 1687+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
 1688+ * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
 1689+ * $conds may be "*" to copy the whole table
 1690+ * srcTable may be an array of tables.
 1691+ */
 1692+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
 1693+ $insertOptions = array(), $selectOptions = array() )
 1694+ {
 1695+ $destTable = $this->tableName( $destTable );
 1696+ if ( is_array( $insertOptions ) ) {
 1697+ $insertOptions = implode( ' ', $insertOptions );
 1698+ }
 1699+ if( !is_array( $selectOptions ) ) {
 1700+ $selectOptions = array( $selectOptions );
 1701+ }
 1702+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
 1703+ if( is_array( $srcTable ) ) {
 1704+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
 1705+ } else {
 1706+ $srcTable = $this->tableName( $srcTable );
 1707+ }
 1708+ $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
 1709+ " SELECT $startOpts " . implode( ',', $varMap ) .
 1710+ " FROM $srcTable $useIndex ";
 1711+ if ( $conds != '*' ) {
 1712+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
 1713+ }
 1714+ $sql .= " $tailOpts";
 1715+ return $this->query( $sql, $fname );
 1716+ }
 1717+
 1718+ /**
 1719+ * Construct a LIMIT query with optional offset. This is used for query
 1720+ * pages. The SQL should be adjusted so that only the first $limit rows
 1721+ * are returned. If $offset is provided as well, then the first $offset
 1722+ * rows should be discarded, and the next $limit rows should be returned.
 1723+ * If the result of the query is not ordered, then the rows to be returned
 1724+ * are theoretically arbitrary.
 1725+ *
 1726+ * $sql is expected to be a SELECT, if that makes a difference. For
 1727+ * UPDATE, limitResultForUpdate should be used.
 1728+ *
 1729+ * The version provided by default works in MySQL and SQLite. It will very
 1730+ * likely need to be overridden for most other DBMSes.
 1731+ *
 1732+ * @param $sql String: SQL query we will append the limit too
 1733+ * @param $limit Integer: the SQL limit
 1734+ * @param $offset Integer the SQL offset (default false)
 1735+ */
 1736+ function limitResult( $sql, $limit, $offset=false ) {
 1737+ if( !is_numeric( $limit ) ) {
 1738+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
 1739+ }
 1740+ return "$sql LIMIT "
 1741+ . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
 1742+ . "{$limit} ";
 1743+ }
 1744+ function limitResultForUpdate( $sql, $num ) {
 1745+ return $this->limitResult( $sql, $num, 0 );
 1746+ }
 1747+
 1748+ /**
 1749+ * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries
 1750+ * within the UNION construct.
 1751+ * @return Boolean
 1752+ */
 1753+ function unionSupportsOrderAndLimit() {
 1754+ return true; // True for almost every DB supported
 1755+ }
 1756+
 1757+ /**
 1758+ * Construct a UNION query
 1759+ * This is used for providing overload point for other DB abstractions
 1760+ * not compatible with the MySQL syntax.
 1761+ * @param $sqls Array: SQL statements to combine
 1762+ * @param $all Boolean: use UNION ALL
 1763+ * @return String: SQL fragment
 1764+ */
 1765+ function unionQueries($sqls, $all) {
 1766+ $glue = $all ? ') UNION ALL (' : ') UNION (';
 1767+ return '('.implode( $glue, $sqls ) . ')';
 1768+ }
 1769+
 1770+ /**
 1771+ * Returns an SQL expression for a simple conditional. This doesn't need
 1772+ * to be overridden unless CASE isn't supported in your DBMS.
 1773+ *
 1774+ * @param $cond String: SQL expression which will result in a boolean value
 1775+ * @param $trueVal String: SQL expression to return if true
 1776+ * @param $falseVal String: SQL expression to return if false
 1777+ * @return String: SQL fragment
 1778+ */
 1779+ function conditional( $cond, $trueVal, $falseVal ) {
 1780+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
 1781+ }
 1782+
 1783+ /**
 1784+ * Returns a comand for str_replace function in SQL query.
 1785+ * Uses REPLACE() in MySQL
 1786+ *
 1787+ * @param $orig String: column to modify
 1788+ * @param $old String: column to seek
 1789+ * @param $new String: column to replace with
 1790+ */
 1791+ function strreplace( $orig, $old, $new ) {
 1792+ return "REPLACE({$orig}, {$old}, {$new})";
 1793+ }
 1794+
 1795+ /**
 1796+ * Determines if the last failure was due to a deadlock
 1797+ * STUB
 1798+ */
 1799+ function wasDeadlock() {
 1800+ return false;
 1801+ }
 1802+
 1803+ /**
 1804+ * Determines if the last query error was something that should be dealt
 1805+ * with by pinging the connection and reissuing the query.
 1806+ * STUB
 1807+ */
 1808+ function wasErrorReissuable() {
 1809+ return false;
 1810+ }
 1811+
 1812+ /**
 1813+ * Determines if the last failure was due to the database being read-only.
 1814+ * STUB
 1815+ */
 1816+ function wasReadOnlyError() {
 1817+ return false;
 1818+ }
 1819+
 1820+ /**
 1821+ * Perform a deadlock-prone transaction.
 1822+ *
 1823+ * This function invokes a callback function to perform a set of write
 1824+ * queries. If a deadlock occurs during the processing, the transaction
 1825+ * will be rolled back and the callback function will be called again.
 1826+ *
 1827+ * Usage:
 1828+ * $dbw->deadlockLoop( callback, ... );
 1829+ *
 1830+ * Extra arguments are passed through to the specified callback function.
 1831+ *
 1832+ * Returns whatever the callback function returned on its successful,
 1833+ * iteration, or false on error, for example if the retry limit was
 1834+ * reached.
 1835+ */
 1836+ function deadlockLoop() {
 1837+ $myFname = 'Database::deadlockLoop';
 1838+
 1839+ $this->begin();
 1840+ $args = func_get_args();
 1841+ $function = array_shift( $args );
 1842+ $oldIgnore = $this->ignoreErrors( true );
 1843+ $tries = DEADLOCK_TRIES;
 1844+ if ( is_array( $function ) ) {
 1845+ $fname = $function[0];
 1846+ } else {
 1847+ $fname = $function;
 1848+ }
 1849+ do {
 1850+ $retVal = call_user_func_array( $function, $args );
 1851+ $error = $this->lastError();
 1852+ $errno = $this->lastErrno();
 1853+ $sql = $this->lastQuery();
 1854+
 1855+ if ( $errno ) {
 1856+ if ( $this->wasDeadlock() ) {
 1857+ # Retry
 1858+ usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
 1859+ } else {
 1860+ $this->reportQueryError( $error, $errno, $sql, $fname );
 1861+ }
 1862+ }
 1863+ } while( $this->wasDeadlock() && --$tries > 0 );
 1864+ $this->ignoreErrors( $oldIgnore );
 1865+ if ( $tries <= 0 ) {
 1866+ $this->query( 'ROLLBACK', $myFname );
 1867+ $this->reportQueryError( $error, $errno, $sql, $fname );
 1868+ return false;
 1869+ } else {
 1870+ $this->query( 'COMMIT', $myFname );
 1871+ return $retVal;
 1872+ }
 1873+ }
 1874+
 1875+ /**
 1876+ * Do a SELECT MASTER_POS_WAIT()
 1877+ *
 1878+ * @param $pos MySQLMasterPos object
 1879+ * @param $timeout Integer: the maximum number of seconds to wait for synchronisation
 1880+ */
 1881+ function masterPosWait( MySQLMasterPos $pos, $timeout ) {
 1882+ $fname = 'Database::masterPosWait';
 1883+ wfProfileIn( $fname );
 1884+
 1885+ # Commit any open transactions
 1886+ if ( $this->mTrxLevel ) {
 1887+ $this->commit();
 1888+ }
 1889+
 1890+ if ( !is_null( $this->mFakeSlaveLag ) ) {
 1891+ $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
 1892+ if ( $wait > $timeout * 1e6 ) {
 1893+ wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
 1894+ wfProfileOut( $fname );
 1895+ return -1;
 1896+ } elseif ( $wait > 0 ) {
 1897+ wfDebug( "Fake slave waiting $wait us\n" );
 1898+ usleep( $wait );
 1899+ wfProfileOut( $fname );
 1900+ return 1;
 1901+ } else {
 1902+ wfDebug( "Fake slave up to date ($wait us)\n" );
 1903+ wfProfileOut( $fname );
 1904+ return 0;
 1905+ }
 1906+ }
 1907+
 1908+ # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
 1909+ $encFile = $this->addQuotes( $pos->file );
 1910+ $encPos = intval( $pos->pos );
 1911+ $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
 1912+ $res = $this->doQuery( $sql );
 1913+ if ( $res && $row = $this->fetchRow( $res ) ) {
 1914+ $this->freeResult( $res );
 1915+ wfProfileOut( $fname );
 1916+ return $row[0];
 1917+ } else {
 1918+ wfProfileOut( $fname );
 1919+ return false;
 1920+ }
 1921+ }
 1922+
 1923+ /**
 1924+ * Get the position of the master from SHOW SLAVE STATUS
 1925+ */
 1926+ function getSlavePos() {
 1927+ if ( !is_null( $this->mFakeSlaveLag ) ) {
 1928+ $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
 1929+ wfDebug( __METHOD__.": fake slave pos = $pos\n" );
 1930+ return $pos;
 1931+ }
 1932+ $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
 1933+ $row = $this->fetchObject( $res );
 1934+ if ( $row ) {
 1935+ $pos = isset($row->Exec_master_log_pos) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos;
 1936+ return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
 1937+ } else {
 1938+ return false;
 1939+ }
 1940+ }
 1941+
 1942+ /**
 1943+ * Get the position of the master from SHOW MASTER STATUS
 1944+ */
 1945+ function getMasterPos() {
 1946+ if ( $this->mFakeMaster ) {
 1947+ return new MySQLMasterPos( 'fake', microtime( true ) );
 1948+ }
 1949+ $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
 1950+ $row = $this->fetchObject( $res );
 1951+ if ( $row ) {
 1952+ return new MySQLMasterPos( $row->File, $row->Position );
 1953+ } else {
 1954+ return false;
 1955+ }
 1956+ }
 1957+
 1958+ /**
 1959+ * Begin a transaction, committing any previously open transaction
 1960+ */
 1961+ function begin( $fname = 'Database::begin' ) {
 1962+ $this->query( 'BEGIN', $fname );
 1963+ $this->mTrxLevel = 1;
 1964+ }
 1965+
 1966+ /**
 1967+ * End a transaction
 1968+ */
 1969+ function commit( $fname = 'Database::commit' ) {
 1970+ $this->query( 'COMMIT', $fname );
 1971+ $this->mTrxLevel = 0;
 1972+ }
 1973+
 1974+ /**
 1975+ * Rollback a transaction.
 1976+ * No-op on non-transactional databases.
 1977+ */
 1978+ function rollback( $fname = 'Database::rollback' ) {
 1979+ $this->query( 'ROLLBACK', $fname, true );
 1980+ $this->mTrxLevel = 0;
 1981+ }
 1982+
 1983+ /**
 1984+ * Begin a transaction, committing any previously open transaction
 1985+ * @deprecated use begin()
 1986+ */
 1987+ function immediateBegin( $fname = 'Database::immediateBegin' ) {
 1988+ $this->begin();
 1989+ }
 1990+
 1991+ /**
 1992+ * Commit transaction, if one is open
 1993+ * @deprecated use commit()
 1994+ */
 1995+ function immediateCommit( $fname = 'Database::immediateCommit' ) {
 1996+ $this->commit();
 1997+ }
 1998+
 1999+ /**
 2000+ * Creates a new table with structure copied from existing table
 2001+ * Note that unlike most database abstraction functions, this function does not
 2002+ * automatically append database prefix, because it works at a lower
 2003+ * abstraction level.
 2004+ *
 2005+ * @param $oldName String: name of table whose structure should be copied
 2006+ * @param $newName String: name of table to be created
 2007+ * @param $temporary Boolean: whether the new table should be temporary
 2008+ * @return Boolean: true if operation was successful
 2009+ */
 2010+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'Database::duplicateTableStructure' ) {
 2011+ throw new MWException( 'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
 2012+ }
 2013+
 2014+ /**
 2015+ * Return MW-style timestamp used for MySQL schema
 2016+ */
 2017+ function timestamp( $ts=0 ) {
 2018+ return wfTimestamp(TS_MW,$ts);
 2019+ }
 2020+
 2021+ /**
 2022+ * Local database timestamp format or null
 2023+ */
 2024+ function timestampOrNull( $ts = null ) {
 2025+ if( is_null( $ts ) ) {
 2026+ return null;
 2027+ } else {
 2028+ return $this->timestamp( $ts );
 2029+ }
 2030+ }
 2031+
 2032+ /**
 2033+ * @todo document
 2034+ */
 2035+ function resultObject( $result ) {
 2036+ if( empty( $result ) ) {
 2037+ return false;
 2038+ } elseif ( $result instanceof ResultWrapper ) {
 2039+ return $result;
 2040+ } elseif ( $result === true ) {
 2041+ // Successful write query
 2042+ return $result;
 2043+ } else {
 2044+ return new ResultWrapper( $this, $result );
 2045+ }
 2046+ }
 2047+
 2048+ /**
 2049+ * Return aggregated value alias
 2050+ */
 2051+ function aggregateValue ($valuedata,$valuename='value') {
 2052+ return $valuename;
 2053+ }
 2054+
 2055+ /**
 2056+ * Returns a wikitext link to the DB's website, e.g.,
 2057+ * return "[http://www.mysql.com/ MySQL]";
 2058+ * Should at least contain plain text, if for some reason
 2059+ * your database has no website.
 2060+ *
 2061+ * @return String: wikitext of a link to the server software's web site
 2062+ */
 2063+ abstract function getSoftwareLink();
 2064+
 2065+ /**
 2066+ * A string describing the current software version, like from
 2067+ * mysql_get_server_info(). Will be listed on Special:Version, etc.
 2068+ *
 2069+ * @return String: Version information from the database
 2070+ */
 2071+ abstract function getServerVersion();
 2072+
 2073+ /**
 2074+ * Ping the server and try to reconnect if it there is no connection
 2075+ *
 2076+ * @return bool Success or failure
 2077+ */
 2078+ function ping() {
 2079+ # Stub. Not essential to override.
 2080+ return true;
 2081+ }
 2082+
 2083+ /**
 2084+ * Get slave lag.
 2085+ * At the moment, this will only work if the DB user has the PROCESS privilege
 2086+ */
 2087+ function getLag() {
 2088+ if ( !is_null( $this->mFakeSlaveLag ) ) {
 2089+ wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
 2090+ return $this->mFakeSlaveLag;
 2091+ }
 2092+ $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ );
 2093+ # Find slave SQL thread
 2094+ while ( $row = $this->fetchObject( $res ) ) {
 2095+ /* This should work for most situations - when default db
 2096+ * for thread is not specified, it had no events executed,
 2097+ * and therefore it doesn't know yet how lagged it is.
 2098+ *
 2099+ * Relay log I/O thread does not select databases.
 2100+ */
 2101+ if ( $row->User == 'system user' &&
 2102+ $row->State != 'Waiting for master to send event' &&
 2103+ $row->State != 'Connecting to master' &&
 2104+ $row->State != 'Queueing master event to the relay log' &&
 2105+ $row->State != 'Waiting for master update' &&
 2106+ $row->State != 'Requesting binlog dump' &&
 2107+ $row->State != 'Waiting to reconnect after a failed master event read' &&
 2108+ $row->State != 'Reconnecting after a failed master event read' &&
 2109+ $row->State != 'Registering slave on master'
 2110+ ) {
 2111+ # This is it, return the time (except -ve)
 2112+ if ( $row->Time > 0x7fffffff ) {
 2113+ return false;
 2114+ } else {
 2115+ return $row->Time;
 2116+ }
 2117+ }
 2118+ }
 2119+ return false;
 2120+ }
 2121+
 2122+ /**
 2123+ * Get status information from SHOW STATUS in an associative array
 2124+ */
 2125+ function getStatus($which="%") {
 2126+ $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
 2127+ $status = array();
 2128+ while ( $row = $this->fetchObject( $res ) ) {
 2129+ $status[$row->Variable_name] = $row->Value;
 2130+ }
 2131+ return $status;
 2132+ }
 2133+
 2134+ /**
 2135+ * Return the maximum number of items allowed in a list, or 0 for unlimited.
 2136+ */
 2137+ function maxListLen() {
 2138+ return 0;
 2139+ }
 2140+
 2141+ function encodeBlob($b) {
 2142+ return $b;
 2143+ }
 2144+
 2145+ function decodeBlob($b) {
 2146+ return $b;
 2147+ }
 2148+
 2149+ /**
 2150+ * Override database's default connection timeout. May be useful for very
 2151+ * long batch queries such as full-wiki dumps, where a single query reads
 2152+ * out over hours or days. May or may not be necessary for non-MySQL
 2153+ * databases. For most purposes, leaving it as a no-op should be fine.
 2154+ *
 2155+ * @param $timeout Integer in seconds
 2156+ */
 2157+ public function setTimeout( $timeout ) {}
 2158+
 2159+ /**
 2160+ * Read and execute SQL commands from a file.
 2161+ * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
 2162+ * @param $filename String: File name to open
 2163+ * @param $lineCallback Callback: Optional function called before reading each line
 2164+ * @param $resultCallback Callback: Optional function called for each MySQL result
 2165+ */
 2166+ function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
 2167+ $fp = fopen( $filename, 'r' );
 2168+ if ( false === $fp ) {
 2169+ throw new MWException( "Could not open \"{$filename}\".\n" );
 2170+ }
 2171+ $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
 2172+ fclose( $fp );
 2173+ return $error;
 2174+ }
 2175+
 2176+ /**
 2177+ * Get the full path of a patch file. Originally based on archive()
 2178+ * from updaters.inc. Keep in mind this always returns a patch, as
 2179+ * it fails back to MySQL if no DB-specific patch can be found
 2180+ *
 2181+ * @param $patch String The name of the patch, like patch-something.sql
 2182+ * @return String Full path to patch file
 2183+ */
 2184+ public static function patchPath( $patch ) {
 2185+ global $wgDBtype, $IP;
 2186+ if ( file_exists( "$IP/maintenance/$wgDBtype/archives/$name" ) ) {
 2187+ return "$IP/maintenance/$wgDBtype/archives/$name";
 2188+ } else {
 2189+ return "$IP/maintenance/archives/$name";
 2190+ }
 2191+ }
 2192+
 2193+ /**
 2194+ * Read and execute commands from an open file handle
 2195+ * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
 2196+ * @param $fp String: File handle
 2197+ * @param $lineCallback Callback: Optional function called before reading each line
 2198+ * @param $resultCallback Callback: Optional function called for each MySQL result
 2199+ */
 2200+ function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
 2201+ $cmd = "";
 2202+ $done = false;
 2203+ $dollarquote = false;
 2204+
 2205+ while ( ! feof( $fp ) ) {
 2206+ if ( $lineCallback ) {
 2207+ call_user_func( $lineCallback );
 2208+ }
 2209+ $line = trim( fgets( $fp, 1024 ) );
 2210+ $sl = strlen( $line ) - 1;
 2211+
 2212+ if ( $sl < 0 ) { continue; }
 2213+ if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
 2214+
 2215+ ## Allow dollar quoting for function declarations
 2216+ if (substr($line,0,4) == '$mw$') {
 2217+ if ($dollarquote) {
 2218+ $dollarquote = false;
 2219+ $done = true;
 2220+ }
 2221+ else {
 2222+ $dollarquote = true;
 2223+ }
 2224+ }
 2225+ else if (!$dollarquote) {
 2226+ if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
 2227+ $done = true;
 2228+ $line = substr( $line, 0, $sl );
 2229+ }
 2230+ }
 2231+
 2232+ if ( $cmd != '' ) { $cmd .= ' '; }
 2233+ $cmd .= "$line\n";
 2234+
 2235+ if ( $done ) {
 2236+ $cmd = str_replace(';;', ";", $cmd);
 2237+ $cmd = $this->replaceVars( $cmd );
 2238+ $res = $this->query( $cmd, __METHOD__ );
 2239+ if ( $resultCallback ) {
 2240+ call_user_func( $resultCallback, $res, $this );
 2241+ }
 2242+
 2243+ if ( false === $res ) {
 2244+ $err = $this->lastError();
 2245+ return "Query \"{$cmd}\" failed with error code \"$err\".\n";
 2246+ }
 2247+
 2248+ $cmd = '';
 2249+ $done = false;
 2250+ }
 2251+ }
 2252+ return true;
 2253+ }
 2254+
 2255+
 2256+ /**
 2257+ * Replace variables in sourced SQL
 2258+ */
 2259+ protected function replaceVars( $ins ) {
 2260+ $varnames = array(
 2261+ 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
 2262+ 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
 2263+ 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
 2264+ );
 2265+
 2266+ // Ordinary variables
 2267+ foreach ( $varnames as $var ) {
 2268+ if( isset( $GLOBALS[$var] ) ) {
 2269+ $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
 2270+ $ins = str_replace( '{$' . $var . '}', $val, $ins );
 2271+ $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
 2272+ $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
 2273+ }
 2274+ }
 2275+
 2276+ // Table prefixes
 2277+ $ins = preg_replace_callback( '!/\*(?:\$wgDBprefix|_)\*/([a-zA-Z_0-9]*)!',
 2278+ array( $this, 'tableNameCallback' ), $ins );
 2279+
 2280+ // Index names
 2281+ $ins = preg_replace_callback( '!/\*i\*/([a-zA-Z_0-9]*)!',
 2282+ array( $this, 'indexNameCallback' ), $ins );
 2283+ return $ins;
 2284+ }
 2285+
 2286+ /**
 2287+ * Table name callback
 2288+ * @private
 2289+ */
 2290+ protected function tableNameCallback( $matches ) {
 2291+ return $this->tableName( $matches[1] );
 2292+ }
 2293+
 2294+ /**
 2295+ * Index name callback
 2296+ */
 2297+ protected function indexNameCallback( $matches ) {
 2298+ return $this->indexName( $matches[1] );
 2299+ }
 2300+
 2301+ /**
 2302+ * Build a concatenation list to feed into a SQL query
 2303+ * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting
 2304+ * @return String
 2305+ */
 2306+ function buildConcat( $stringList ) {
 2307+ return 'CONCAT(' . implode( ',', $stringList ) . ')';
 2308+ }
 2309+
 2310+ /**
 2311+ * Acquire a named lock
 2312+ *
 2313+ * Abstracted from Filestore::lock() so child classes can implement for
 2314+ * their own needs.
 2315+ *
 2316+ * @param $lockName String: Name of lock to aquire
 2317+ * @param $method String: Name of method calling us
 2318+ * @return bool
 2319+ */
 2320+ public function lock( $lockName, $method, $timeout = 5 ) {
 2321+ return true;
 2322+ }
 2323+
 2324+ /**
 2325+ * Release a lock.
 2326+ *
 2327+ * @param $lockName String: Name of lock to release
 2328+ * @param $method String: Name of method calling us
 2329+ *
 2330+ * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
 2331+ * @return Returns 1 if the lock was released, 0 if the lock was not established
 2332+ * by this thread (in which case the lock is not released), and NULL if the named
 2333+ * lock did not exist
 2334+ */
 2335+ public function unlock( $lockName, $method ) {
 2336+ return true;
 2337+ }
 2338+
 2339+ /**
 2340+ * Lock specific tables
 2341+ *
 2342+ * @param $read Array of tables to lock for read access
 2343+ * @param $write Array of tables to lock for write access
 2344+ * @param $method String name of caller
 2345+ * @param $lowPriority bool Whether to indicate writes to be LOW PRIORITY
 2346+ */
 2347+ public function lockTables( $read, $write, $method, $lowPriority = true ) {
 2348+ return true;
 2349+ }
 2350+
 2351+ /**
 2352+ * Unlock specific tables
 2353+ *
 2354+ * @param $method String the caller
 2355+ */
 2356+ public function unlockTables( $method ) {
 2357+ return true;
 2358+ }
 2359+
 2360+ /**
 2361+ * Get search engine class. All subclasses of this
 2362+ * need to implement this if they wish to use searching.
 2363+ *
 2364+ * @return String
 2365+ */
 2366+ public function getSearchEngine() {
 2367+ return "SearchMySQL";
 2368+ }
 2369+
 2370+ /**
 2371+ * Allow or deny "big selects" for this session only. This is done by setting
 2372+ * the sql_big_selects session variable.
 2373+ *
 2374+ * This is a MySQL-specific feature.
 2375+ *
 2376+ * @param mixed $value true for allow, false for deny, or "default" to restore the initial value
 2377+ */
 2378+ public function setBigSelects( $value = true ) {
 2379+ // no-op
 2380+ }
 2381+}
 2382+
 2383+
 2384+/******************************************************************************
 2385+ * Utility classes
 2386+ *****************************************************************************/
 2387+
 2388+/**
 2389+ * Utility class.
 2390+ * @ingroup Database
 2391+ */
 2392+class DBObject {
 2393+ public $mData;
 2394+
 2395+ function DBObject($data) {
 2396+ $this->mData = $data;
 2397+ }
 2398+
 2399+ function isLOB() {
 2400+ return false;
 2401+ }
 2402+
 2403+ function data() {
 2404+ return $this->mData;
 2405+ }
 2406+}
 2407+
 2408+/**
 2409+ * Utility class
 2410+ * @ingroup Database
 2411+ *
 2412+ * This allows us to distinguish a blob from a normal string and an array of strings
 2413+ */
 2414+class Blob {
 2415+ private $mData;
 2416+ function __construct($data) {
 2417+ $this->mData = $data;
 2418+ }
 2419+ function fetch() {
 2420+ return $this->mData;
 2421+ }
 2422+}
 2423+
 2424+/**
 2425+ * Utility class.
 2426+ * @ingroup Database
 2427+ */
 2428+class MySQLField {
 2429+ private $name, $tablename, $default, $max_length, $nullable,
 2430+ $is_pk, $is_unique, $is_multiple, $is_key, $type;
 2431+ function __construct ($info) {
 2432+ $this->name = $info->name;
 2433+ $this->tablename = $info->table;
 2434+ $this->default = $info->def;
 2435+ $this->max_length = $info->max_length;
 2436+ $this->nullable = !$info->not_null;
 2437+ $this->is_pk = $info->primary_key;
 2438+ $this->is_unique = $info->unique_key;
 2439+ $this->is_multiple = $info->multiple_key;
 2440+ $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
 2441+ $this->type = $info->type;
 2442+ }
 2443+
 2444+ function name() {
 2445+ return $this->name;
 2446+ }
 2447+
 2448+ function tableName() {
 2449+ return $this->tableName;
 2450+ }
 2451+
 2452+ function defaultValue() {
 2453+ return $this->default;
 2454+ }
 2455+
 2456+ function maxLength() {
 2457+ return $this->max_length;
 2458+ }
 2459+
 2460+ function nullable() {
 2461+ return $this->nullable;
 2462+ }
 2463+
 2464+ function isKey() {
 2465+ return $this->is_key;
 2466+ }
 2467+
 2468+ function isMultipleKey() {
 2469+ return $this->is_multiple;
 2470+ }
 2471+
 2472+ function type() {
 2473+ return $this->type;
 2474+ }
 2475+}
 2476+
 2477+/******************************************************************************
 2478+ * Error classes
 2479+ *****************************************************************************/
 2480+
 2481+/**
 2482+ * Database error base class
 2483+ * @ingroup Database
 2484+ */
 2485+class DBError extends MWException {
 2486+ public $db;
 2487+
 2488+ /**
 2489+ * Construct a database error
 2490+ * @param $db Database object which threw the error
 2491+ * @param $error A simple error message to be used for debugging
 2492+ */
 2493+ function __construct( DatabaseBase &$db, $error ) {
 2494+ $this->db =& $db;
 2495+ parent::__construct( $error );
 2496+ }
 2497+
 2498+ function getText() {
 2499+ global $wgShowDBErrorBacktrace;
 2500+ $s = $this->getMessage() . "\n";
 2501+ if ( $wgShowDBErrorBacktrace ) {
 2502+ $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
 2503+ }
 2504+ return $s;
 2505+ }
 2506+}
 2507+
 2508+/**
 2509+ * @ingroup Database
 2510+ */
 2511+class DBConnectionError extends DBError {
 2512+ public $error;
 2513+
 2514+ function __construct( DatabaseBase &$db, $error = 'unknown error' ) {
 2515+ $msg = 'DB connection error';
 2516+ if ( trim( $error ) != '' ) {
 2517+ $msg .= ": $error";
 2518+ }
 2519+ $this->error = $error;
 2520+ parent::__construct( $db, $msg );
 2521+ }
 2522+
 2523+ function useOutputPage() {
 2524+ // Not likely to work
 2525+ return false;
 2526+ }
 2527+
 2528+ function useMessageCache() {
 2529+ // Not likely to work
 2530+ return false;
 2531+ }
 2532+
 2533+ function getLogMessage() {
 2534+ # Don't send to the exception log
 2535+ return false;
 2536+ }
 2537+
 2538+ function getPageTitle() {
 2539+ global $wgSitename, $wgLang;
 2540+ $header = "$wgSitename has a problem";
 2541+ if ( $wgLang instanceof Language ) {
 2542+ $header = htmlspecialchars( $wgLang->getMessage( 'dberr-header' ) );
 2543+ }
 2544+
 2545+ return $header;
 2546+ }
 2547+
 2548+ function getHTML() {
 2549+ global $wgLang, $wgMessageCache, $wgUseFileCache, $wgShowDBErrorBacktrace;
 2550+
 2551+ $sorry = 'Sorry! This site is experiencing technical difficulties.';
 2552+ $again = 'Try waiting a few minutes and reloading.';
 2553+ $info = '(Can\'t contact the database server: $1)';
 2554+
 2555+ if ( $wgLang instanceof Language ) {
 2556+ $sorry = htmlspecialchars( $wgLang->getMessage( 'dberr-problems' ) );
 2557+ $again = htmlspecialchars( $wgLang->getMessage( 'dberr-again' ) );
 2558+ $info = htmlspecialchars( $wgLang->getMessage( 'dberr-info' ) );
 2559+ }
 2560+
 2561+ # No database access
 2562+ if ( is_object( $wgMessageCache ) ) {
 2563+ $wgMessageCache->disable();
 2564+ }
 2565+
 2566+ if ( trim( $this->error ) == '' ) {
 2567+ $this->error = $this->db->getProperty('mServer');
 2568+ }
 2569+
 2570+ $noconnect = "<p><strong>$sorry</strong><br />$again</p><p><small>$info</small></p>";
 2571+ $text = str_replace( '$1', $this->error, $noconnect );
 2572+
 2573+ if ( $wgShowDBErrorBacktrace ) {
 2574+ $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
 2575+ }
 2576+
 2577+ $extra = $this->searchForm();
 2578+
 2579+ if( $wgUseFileCache ) {
 2580+ try {
 2581+ $cache = $this->fileCachedPage();
 2582+ # Cached version on file system?
 2583+ if( $cache !== null ) {
 2584+ # Hack: extend the body for error messages
 2585+ $cache = str_replace( array('</html>','</body>'), '', $cache );
 2586+ # Add cache notice...
 2587+ $cachederror = "This is a cached copy of the requested page, and may not be up to date. ";
 2588+ # Localize it if possible...
 2589+ if( $wgLang instanceof Language ) {
 2590+ $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) );
 2591+ }
 2592+ $warning = "<div style='color:red;font-size:150%;font-weight:bold;'>$cachederror</div>";
 2593+ # Output cached page with notices on bottom and re-close body
 2594+ return "{$cache}{$warning}<hr />$text<hr />$extra</body></html>";
 2595+ }
 2596+ } catch( MWException $e ) {
 2597+ // Do nothing, just use the default page
 2598+ }
 2599+ }
 2600+ # Headers needed here - output is just the error message
 2601+ return $this->htmlHeader()."$text<hr />$extra".$this->htmlFooter();
 2602+ }
 2603+
 2604+ function searchForm() {
 2605+ global $wgSitename, $wgServer, $wgLang, $wgInputEncoding;
 2606+ $usegoogle = "You can try searching via Google in the meantime.";
 2607+ $outofdate = "Note that their indexes of our content may be out of date.";
 2608+ $googlesearch = "Search";
 2609+
 2610+ if ( $wgLang instanceof Language ) {
 2611+ $usegoogle = htmlspecialchars( $wgLang->getMessage( 'dberr-usegoogle' ) );
 2612+ $outofdate = htmlspecialchars( $wgLang->getMessage( 'dberr-outofdate' ) );
 2613+ $googlesearch = htmlspecialchars( $wgLang->getMessage( 'searchbutton' ) );
 2614+ }
 2615+
 2616+ $search = htmlspecialchars(@$_REQUEST['search']);
 2617+
 2618+ $trygoogle = <<<EOT
 2619+<div style="margin: 1.5em">$usegoogle<br />
 2620+<small>$outofdate</small></div>
 2621+<!-- SiteSearch Google -->
 2622+<form method="get" action="http://www.google.com/search" id="googlesearch">
 2623+ <input type="hidden" name="domains" value="$wgServer" />
 2624+ <input type="hidden" name="num" value="50" />
 2625+ <input type="hidden" name="ie" value="$wgInputEncoding" />
 2626+ <input type="hidden" name="oe" value="$wgInputEncoding" />
 2627+
 2628+ <img src="http://www.google.com/logos/Logo_40wht.gif" alt="" style="float:left; margin-left: 1.5em; margin-right: 1.5em;" />
 2629+
 2630+ <input type="text" name="q" size="31" maxlength="255" value="$search" />
 2631+ <input type="submit" name="btnG" value="$googlesearch" />
 2632+ <div>
 2633+ <input type="radio" name="sitesearch" id="gwiki" value="$wgServer" checked="checked" /><label for="gwiki">$wgSitename</label>
 2634+ <input type="radio" name="sitesearch" id="gWWW" value="" /><label for="gWWW">WWW</label>
 2635+ </div>
 2636+</form>
 2637+<!-- SiteSearch Google -->
 2638+EOT;
 2639+ return $trygoogle;
 2640+ }
 2641+
 2642+ function fileCachedPage() {
 2643+ global $wgTitle, $title, $wgLang, $wgOut;
 2644+ if( $wgOut->isDisabled() ) return; // Done already?
 2645+ $mainpage = 'Main Page';
 2646+ if ( $wgLang instanceof Language ) {
 2647+ $mainpage = htmlspecialchars( $wgLang->getMessage( 'mainpage' ) );
 2648+ }
 2649+
 2650+ if( $wgTitle ) {
 2651+ $t =& $wgTitle;
 2652+ } elseif( $title ) {
 2653+ $t = Title::newFromURL( $title );
 2654+ } else {
 2655+ $t = Title::newFromText( $mainpage );
 2656+ }
 2657+
 2658+ $cache = new HTMLFileCache( $t );
 2659+ if( $cache->isFileCached() ) {
 2660+ return $cache->fetchPageText();
 2661+ } else {
 2662+ return '';
 2663+ }
 2664+ }
 2665+
 2666+ function htmlBodyOnly() {
 2667+ return true;
 2668+ }
 2669+
 2670+}
 2671+
 2672+/**
 2673+ * @ingroup Database
 2674+ */
 2675+class DBQueryError extends DBError {
 2676+ public $error, $errno, $sql, $fname;
 2677+
 2678+ function __construct( DatabaseBase &$db, $error, $errno, $sql, $fname ) {
 2679+ $message = "A database error has occurred\n" .
 2680+ "Query: $sql\n" .
 2681+ "Function: $fname\n" .
 2682+ "Error: $errno $error\n";
 2683+
 2684+ parent::__construct( $db, $message );
 2685+ $this->error = $error;
 2686+ $this->errno = $errno;
 2687+ $this->sql = $sql;
 2688+ $this->fname = $fname;
 2689+ }
 2690+
 2691+ function getText() {
 2692+ global $wgShowDBErrorBacktrace;
 2693+ if ( $this->useMessageCache() ) {
 2694+ $s = wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
 2695+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
 2696+ if ( $wgShowDBErrorBacktrace ) {
 2697+ $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
 2698+ }
 2699+ return $s;
 2700+ } else {
 2701+ return parent::getText();
 2702+ }
 2703+ }
 2704+
 2705+ function getSQL() {
 2706+ global $wgShowSQLErrors;
 2707+ if( !$wgShowSQLErrors ) {
 2708+ return $this->msg( 'sqlhidden', 'SQL hidden' );
 2709+ } else {
 2710+ return $this->sql;
 2711+ }
 2712+ }
 2713+
 2714+ function getLogMessage() {
 2715+ # Don't send to the exception log
 2716+ return false;
 2717+ }
 2718+
 2719+ function getPageTitle() {
 2720+ return $this->msg( 'databaseerror', 'Database error' );
 2721+ }
 2722+
 2723+ function getHTML() {
 2724+ global $wgShowDBErrorBacktrace;
 2725+ if ( $this->useMessageCache() ) {
 2726+ $s = wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
 2727+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
 2728+ } else {
 2729+ $s = nl2br( htmlspecialchars( $this->getMessage() ) );
 2730+ }
 2731+ if ( $wgShowDBErrorBacktrace ) {
 2732+ $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
 2733+ }
 2734+ return $s;
 2735+ }
 2736+}
 2737+
 2738+/**
 2739+ * @ingroup Database
 2740+ */
 2741+class DBUnexpectedError extends DBError {}
 2742+
 2743+
 2744+/**
 2745+ * Result wrapper for grabbing data queried by someone else
 2746+ * @ingroup Database
 2747+ */
 2748+class ResultWrapper implements Iterator {
 2749+ var $db, $result, $pos = 0, $currentRow = null;
 2750+
 2751+ /**
 2752+ * Create a new result object from a result resource and a Database object
 2753+ */
 2754+ function ResultWrapper( $database, $result ) {
 2755+ $this->db = $database;
 2756+ if ( $result instanceof ResultWrapper ) {
 2757+ $this->result = $result->result;
 2758+ } else {
 2759+ $this->result = $result;
 2760+ }
 2761+ }
 2762+
 2763+ /**
 2764+ * Get the number of rows in a result object
 2765+ */
 2766+ function numRows() {
 2767+ return $this->db->numRows( $this );
 2768+ }
 2769+
 2770+ /**
 2771+ * Fetch the next row from the given result object, in object form.
 2772+ * Fields can be retrieved with $row->fieldname, with fields acting like
 2773+ * member variables.
 2774+ *
 2775+ * @param $res SQL result object as returned from Database::query(), etc.
 2776+ * @return MySQL row object
 2777+ * @throws DBUnexpectedError Thrown if the database returns an error
 2778+ */
 2779+ function fetchObject() {
 2780+ return $this->db->fetchObject( $this );
 2781+ }
 2782+
 2783+ /**
 2784+ * Fetch the next row from the given result object, in associative array
 2785+ * form. Fields are retrieved with $row['fieldname'].
 2786+ *
 2787+ * @param $res SQL result object as returned from Database::query(), etc.
 2788+ * @return MySQL row object
 2789+ * @throws DBUnexpectedError Thrown if the database returns an error
 2790+ */
 2791+ function fetchRow() {
 2792+ return $this->db->fetchRow( $this );
 2793+ }
 2794+
 2795+ /**
 2796+ * Free a result object
 2797+ */
 2798+ function free() {
 2799+ $this->db->freeResult( $this );
 2800+ unset( $this->result );
 2801+ unset( $this->db );
 2802+ }
 2803+
 2804+ /**
 2805+ * Change the position of the cursor in a result object
 2806+ * See mysql_data_seek()
 2807+ */
 2808+ function seek( $row ) {
 2809+ $this->db->dataSeek( $this, $row );
 2810+ }
 2811+
 2812+ /*********************
 2813+ * Iterator functions
 2814+ * Note that using these in combination with the non-iterator functions
 2815+ * above may cause rows to be skipped or repeated.
 2816+ */
 2817+
 2818+ function rewind() {
 2819+ if ($this->numRows()) {
 2820+ $this->db->dataSeek($this, 0);
 2821+ }
 2822+ $this->pos = 0;
 2823+ $this->currentRow = null;
 2824+ }
 2825+
 2826+ function current() {
 2827+ if ( is_null( $this->currentRow ) ) {
 2828+ $this->next();
 2829+ }
 2830+ return $this->currentRow;
 2831+ }
 2832+
 2833+ function key() {
 2834+ return $this->pos;
 2835+ }
 2836+
 2837+ function next() {
 2838+ $this->pos++;
 2839+ $this->currentRow = $this->fetchObject();
 2840+ return $this->currentRow;
 2841+ }
 2842+
 2843+ function valid() {
 2844+ return $this->current() !== false;
 2845+ }
 2846+}
 2847+
 2848+/**
 2849+ * Used by DatabaseBase::buildLike() to represent characters that have special meaning in SQL LIKE clauses
 2850+ * and thus need no escaping. Don't instantiate it manually, use Database::anyChar() and anyString() instead.
 2851+ */
 2852+class LikeMatch {
 2853+ private $str;
 2854+
 2855+ public function __construct( $s ) {
 2856+ $this->str = $s;
 2857+ }
 2858+
 2859+ public function toString() {
 2860+ return $this->str;
 2861+ }
 2862+}
Property changes on: trunk/extensions/MSSQLBackCompat/db/Database.php
___________________________________________________________________
Added: svn:keywords
12863 + Author Date Id Revision
Added: svn:eol-style
22864 + native
Index: trunk/extensions/MSSQLBackCompat/db/DatabaseMssql.php
@@ -0,0 +1,968 @@
 2+<?php
 3+/**
 4+ * This script is the MSSQL Server database abstraction layer
 5+ *
 6+ * See maintenance/mssql/README for development notes and other specific information
 7+ * @ingroup Database
 8+ * @file
 9+ */
 10+
 11+/**
 12+ * @ingroup Database
 13+ */
 14+class DatabaseMssql extends DatabaseBase {
 15+
 16+ var $mAffectedRows;
 17+ var $mLastResult;
 18+ var $mLastError;
 19+ var $mLastErrorNo;
 20+ var $mDatabaseFile;
 21+
 22+ /**
 23+ * Constructor
 24+ */
 25+ function __construct($server = false, $user = false, $password = false, $dbName = false,
 26+ $failFunction = false, $flags = 0, $tablePrefix = 'get from global') {
 27+
 28+ global $wgOut, $wgDBprefix, $wgCommandLineMode;
 29+ if (!isset($wgOut)) $wgOut = null; # Can't get a reference if it hasn't been set yet
 30+ $this->mOut =& $wgOut;
 31+ $this->mFailFunction = $failFunction;
 32+ $this->mFlags = $flags;
 33+
 34+ if ( $this->mFlags & DBO_DEFAULT ) {
 35+ if ( $wgCommandLineMode ) {
 36+ $this->mFlags &= ~DBO_TRX;
 37+ } else {
 38+ $this->mFlags |= DBO_TRX;
 39+ }
 40+ }
 41+
 42+ /** Get the default table prefix*/
 43+ $this->mTablePrefix = $tablePrefix == 'get from global' ? $wgDBprefix : $tablePrefix;
 44+
 45+ if ($server) $this->open($server, $user, $password, $dbName);
 46+
 47+ }
 48+
 49+ function getType() {
 50+ return 'mssql';
 51+ }
 52+
 53+ /**
 54+ * todo: check if these should be true like parent class
 55+ */
 56+ function implicitGroupby() { return false; }
 57+ function implicitOrderby() { return false; }
 58+
 59+ static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
 60+ return new DatabaseMssql($server, $user, $password, $dbName, $failFunction, $flags);
 61+ }
 62+
 63+ /** Open an MSSQL database and return a resource handle to it
 64+ * NOTE: only $dbName is used, the other parameters are irrelevant for MSSQL databases
 65+ */
 66+ function open($server,$user,$password,$dbName) {
 67+ wfProfileIn(__METHOD__);
 68+
 69+ # Test for missing mysql.so
 70+ # First try to load it
 71+ if (!@extension_loaded('mssql')) {
 72+ @dl('mssql.so');
 73+ }
 74+
 75+ # Fail now
 76+ # Otherwise we get a suppressed fatal error, which is very hard to track down
 77+ if (!function_exists( 'mssql_connect')) {
 78+ throw new DBConnectionError( $this, "MSSQL functions missing, have you compiled PHP with the --with-mssql option?\n" );
 79+ }
 80+
 81+ $this->close();
 82+ $this->mServer = $server;
 83+ $this->mUser = $user;
 84+ $this->mPassword = $password;
 85+ $this->mDBname = $dbName;
 86+
 87+ wfProfileIn("dbconnect-$server");
 88+
 89+ # Try to connect up to three times
 90+ # The kernel's default SYN retransmission period is far too slow for us,
 91+ # so we use a short timeout plus a manual retry.
 92+ $this->mConn = false;
 93+ $max = 3;
 94+ for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
 95+ if ( $i > 1 ) {
 96+ usleep( 1000 );
 97+ }
 98+ if ($this->mFlags & DBO_PERSISTENT) {
 99+ @/**/$this->mConn = mssql_pconnect($server, $user, $password);
 100+ } else {
 101+ # Create a new connection...
 102+ @/**/$this->mConn = mssql_connect($server, $user, $password, true);
 103+ }
 104+ }
 105+
 106+ wfProfileOut("dbconnect-$server");
 107+
 108+ if ($dbName != '') {
 109+ if ($this->mConn !== false) {
 110+ $success = @/**/mssql_select_db($dbName, $this->mConn);
 111+ if (!$success) {
 112+ $error = "Error selecting database $dbName on server {$this->mServer} " .
 113+ "from client host " . wfHostname() . "\n";
 114+ wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
 115+ wfDebug( $error );
 116+ }
 117+ } else {
 118+ wfDebug("DB connection error\n");
 119+ wfDebug("Server: $server, User: $user, Password: ".substr($password, 0, 3)."...\n");
 120+ $success = false;
 121+ }
 122+ } else {
 123+ # Delay USE query
 124+ $success = (bool)$this->mConn;
 125+ }
 126+
 127+ if (!$success) $this->reportConnectionError();
 128+ $this->mOpened = $success;
 129+ wfProfileOut(__METHOD__);
 130+ return $success;
 131+ }
 132+
 133+ /**
 134+ * Close an MSSQL database
 135+ */
 136+ function close() {
 137+ $this->mOpened = false;
 138+ if ($this->mConn) {
 139+ if ($this->trxLevel()) $this->commit();
 140+ return mssql_close($this->mConn);
 141+ } else return true;
 142+ }
 143+
 144+ /**
 145+ * - MSSQL doesn't seem to do buffered results
 146+ * - the trasnaction syntax is modified here to avoid having to replicate
 147+ * Database::query which uses BEGIN, COMMIT, ROLLBACK
 148+ */
 149+ function doQuery($sql) {
 150+ if ($sql == 'BEGIN' || $sql == 'COMMIT' || $sql == 'ROLLBACK') return true; # $sql .= ' TRANSACTION';
 151+ $sql = preg_replace('|[^\x07-\x7e]|','?',$sql); # TODO: need to fix unicode - just removing it here while testing
 152+ $ret = mssql_query($sql, $this->mConn);
 153+ if ($ret === false) {
 154+ $err = mssql_get_last_message();
 155+ if ($err) $this->mlastError = $err;
 156+ $row = mssql_fetch_row(mssql_query('select @@ERROR'));
 157+ if ($row[0]) $this->mlastErrorNo = $row[0];
 158+ } else $this->mlastErrorNo = false;
 159+ return $ret;
 160+ }
 161+
 162+ /**
 163+ * Free a result object
 164+ */
 165+ function freeResult( $res ) {
 166+ if ( $res instanceof ResultWrapper ) {
 167+ $res = $res->result;
 168+ }
 169+ if ( !@/**/mssql_free_result( $res ) ) {
 170+ throw new DBUnexpectedError( $this, "Unable to free MSSQL result" );
 171+ }
 172+ }
 173+
 174+ /**
 175+ * Fetch the next row from the given result object, in object form.
 176+ * Fields can be retrieved with $row->fieldname, with fields acting like
 177+ * member variables.
 178+ *
 179+ * @param $res SQL result object as returned from Database::query(), etc.
 180+ * @return MySQL row object
 181+ * @throws DBUnexpectedError Thrown if the database returns an error
 182+ */
 183+ function fetchObject( $res ) {
 184+ if ( $res instanceof ResultWrapper ) {
 185+ $res = $res->result;
 186+ }
 187+ @/**/$row = mssql_fetch_object( $res );
 188+ if ( $this->lastErrno() ) {
 189+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
 190+ }
 191+ return $row;
 192+ }
 193+
 194+ /**
 195+ * Fetch the next row from the given result object, in associative array
 196+ * form. Fields are retrieved with $row['fieldname'].
 197+ *
 198+ * @param $res SQL result object as returned from Database::query(), etc.
 199+ * @return MySQL row object
 200+ * @throws DBUnexpectedError Thrown if the database returns an error
 201+ */
 202+ function fetchRow( $res ) {
 203+ if ( $res instanceof ResultWrapper ) {
 204+ $res = $res->result;
 205+ }
 206+ @/**/$row = mssql_fetch_array( $res );
 207+ if ( $this->lastErrno() ) {
 208+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
 209+ }
 210+ return $row;
 211+ }
 212+
 213+ /**
 214+ * Get the number of rows in a result object
 215+ */
 216+ function numRows( $res ) {
 217+ if ( $res instanceof ResultWrapper ) {
 218+ $res = $res->result;
 219+ }
 220+ @/**/$n = mssql_num_rows( $res );
 221+ if ( $this->lastErrno() ) {
 222+ throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
 223+ }
 224+ return $n;
 225+ }
 226+
 227+ /**
 228+ * Get the number of fields in a result object
 229+ * See documentation for mysql_num_fields()
 230+ * @param $res SQL result object as returned from Database::query(), etc.
 231+ */
 232+ function numFields( $res ) {
 233+ if ( $res instanceof ResultWrapper ) {
 234+ $res = $res->result;
 235+ }
 236+ return mssql_num_fields( $res );
 237+ }
 238+
 239+ /**
 240+ * Get a field name in a result object
 241+ * See documentation for mysql_field_name():
 242+ * http://www.php.net/mysql_field_name
 243+ * @param $res SQL result object as returned from Database::query(), etc.
 244+ * @param $n Int
 245+ */
 246+ function fieldName( $res, $n ) {
 247+ if ( $res instanceof ResultWrapper ) {
 248+ $res = $res->result;
 249+ }
 250+ return mssql_field_name( $res, $n );
 251+ }
 252+
 253+ /**
 254+ * Get the inserted value of an auto-increment row
 255+ *
 256+ * The value inserted should be fetched from nextSequenceValue()
 257+ *
 258+ * Example:
 259+ * $id = $dbw->nextSequenceValue('page_page_id_seq');
 260+ * $dbw->insert('page',array('page_id' => $id));
 261+ * $id = $dbw->insertId();
 262+ */
 263+ function insertId() {
 264+ $row = mssql_fetch_row(mssql_query('select @@IDENTITY'));
 265+ return $row[0];
 266+ }
 267+
 268+ /**
 269+ * Change the position of the cursor in a result object
 270+ * See mysql_data_seek()
 271+ * @param $res SQL result object as returned from Database::query(), etc.
 272+ * @param $row Database row
 273+ */
 274+ function dataSeek( $res, $row ) {
 275+ if ( $res instanceof ResultWrapper ) {
 276+ $res = $res->result;
 277+ }
 278+ return mssql_data_seek( $res, $row );
 279+ }
 280+
 281+ /**
 282+ * Get the last error number
 283+ */
 284+ function lastErrno() {
 285+ return $this->mlastErrorNo;
 286+ }
 287+
 288+ /**
 289+ * Get a description of the last error
 290+ */
 291+ function lastError() {
 292+ return $this->mlastError;
 293+ }
 294+
 295+ /**
 296+ * Get the number of rows affected by the last write query
 297+ */
 298+ function affectedRows() {
 299+ return mssql_rows_affected( $this->mConn );
 300+ }
 301+
 302+ /**
 303+ * Simple UPDATE wrapper
 304+ * Usually aborts on failure
 305+ * If errors are explicitly ignored, returns success
 306+ *
 307+ * This function exists for historical reasons, Database::update() has a more standard
 308+ * calling convention and feature set
 309+ */
 310+ function set( $table, $var, $value, $cond, $fname = 'Database::set' )
 311+ {
 312+ if ($value == "NULL") $value = "''"; # see comments in makeListWithoutNulls()
 313+ $table = $this->tableName( $table );
 314+ $sql = "UPDATE $table SET $var = '" .
 315+ $this->strencode( $value ) . "' WHERE ($cond)";
 316+ return (bool)$this->query( $sql, $fname );
 317+ }
 318+
 319+ /**
 320+ * Simple SELECT wrapper, returns a single field, input must be encoded
 321+ * Usually aborts on failure
 322+ * If errors are explicitly ignored, returns FALSE on failure
 323+ */
 324+ function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
 325+ if ( !is_array( $options ) ) {
 326+ $options = array( $options );
 327+ }
 328+ $options['LIMIT'] = 1;
 329+
 330+ $res = $this->select( $table, $var, $cond, $fname, $options );
 331+ if ( $res === false || !$this->numRows( $res ) ) {
 332+ return false;
 333+ }
 334+ $row = $this->fetchRow( $res );
 335+ if ( $row !== false ) {
 336+ $this->freeResult( $res );
 337+ return $row[0];
 338+ } else {
 339+ return false;
 340+ }
 341+ }
 342+
 343+ /**
 344+ * Returns an optional USE INDEX clause to go after the table, and a
 345+ * string to go at the end of the query
 346+ *
 347+ * @private
 348+ *
 349+ * @param $options Array: an associative array of options to be turned into
 350+ * an SQL query, valid keys are listed in the function.
 351+ * @return array
 352+ */
 353+ function makeSelectOptions( $options ) {
 354+ $preLimitTail = $postLimitTail = '';
 355+ $startOpts = '';
 356+
 357+ $noKeyOptions = array();
 358+ foreach ( $options as $key => $option ) {
 359+ if ( is_numeric( $key ) ) {
 360+ $noKeyOptions[$option] = true;
 361+ }
 362+ }
 363+
 364+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
 365+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
 366+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
 367+
 368+ //if (isset($options['LIMIT'])) {
 369+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
 370+ // isset($options['OFFSET']) ? $options['OFFSET']
 371+ // : false);
 372+ //}
 373+
 374+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
 375+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
 376+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
 377+
 378+ # Various MySQL extensions
 379+ if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
 380+ if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
 381+ if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
 382+ if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
 383+ if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
 384+ if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
 385+ if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
 386+ if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
 387+
 388+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
 389+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
 390+ } else {
 391+ $useIndex = '';
 392+ }
 393+
 394+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
 395+ }
 396+
 397+ /**
 398+ * SELECT wrapper
 399+ *
 400+ * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
 401+ * @param $vars Mixed: Array or string, field name(s) to be retrieved
 402+ * @param $conds Mixed: Array or string, condition(s) for WHERE
 403+ * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
 404+ * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
 405+ * see Database::makeSelectOptions code for list of supported stuff
 406+ * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
 407+ */
 408+ function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() )
 409+ {
 410+ if( is_array( $vars ) ) {
 411+ $vars = implode( ',', $vars );
 412+ }
 413+ if( !is_array( $options ) ) {
 414+ $options = array( $options );
 415+ }
 416+ if( is_array( $table ) ) {
 417+ if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
 418+ $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
 419+ else
 420+ $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
 421+ } elseif ($table!='') {
 422+ if ($table{0}==' ') {
 423+ $from = ' FROM ' . $table;
 424+ } else {
 425+ $from = ' FROM ' . $this->tableName( $table );
 426+ }
 427+ } else {
 428+ $from = '';
 429+ }
 430+
 431+ list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
 432+
 433+ if( !empty( $conds ) ) {
 434+ if ( is_array( $conds ) ) {
 435+ $conds = $this->makeList( $conds, LIST_AND );
 436+ }
 437+ $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
 438+ } else {
 439+ $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
 440+ }
 441+
 442+ if (isset($options['LIMIT']))
 443+ $sql = $this->limitResult($sql, $options['LIMIT'],
 444+ isset($options['OFFSET']) ? $options['OFFSET'] : false);
 445+ $sql = "$sql $postLimitTail";
 446+
 447+ if (isset($options['EXPLAIN'])) {
 448+ $sql = 'EXPLAIN ' . $sql;
 449+ }
 450+ return $this->query( $sql, $fname );
 451+ }
 452+
 453+ /**
 454+ * Determines whether a field exists in a table
 455+ * Usually aborts on failure
 456+ * If errors are explicitly ignored, returns NULL on failure
 457+ */
 458+ function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
 459+ $table = $this->tableName( $table );
 460+ $sql = "SELECT TOP 1 * FROM $table";
 461+ $res = $this->query( $sql, 'Database::fieldExists' );
 462+
 463+ $found = false;
 464+ while ( $row = $this->fetchArray( $res ) ) {
 465+ if ( isset($row[$field]) ) {
 466+ $found = true;
 467+ break;
 468+ }
 469+ }
 470+
 471+ $this->freeResult( $res );
 472+ return $found;
 473+ }
 474+
 475+ /**
 476+ * Get information about an index into an object
 477+ * Returns false if the index does not exist
 478+ */
 479+ function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
 480+
 481+ throw new DBUnexpectedError( $this, 'Database::indexInfo called which is not supported yet' );
 482+ return null;
 483+
 484+ $table = $this->tableName( $table );
 485+ $sql = 'SHOW INDEX FROM '.$table;
 486+ $res = $this->query( $sql, $fname );
 487+ if ( !$res ) {
 488+ return null;
 489+ }
 490+
 491+ $result = array();
 492+ while ( $row = $this->fetchObject( $res ) ) {
 493+ if ( $row->Key_name == $index ) {
 494+ $result[] = $row;
 495+ }
 496+ }
 497+ $this->freeResult($res);
 498+
 499+ return empty($result) ? false : $result;
 500+ }
 501+
 502+ /**
 503+ * Query whether a given table exists
 504+ */
 505+ function tableExists( $table ) {
 506+ $table = $this->tableName( $table );
 507+ $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '$table'" );
 508+ $exist = ($res->numRows() > 0);
 509+ $this->freeResult($res);
 510+ return $exist;
 511+ }
 512+
 513+ /**
 514+ * mysql_fetch_field() wrapper
 515+ * Returns false if the field doesn't exist
 516+ *
 517+ * @param $table
 518+ * @param $field
 519+ */
 520+ function fieldInfo( $table, $field ) {
 521+ $table = $this->tableName( $table );
 522+ $res = $this->query( "SELECT TOP 1 * FROM $table" );
 523+ $n = mssql_num_fields( $res->result );
 524+ for( $i = 0; $i < $n; $i++ ) {
 525+ $meta = mssql_fetch_field( $res->result, $i );
 526+ if( $field == $meta->name ) {
 527+ return new MSSQLField($meta);
 528+ }
 529+ }
 530+ return false;
 531+ }
 532+
 533+ /**
 534+ * mysql_field_type() wrapper
 535+ */
 536+ function fieldType( $res, $index ) {
 537+ if ( $res instanceof ResultWrapper ) {
 538+ $res = $res->result;
 539+ }
 540+ return mssql_field_type( $res, $index );
 541+ }
 542+
 543+ /**
 544+ * INSERT wrapper, inserts an array into a table
 545+ *
 546+ * $a may be a single associative array, or an array of these with numeric keys, for
 547+ * multi-row insert.
 548+ *
 549+ * Usually aborts on failure
 550+ * If errors are explicitly ignored, returns success
 551+ *
 552+ * Same as parent class implementation except that it removes primary key from column lists
 553+ * because MSSQL doesn't support writing nulls to IDENTITY (AUTO_INCREMENT) columns
 554+ */
 555+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
 556+ # No rows to insert, easy just return now
 557+ if ( !count( $a ) ) {
 558+ return true;
 559+ }
 560+ $table = $this->tableName( $table );
 561+ if ( !is_array( $options ) ) {
 562+ $options = array( $options );
 563+ }
 564+
 565+ # todo: need to record primary keys at table create time, and remove NULL assignments to them
 566+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
 567+ $multi = true;
 568+ $keys = array_keys( $a[0] );
 569+# if (ereg('_id$',$keys[0])) {
 570+ foreach ($a as $i) {
 571+ if (is_null($i[$keys[0]])) unset($i[$keys[0]]); # remove primary-key column from multiple insert lists if empty value
 572+ }
 573+# }
 574+ $keys = array_keys( $a[0] );
 575+ } else {
 576+ $multi = false;
 577+ $keys = array_keys( $a );
 578+# if (ereg('_id$',$keys[0]) && empty($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
 579+ if (is_null($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
 580+ $keys = array_keys( $a );
 581+ }
 582+
 583+ # handle IGNORE option
 584+ # example:
 585+ # MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
 586+ # MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
 587+ $ignore = in_array('IGNORE',$options);
 588+
 589+ # remove IGNORE from options list
 590+ if ($ignore) {
 591+ $oldoptions = $options;
 592+ $options = array();
 593+ foreach ($oldoptions as $o) if ($o != 'IGNORE') $options[] = $o;
 594+ }
 595+
 596+ $keylist = implode(',', $keys);
 597+ $sql = 'INSERT '.implode(' ', $options)." INTO $table (".implode(',', $keys).') VALUES ';
 598+ if ($multi) {
 599+ if ($ignore) {
 600+ # If multiple and ignore, then do each row as a separate conditional insert
 601+ foreach ($a as $row) {
 602+ $prival = $row[$keys[0]];
 603+ $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
 604+ if (!$this->query("$sql (".$this->makeListWithoutNulls($row).')', $fname)) return false;
 605+ }
 606+ return true;
 607+ } else {
 608+ $first = true;
 609+ foreach ($a as $row) {
 610+ if ($first) $first = false; else $sql .= ',';
 611+ $sql .= '('.$this->makeListWithoutNulls($row).')';
 612+ }
 613+ }
 614+ } else {
 615+ if ($ignore) {
 616+ $prival = $a[$keys[0]];
 617+ $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
 618+ }
 619+ $sql .= '('.$this->makeListWithoutNulls($a).')';
 620+ }
 621+ return (bool)$this->query( $sql, $fname );
 622+ }
 623+
 624+ /**
 625+ * MSSQL doesn't allow implicit casting of NULL's into non-null values for NOT NULL columns
 626+ * for now I've just converted the NULL's in the lists for updates and inserts into empty strings
 627+ * which get implicitly casted to 0 for numeric columns
 628+ * NOTE: the set() method above converts NULL to empty string as well but not via this method
 629+ */
 630+ function makeListWithoutNulls($a, $mode = LIST_COMMA) {
 631+ return str_replace("NULL","''",$this->makeList($a,$mode));
 632+ }
 633+
 634+ /**
 635+ * UPDATE wrapper, takes a condition array and a SET array
 636+ *
 637+ * @param $table String: The table to UPDATE
 638+ * @param $values Array: An array of values to SET
 639+ * @param $conds Array: An array of conditions (WHERE). Use '*' to update all rows.
 640+ * @param $fname String: The Class::Function calling this function
 641+ * (for the log)
 642+ * @param $options Array: An array of UPDATE options, can be one or
 643+ * more of IGNORE, LOW_PRIORITY
 644+ * @return bool
 645+ */
 646+ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
 647+ $table = $this->tableName( $table );
 648+ $opts = $this->makeUpdateOptions( $options );
 649+ $sql = "UPDATE $opts $table SET " . $this->makeListWithoutNulls( $values, LIST_SET );
 650+ if ( $conds != '*' ) {
 651+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
 652+ }
 653+ return $this->query( $sql, $fname );
 654+ }
 655+
 656+ /**
 657+ * Make UPDATE options for the Database::update function
 658+ *
 659+ * @private
 660+ * @param $options Array: The options passed to Database::update
 661+ * @return string
 662+ */
 663+ function makeUpdateOptions( $options ) {
 664+ if( !is_array( $options ) ) {
 665+ $options = array( $options );
 666+ }
 667+ $opts = array();
 668+ if ( in_array( 'LOW_PRIORITY', $options ) )
 669+ $opts[] = $this->lowPriorityOption();
 670+ if ( in_array( 'IGNORE', $options ) )
 671+ $opts[] = 'IGNORE';
 672+ return implode(' ', $opts);
 673+ }
 674+
 675+ /**
 676+ * Change the current database
 677+ */
 678+ function selectDB( $db ) {
 679+ $this->mDBname = $db;
 680+ return mssql_select_db( $db, $this->mConn );
 681+ }
 682+
 683+ /**
 684+ * MSSQL has a problem with the backtick quoting, so all this does is ensure the prefix is added exactly once
 685+ */
 686+ function tableName($name) {
 687+ return strpos($name, $this->mTablePrefix) === 0 ? $name : "{$this->mTablePrefix}$name";
 688+ }
 689+
 690+ /**
 691+ * MSSQL doubles quotes instead of escaping them
 692+ * @param $s String to be slashed.
 693+ * @return string slashed string.
 694+ */
 695+ function strencode($s) {
 696+ return str_replace("'","''",$s);
 697+ }
 698+
 699+ /**
 700+ * REPLACE query wrapper
 701+ * PostgreSQL simulates this with a DELETE followed by INSERT
 702+ * $row is the row to insert, an associative array
 703+ * $uniqueIndexes is an array of indexes. Each element may be either a
 704+ * field name or an array of field names
 705+ *
 706+ * It may be more efficient to leave off unique indexes which are unlikely to collide.
 707+ * However if you do this, you run the risk of encountering errors which wouldn't have
 708+ * occurred in MySQL
 709+ *
 710+ * @todo migrate comment to phodocumentor format
 711+ */
 712+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
 713+ $table = $this->tableName( $table );
 714+
 715+ # Single row case
 716+ if ( !is_array( reset( $rows ) ) ) {
 717+ $rows = array( $rows );
 718+ }
 719+
 720+ $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
 721+ $first = true;
 722+ foreach ( $rows as $row ) {
 723+ if ( $first ) {
 724+ $first = false;
 725+ } else {
 726+ $sql .= ',';
 727+ }
 728+ $sql .= '(' . $this->makeList( $row ) . ')';
 729+ }
 730+ return $this->query( $sql, $fname );
 731+ }
 732+
 733+ /**
 734+ * DELETE where the condition is a join
 735+ * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
 736+ *
 737+ * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
 738+ * join condition matches, set $conds='*'
 739+ *
 740+ * DO NOT put the join condition in $conds
 741+ *
 742+ * @param $delTable String: The table to delete from.
 743+ * @param $joinTable String: The other table.
 744+ * @param $delVar String: The variable to join on, in the first table.
 745+ * @param $joinVar String: The variable to join on, in the second table.
 746+ * @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause
 747+ * @param $fname String: Calling function name
 748+ */
 749+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
 750+ if ( !$conds ) {
 751+ throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
 752+ }
 753+
 754+ $delTable = $this->tableName( $delTable );
 755+ $joinTable = $this->tableName( $joinTable );
 756+ $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
 757+ if ( $conds != '*' ) {
 758+ $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
 759+ }
 760+
 761+ return $this->query( $sql, $fname );
 762+ }
 763+
 764+ /**
 765+ * Returns the size of a text field, or -1 for "unlimited"
 766+ */
 767+ function textFieldSize( $table, $field ) {
 768+ $table = $this->tableName( $table );
 769+ $sql = "SELECT TOP 1 * FROM $table;";
 770+ $res = $this->query( $sql, 'Database::textFieldSize' );
 771+ $row = $this->fetchObject( $res );
 772+ $this->freeResult( $res );
 773+
 774+ $m = array();
 775+ if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
 776+ $size = $m[1];
 777+ } else {
 778+ $size = -1;
 779+ }
 780+ return $size;
 781+ }
 782+
 783+ /**
 784+ * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
 785+ */
 786+ function lowPriorityOption() {
 787+ return 'LOW_PRIORITY';
 788+ }
 789+
 790+ /**
 791+ * INSERT SELECT wrapper
 792+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
 793+ * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
 794+ * $conds may be "*" to copy the whole table
 795+ * srcTable may be an array of tables.
 796+ */
 797+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
 798+ $insertOptions = array(), $selectOptions = array() )
 799+ {
 800+ $destTable = $this->tableName( $destTable );
 801+ if ( is_array( $insertOptions ) ) {
 802+ $insertOptions = implode( ' ', $insertOptions );
 803+ }
 804+ if( !is_array( $selectOptions ) ) {
 805+ $selectOptions = array( $selectOptions );
 806+ }
 807+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
 808+ if( is_array( $srcTable ) ) {
 809+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
 810+ } else {
 811+ $srcTable = $this->tableName( $srcTable );
 812+ }
 813+ $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
 814+ " SELECT $startOpts " . implode( ',', $varMap ) .
 815+ " FROM $srcTable $useIndex ";
 816+ if ( $conds != '*' ) {
 817+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
 818+ }
 819+ $sql .= " $tailOpts";
 820+ return $this->query( $sql, $fname );
 821+ }
 822+
 823+ /**
 824+ * Construct a LIMIT query with optional offset
 825+ * This is used for query pages
 826+ * $sql string SQL query we will append the limit to
 827+ * $limit integer the SQL limit
 828+ * $offset integer the SQL offset (default false)
 829+ */
 830+ function limitResult($sql, $limit, $offset=false) {
 831+ if( !is_numeric($limit) ) {
 832+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
 833+ }
 834+ if ($offset) {
 835+ throw new DBUnexpectedError( $this, 'Database::limitResult called with non-zero offset which is not supported yet' );
 836+ } else {
 837+ $sql = ereg_replace("^SELECT", "SELECT TOP $limit", $sql);
 838+ }
 839+ return $sql;
 840+ }
 841+
 842+ /**
 843+ * Should determine if the last failure was due to a deadlock
 844+ * @return bool
 845+ */
 846+ function wasDeadlock() {
 847+ return $this->lastErrno() == 1205;
 848+ }
 849+
 850+ /**
 851+ * Return MW-style timestamp used for MySQL schema
 852+ */
 853+ function timestamp( $ts=0 ) {
 854+ return wfTimestamp(TS_MW,$ts);
 855+ }
 856+
 857+ /**
 858+ * Local database timestamp format or null
 859+ */
 860+ function timestampOrNull( $ts = null ) {
 861+ if( is_null( $ts ) ) {
 862+ return null;
 863+ } else {
 864+ return $this->timestamp( $ts );
 865+ }
 866+ }
 867+
 868+ /**
 869+ * @return string wikitext of a link to the server software's web site
 870+ */
 871+ function getSoftwareLink() {
 872+ return "[http://www.microsoft.com/sql/default.mspx Microsoft SQL Server 2005 Home]";
 873+ }
 874+
 875+ /**
 876+ * @return string Version information from the database
 877+ */
 878+ function getServerVersion() {
 879+ $row = mssql_fetch_row(mssql_query('select @@VERSION'));
 880+ return ereg("^(.+[0-9]+\\.[0-9]+\\.[0-9]+) ",$row[0],$m) ? $m[1] : $row[0];
 881+ }
 882+
 883+ function limitResultForUpdate($sql, $num) {
 884+ return $sql;
 885+ }
 886+
 887+ /**
 888+ * How lagged is this slave?
 889+ */
 890+ public function getLag() {
 891+ return 0;
 892+ }
 893+
 894+ /**
 895+ * Called by the installer script
 896+ * - this is the same way as DatabasePostgresql.php, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
 897+ */
 898+ public function setup_database() {
 899+ global $IP,$wgDBTableOptions;
 900+ $wgDBTableOptions = '';
 901+ $mysql_tmpl = "$IP/maintenance/tables.sql";
 902+ $mysql_iw = "$IP/maintenance/interwiki.sql";
 903+ $mssql_tmpl = "$IP/maintenance/mssql/tables.sql";
 904+
 905+ # Make an MSSQL template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
 906+ if (!file_exists($mssql_tmpl)) { # todo: make this conditional again
 907+ $sql = file_get_contents($mysql_tmpl);
 908+ $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
 909+ $sql = preg_replace('/^\s*(UNIQUE )?(INDEX|KEY|FULLTEXT).+?$/m', '', $sql); # These indexes should be created with a CREATE INDEX query
 910+ $sql = preg_replace('/(\sKEY) [^\(]+\(/is', '$1 (', $sql); # "KEY foo (foo)" should just be "KEY (foo)"
 911+ $sql = preg_replace('/(varchar\([0-9]+\))\s+binary/i', '$1', $sql); # "varchar(n) binary" cannot be followed by "binary"
 912+ $sql = preg_replace('/(var)?binary\(([0-9]+)\)/ie', '"varchar(".strlen(pow(2,$2)).")"', $sql); # use varchar(chars) not binary(bits)
 913+ $sql = preg_replace('/ (var)?binary/i', ' varchar', $sql); # use varchar not binary
 914+ $sql = preg_replace('/(varchar\([0-9]+\)(?! N))/', '$1 NULL', $sql); # MSSQL complains if NULL is put into a varchar
 915+ #$sql = preg_replace('/ binary/i',' varchar',$sql); # MSSQL binary's can't be assigned with strings, so use varchar's instead
 916+ #$sql = preg_replace('/(binary\([0-9]+\) (NOT NULL )?default) [\'"].*?[\'"]/i','$1 0',$sql); # binary default cannot be string
 917+ $sql = preg_replace('/[a-z]*(blob|text)([ ,])/i', 'text$2', $sql); # no BLOB types in MSSQL
 918+ $sql = preg_replace('/\).+?;/',');', $sql); # remove all table options
 919+ $sql = preg_replace('/ (un)?signed/i', '', $sql);
 920+ $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
 921+ $sql = str_replace(' bool ', ' bit ', $sql);
 922+ $sql = str_replace('auto_increment', 'IDENTITY(1,1)', $sql);
 923+ #$sql = preg_replace('/NOT NULL(?! IDENTITY)/', 'NULL', $sql); # Allow NULL's for non IDENTITY columns
 924+
 925+ # Tidy up and write file
 926+ $sql = preg_replace('/,\s*\)/s', "\n)", $sql); # Remove spurious commas left after INDEX removals
 927+ $sql = preg_replace('/^\s*^/m', '', $sql); # Remove empty lines
 928+ $sql = preg_replace('/;$/m', ";\n", $sql); # Separate each statement with an empty line
 929+ file_put_contents($mssql_tmpl, $sql);
 930+ }
 931+
 932+ # Parse the MSSQL template replacing inline variables such as /*$wgDBprefix*/
 933+ $err = $this->sourceFile($mssql_tmpl);
 934+ if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
 935+
 936+ # Use DatabasePostgres's code to populate interwiki from MySQL template
 937+ $f = fopen($mysql_iw,'r');
 938+ if ($f == false) dieout("<li>Could not find the interwiki.sql file");
 939+ $sql = "INSERT INTO {$this->mTablePrefix}interwiki(iw_prefix,iw_url,iw_local) VALUES ";
 940+ while (!feof($f)) {
 941+ $line = fgets($f,1024);
 942+ $matches = array();
 943+ if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
 944+ $this->query("$sql $matches[1],$matches[2])");
 945+ }
 946+ }
 947+
 948+ public function getSearchEngine() {
 949+ return "SearchEngineDummy";
 950+ }
 951+}
 952+
 953+/**
 954+ * @ingroup Database
 955+ */
 956+class MSSQLField extends MySQLField {
 957+
 958+ function __construct() {
 959+ }
 960+
 961+ static function fromText($db, $table, $field) {
 962+ $n = new MSSQLField;
 963+ $n->name = $field;
 964+ $n->tablename = $table;
 965+ return $n;
 966+ }
 967+
 968+} // end DatabaseMssql class
 969+
Property changes on: trunk/extensions/MSSQLBackCompat/db/DatabaseMssql.php
___________________________________________________________________
Added: svn:eol-style
1970 + native
Index: trunk/extensions/MSSQLBackCompat/db/LBFactory.php
@@ -0,0 +1,271 @@
 2+<?php
 3+/**
 4+ * @file
 5+ * @ingroup Database
 6+ */
 7+
 8+/**
 9+ * An interface for generating database load balancers
 10+ * @ingroup Database
 11+ */
 12+abstract class LBFactory {
 13+ static $instance;
 14+
 15+ /**
 16+ * Get an LBFactory instance
 17+ */
 18+ static function &singleton() {
 19+ if ( is_null( self::$instance ) ) {
 20+ global $wgLBFactoryConf;
 21+ $class = $wgLBFactoryConf['class'];
 22+ self::$instance = new $class( $wgLBFactoryConf );
 23+ }
 24+ return self::$instance;
 25+ }
 26+
 27+ /**
 28+ * Shut down, close connections and destroy the cached instance.
 29+ *
 30+ */
 31+ static function destroyInstance() {
 32+ if ( self::$instance ) {
 33+ self::$instance->shutdown();
 34+ self::$instance->forEachLBCallMethod( 'closeAll' );
 35+ self::$instance = null;
 36+ }
 37+ }
 38+
 39+ /**
 40+ * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
 41+ */
 42+ abstract function __construct( $conf );
 43+
 44+ /**
 45+ * Create a new load balancer object. The resulting object will be untracked,
 46+ * not chronology-protected, and the caller is responsible for cleaning it up.
 47+ *
 48+ * @param string $wiki Wiki ID, or false for the current wiki
 49+ * @return LoadBalancer
 50+ */
 51+ abstract function newMainLB( $wiki = false );
 52+
 53+ /**
 54+ * Get a cached (tracked) load balancer object.
 55+ *
 56+ * @param string $wiki Wiki ID, or false for the current wiki
 57+ * @return LoadBalancer
 58+ */
 59+ abstract function getMainLB( $wiki = false );
 60+
 61+ /*
 62+ * Create a new load balancer for external storage. The resulting object will be
 63+ * untracked, not chronology-protected, and the caller is responsible for
 64+ * cleaning it up.
 65+ *
 66+ * @param string $cluster External storage cluster, or false for core
 67+ * @param string $wiki Wiki ID, or false for the current wiki
 68+ */
 69+ abstract function newExternalLB( $cluster, $wiki = false );
 70+
 71+ /*
 72+ * Get a cached (tracked) load balancer for external storage
 73+ *
 74+ * @param string $cluster External storage cluster, or false for core
 75+ * @param string $wiki Wiki ID, or false for the current wiki
 76+ */
 77+ abstract function &getExternalLB( $cluster, $wiki = false );
 78+
 79+ /**
 80+ * Execute a function for each tracked load balancer
 81+ * The callback is called with the load balancer as the first parameter,
 82+ * and $params passed as the subsequent parameters.
 83+ */
 84+ abstract function forEachLB( $callback, $params = array() );
 85+
 86+ /**
 87+ * Prepare all tracked load balancers for shutdown
 88+ * STUB
 89+ */
 90+ function shutdown() {}
 91+
 92+ /**
 93+ * Call a method of each tracked load balancer
 94+ */
 95+ function forEachLBCallMethod( $methodName, $args = array() ) {
 96+ $this->forEachLB( array( $this, 'callMethod' ), array( $methodName, $args ) );
 97+ }
 98+
 99+ /**
 100+ * Private helper for forEachLBCallMethod
 101+ */
 102+ function callMethod( $loadBalancer, $methodName, $args ) {
 103+ call_user_func_array( array( $loadBalancer, $methodName ), $args );
 104+ }
 105+
 106+ /**
 107+ * Commit changes on all master connections
 108+ */
 109+ function commitMasterChanges() {
 110+ $this->forEachLBCallMethod( 'commitMasterChanges' );
 111+ }
 112+}
 113+
 114+/**
 115+ * A simple single-master LBFactory that gets its configuration from the b/c globals
 116+ */
 117+class LBFactory_Simple extends LBFactory {
 118+ var $mainLB;
 119+ var $extLBs = array();
 120+
 121+ # Chronology protector
 122+ var $chronProt;
 123+
 124+ function __construct( $conf ) {
 125+ $this->chronProt = new ChronologyProtector;
 126+ }
 127+
 128+ function newMainLB( $wiki = false ) {
 129+ global $wgDBservers, $wgMasterWaitTimeout;
 130+ if ( $wgDBservers ) {
 131+ $servers = $wgDBservers;
 132+ } else {
 133+ global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
 134+ $servers = array(array(
 135+ 'host' => $wgDBserver,
 136+ 'user' => $wgDBuser,
 137+ 'password' => $wgDBpassword,
 138+ 'dbname' => $wgDBname,
 139+ 'type' => $wgDBtype,
 140+ 'load' => 1,
 141+ 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
 142+ ));
 143+ }
 144+
 145+ return new LoadBalancer( array(
 146+ 'servers' => $servers,
 147+ 'masterWaitTimeout' => $wgMasterWaitTimeout
 148+ ));
 149+ }
 150+
 151+ function getMainLB( $wiki = false ) {
 152+ if ( !isset( $this->mainLB ) ) {
 153+ $this->mainLB = $this->newMainLB( $wiki );
 154+ $this->mainLB->parentInfo( array( 'id' => 'main' ) );
 155+ $this->chronProt->initLB( $this->mainLB );
 156+ }
 157+ return $this->mainLB;
 158+ }
 159+
 160+ function newExternalLB( $cluster, $wiki = false ) {
 161+ global $wgExternalServers;
 162+ if ( !isset( $wgExternalServers[$cluster] ) ) {
 163+ throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
 164+ }
 165+ return new LoadBalancer( array(
 166+ 'servers' => $wgExternalServers[$cluster]
 167+ ));
 168+ }
 169+
 170+ function &getExternalLB( $cluster, $wiki = false ) {
 171+ if ( !isset( $this->extLBs[$cluster] ) ) {
 172+ $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
 173+ $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
 174+ }
 175+ return $this->extLBs[$cluster];
 176+ }
 177+
 178+ /**
 179+ * Execute a function for each tracked load balancer
 180+ * The callback is called with the load balancer as the first parameter,
 181+ * and $params passed as the subsequent parameters.
 182+ */
 183+ function forEachLB( $callback, $params = array() ) {
 184+ if ( isset( $this->mainLB ) ) {
 185+ call_user_func_array( $callback, array_merge( array( $this->mainLB ), $params ) );
 186+ }
 187+ foreach ( $this->extLBs as $lb ) {
 188+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
 189+ }
 190+ }
 191+
 192+ function shutdown() {
 193+ if ( $this->mainLB ) {
 194+ $this->chronProt->shutdownLB( $this->mainLB );
 195+ }
 196+ $this->chronProt->shutdown();
 197+ $this->commitMasterChanges();
 198+ }
 199+}
 200+
 201+/**
 202+ * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
 203+ * Kind of like Hawking's [[Chronology Protection Agency]].
 204+ */
 205+class ChronologyProtector {
 206+ var $startupPos;
 207+ var $shutdownPos = array();
 208+
 209+ /**
 210+ * Initialise a LoadBalancer to give it appropriate chronology protection.
 211+ *
 212+ * @param LoadBalancer $lb
 213+ */
 214+ function initLB( $lb ) {
 215+ if ( $this->startupPos === null ) {
 216+ if ( !empty( $_SESSION[__CLASS__] ) ) {
 217+ $this->startupPos = $_SESSION[__CLASS__];
 218+ }
 219+ }
 220+ if ( !$this->startupPos ) {
 221+ return;
 222+ }
 223+ $masterName = $lb->getServerName( 0 );
 224+
 225+ if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) {
 226+ $info = $lb->parentInfo();
 227+ $pos = $this->startupPos[$masterName];
 228+ wfDebug( __METHOD__.": LB " . $info['id'] . " waiting for master pos $pos\n" );
 229+ $lb->waitFor( $this->startupPos[$masterName] );
 230+ }
 231+ }
 232+
 233+ /**
 234+ * Notify the ChronologyProtector that the LoadBalancer is about to shut
 235+ * down. Saves replication positions.
 236+ *
 237+ * @param LoadBalancer $lb
 238+ */
 239+ function shutdownLB( $lb ) {
 240+ // Don't start a session, don't bother with non-replicated setups
 241+ if ( strval( session_id() ) == '' || $lb->getServerCount() <= 1 ) {
 242+ return;
 243+ }
 244+ $masterName = $lb->getServerName( 0 );
 245+ if ( isset( $this->shutdownPos[$masterName] ) ) {
 246+ // Already done
 247+ return;
 248+ }
 249+ // Only save the position if writes have been done on the connection
 250+ $db = $lb->getAnyOpenConnection( 0 );
 251+ $info = $lb->parentInfo();
 252+ if ( !$db || !$db->doneWrites() ) {
 253+ wfDebug( __METHOD__.": LB {$info['id']}, no writes done\n" );
 254+ return;
 255+ }
 256+ $pos = $db->getMasterPos();
 257+ wfDebug( __METHOD__.": LB {$info['id']} has master pos $pos\n" );
 258+ $this->shutdownPos[$masterName] = $pos;
 259+ }
 260+
 261+ /**
 262+ * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
 263+ * May commit chronology data to persistent storage.
 264+ */
 265+ function shutdown() {
 266+ if ( session_id() != '' && count( $this->shutdownPos ) ) {
 267+ wfDebug( __METHOD__.": saving master pos for " .
 268+ count( $this->shutdownPos ) . " master(s)\n" );
 269+ $_SESSION[__CLASS__] = $this->shutdownPos;
 270+ }
 271+ }
 272+}
Property changes on: trunk/extensions/MSSQLBackCompat/db/LBFactory.php
___________________________________________________________________
Added: svn:eol-style
1273 + native
Index: trunk/extensions/MSSQLBackCompat/db/DatabaseSqlite.php
@@ -0,0 +1,581 @@
 2+<?php
 3+/**
 4+ * This script is the SQLite database abstraction layer
 5+ *
 6+ * See maintenance/sqlite/README for development notes and other specific information
 7+ * @ingroup Database
 8+ * @file
 9+ */
 10+
 11+/**
 12+ * @ingroup Database
 13+ */
 14+class DatabaseSqlite extends DatabaseBase {
 15+
 16+ var $mAffectedRows;
 17+ var $mLastResult;
 18+ var $mDatabaseFile;
 19+ var $mName;
 20+
 21+ /**
 22+ * Constructor.
 23+ * Parameters $server, $user and $password are not used.
 24+ */
 25+ function __construct( $server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0 ) {
 26+ global $wgSQLiteDataDir;
 27+ $this->mFailFunction = $failFunction;
 28+ $this->mFlags = $flags;
 29+ $this->mDatabaseFile = self::generateFileName( $wgSQLiteDataDir, $dbName );
 30+ if( !is_readable( $this->mDatabaseFile ) )
 31+ throw new DBConnectionError( $this, "SQLite database not accessible" );
 32+ $this->mName = $dbName;
 33+ $this->open( $server, $user, $password, $dbName );
 34+ }
 35+
 36+ function getType() {
 37+ return 'sqlite';
 38+ }
 39+
 40+ /**
 41+ * @todo: check if it should be true like parent class
 42+ */
 43+ function implicitGroupby() { return false; }
 44+
 45+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) {
 46+ return new DatabaseSqlite( $server, $user, $password, $dbName, $failFunction, $flags );
 47+ }
 48+
 49+ /** Open an SQLite database and return a resource handle to it
 50+ * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
 51+ */
 52+ function open( $server, $user, $pass, $dbName ) {
 53+ $this->mConn = false;
 54+ if ( $dbName ) {
 55+ $file = $this->mDatabaseFile;
 56+ try {
 57+ if ( $this->mFlags & DBO_PERSISTENT ) {
 58+ $this->mConn = new PDO( "sqlite:$file", $user, $pass,
 59+ array( PDO::ATTR_PERSISTENT => true ) );
 60+ } else {
 61+ $this->mConn = new PDO( "sqlite:$file", $user, $pass );
 62+ }
 63+ } catch ( PDOException $e ) {
 64+ $err = $e->getMessage();
 65+ }
 66+ if ( $this->mConn === false ) {
 67+ wfDebug( "DB connection error: $err\n" );
 68+ if ( !$this->mFailFunction ) {
 69+ throw new DBConnectionError( $this, $err );
 70+ } else {
 71+ return false;
 72+ }
 73+
 74+ }
 75+ $this->mOpened = $this->mConn;
 76+ # set error codes only, don't raise exceptions
 77+ $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
 78+ }
 79+ return $this->mConn;
 80+ }
 81+
 82+ /**
 83+ * Close an SQLite database
 84+ */
 85+ function close() {
 86+ $this->mOpened = false;
 87+ if ( is_object( $this->mConn ) ) {
 88+ if ( $this->trxLevel() ) $this->commit();
 89+ $this->mConn = null;
 90+ }
 91+ return true;
 92+ }
 93+
 94+ /**
 95+ * Generates a database file name. Explicitly public for installer.
 96+ * @param $dir String: Directory where database resides
 97+ * @param $dbName String: Database name
 98+ * @return String
 99+ */
 100+ public static function generateFileName( $dir, $dbName ) {
 101+ return "$dir/$dbName.sqlite";
 102+ }
 103+
 104+ /**
 105+ * Returns version of currently supported SQLite fulltext search module or false if none present.
 106+ * @return String
 107+ */
 108+ function getFulltextSearchModule() {
 109+ $table = 'dummy_search_test';
 110+ $this->query( "DROP TABLE IF EXISTS $table", __METHOD__ );
 111+ if ( $this->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
 112+ $this->query( "DROP TABLE IF EXISTS $table", __METHOD__ );
 113+ return 'FTS3';
 114+ }
 115+ return false;
 116+ }
 117+
 118+ /**
 119+ * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
 120+ */
 121+ function doQuery( $sql ) {
 122+ $res = $this->mConn->query( $sql );
 123+ if ( $res === false ) {
 124+ return false;
 125+ } else {
 126+ $r = $res instanceof ResultWrapper ? $res->result : $res;
 127+ $this->mAffectedRows = $r->rowCount();
 128+ $res = new ResultWrapper( $this, $r->fetchAll() );
 129+ }
 130+ return $res;
 131+ }
 132+
 133+ function freeResult( $res ) {
 134+ if ( $res instanceof ResultWrapper )
 135+ $res->result = null;
 136+ else
 137+ $res = null;
 138+ }
 139+
 140+ function fetchObject($res) {
 141+ if ($res instanceof ResultWrapper)
 142+ $r =& $res->result;
 143+ else
 144+ $r =& $res;
 145+
 146+ $cur = current( $r );
 147+ if ( is_array( $cur ) ) {
 148+ next( $r );
 149+ $obj = new stdClass;
 150+ foreach ( $cur as $k => $v )
 151+ if ( !is_numeric( $k ) )
 152+ $obj->$k = $v;
 153+
 154+ return $obj;
 155+ }
 156+ return false;
 157+ }
 158+
 159+ function fetchRow($res) {
 160+ if ( $res instanceof ResultWrapper )
 161+ $r =& $res->result;
 162+ else
 163+ $r =& $res;
 164+
 165+ $cur = current($r);
 166+ if (is_array($cur)) {
 167+ next($r);
 168+ return $cur;
 169+ }
 170+ return false;
 171+ }
 172+
 173+ /**
 174+ * The PDO::Statement class implements the array interface so count() will work
 175+ */
 176+ function numRows( $res ) {
 177+ $r = $res instanceof ResultWrapper ? $res->result : $res;
 178+ return count( $r );
 179+ }
 180+
 181+ function numFields( $res ) {
 182+ $r = $res instanceof ResultWrapper ? $res->result : $res;
 183+ return is_array( $r ) ? count( $r[0] ) : 0;
 184+ }
 185+
 186+ function fieldName( $res, $n ) {
 187+ $r = $res instanceof ResultWrapper ? $res->result : $res;
 188+ if ( is_array( $r ) ) {
 189+ $keys = array_keys( $r[0] );
 190+ return $keys[$n];
 191+ }
 192+ return false;
 193+ }
 194+
 195+ /**
 196+ * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
 197+ */
 198+ function tableName( $name ) {
 199+ return str_replace( '`', '', parent::tableName( $name ) );
 200+ }
 201+
 202+ /**
 203+ * Index names have DB scope
 204+ */
 205+ function indexName( $index ) {
 206+ return $index;
 207+ }
 208+
 209+ /**
 210+ * This must be called after nextSequenceVal
 211+ */
 212+ function insertId() {
 213+ return $this->mConn->lastInsertId();
 214+ }
 215+
 216+ function dataSeek( $res, $row ) {
 217+ if ( $res instanceof ResultWrapper )
 218+ $r =& $res->result;
 219+ else
 220+ $r =& $res;
 221+ reset( $r );
 222+ if ( $row > 0 )
 223+ for ( $i = 0; $i < $row; $i++ )
 224+ next( $r );
 225+ }
 226+
 227+ function lastError() {
 228+ if ( !is_object( $this->mConn ) )
 229+ return "Cannot return last error, no db connection";
 230+ $e = $this->mConn->errorInfo();
 231+ return isset( $e[2] ) ? $e[2] : '';
 232+ }
 233+
 234+ function lastErrno() {
 235+ if ( !is_object( $this->mConn ) ) {
 236+ return "Cannot return last error, no db connection";
 237+ } else {
 238+ $info = $this->mConn->errorInfo();
 239+ return $info[1];
 240+ }
 241+ }
 242+
 243+ function affectedRows() {
 244+ return $this->mAffectedRows;
 245+ }
 246+
 247+ /**
 248+ * Returns information about an index
 249+ * Returns false if the index does not exist
 250+ * - if errors are explicitly ignored, returns NULL on failure
 251+ */
 252+ function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) {
 253+ $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
 254+ $res = $this->query( $sql, $fname );
 255+ if ( !$res ) {
 256+ return null;
 257+ }
 258+ if ( $res->numRows() == 0 ) {
 259+ return false;
 260+ }
 261+ $info = array();
 262+ foreach ( $res as $row ) {
 263+ $info[] = $row->name;
 264+ }
 265+ return $info;
 266+ }
 267+
 268+ function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) {
 269+ $row = $this->selectRow( 'sqlite_master', '*',
 270+ array(
 271+ 'type' => 'index',
 272+ 'name' => $this->indexName( $index ),
 273+ ), $fname );
 274+ if ( !$row || !isset( $row->sql ) ) {
 275+ return null;
 276+ }
 277+
 278+ // $row->sql will be of the form CREATE [UNIQUE] INDEX ...
 279+ $indexPos = strpos( $row->sql, 'INDEX' );
 280+ if ( $indexPos === false ) {
 281+ return null;
 282+ }
 283+ $firstPart = substr( $row->sql, 0, $indexPos );
 284+ $options = explode( ' ', $firstPart );
 285+ return in_array( 'UNIQUE', $options );
 286+ }
 287+
 288+ /**
 289+ * Filter the options used in SELECT statements
 290+ */
 291+ function makeSelectOptions( $options ) {
 292+ foreach ( $options as $k => $v )
 293+ if ( is_numeric( $k ) && $v == 'FOR UPDATE' )
 294+ $options[$k] = '';
 295+ return parent::makeSelectOptions( $options );
 296+ }
 297+
 298+ /**
 299+ * Based on MySQL method (parent) with some prior SQLite-sepcific adjustments
 300+ */
 301+ function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) {
 302+ if ( !count( $a ) ) return true;
 303+ if ( !is_array( $options ) ) $options = array( $options );
 304+
 305+ # SQLite uses OR IGNORE not just IGNORE
 306+ foreach ( $options as $k => $v )
 307+ if ( $v == 'IGNORE' )
 308+ $options[$k] = 'OR IGNORE';
 309+
 310+ # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
 311+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
 312+ $ret = true;
 313+ foreach ( $a as $k => $v )
 314+ if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) )
 315+ $ret = false;
 316+ } else {
 317+ $ret = parent::insert( $table, $a, "$fname/single-row", $options );
 318+ }
 319+
 320+ return $ret;
 321+ }
 322+
 323+ /**
 324+ * Returns the size of a text field, or -1 for "unlimited"
 325+ * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though.
 326+ */
 327+ function textFieldSize( $table, $field ) {
 328+ return - 1;
 329+ }
 330+
 331+ function unionSupportsOrderAndLimit() {
 332+ return false;
 333+ }
 334+
 335+ function unionQueries( $sqls, $all ) {
 336+ $glue = $all ? ' UNION ALL ' : ' UNION ';
 337+ return implode( $glue, $sqls );
 338+ }
 339+
 340+ function wasDeadlock() {
 341+ return $this->lastErrno() == 5; // SQLITE_BUSY
 342+ }
 343+
 344+ function wasErrorReissuable() {
 345+ return $this->lastErrno() == 17; // SQLITE_SCHEMA;
 346+ }
 347+
 348+ function wasReadOnlyError() {
 349+ return $this->lastErrno() == 8; // SQLITE_READONLY;
 350+ }
 351+
 352+ /**
 353+ * @return string wikitext of a link to the server software's web site
 354+ */
 355+ function getSoftwareLink() {
 356+ return "[http://sqlite.org/ SQLite]";
 357+ }
 358+
 359+ /**
 360+ * @return string Version information from the database
 361+ */
 362+ function getServerVersion() {
 363+ $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
 364+ return $ver;
 365+ }
 366+
 367+ /**
 368+ * Query whether a given column exists in the mediawiki schema
 369+ */
 370+ function fieldExists( $table, $field, $fname = '' ) {
 371+ $info = $this->fieldInfo( $table, $field );
 372+ return (bool)$info;
 373+ }
 374+
 375+ /**
 376+ * Get information about a given field
 377+ * Returns false if the field does not exist.
 378+ */
 379+ function fieldInfo( $table, $field ) {
 380+ $tableName = $this->tableName( $table );
 381+ $sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
 382+ $res = $this->query( $sql, __METHOD__ );
 383+ foreach ( $res as $row ) {
 384+ if ( $row->name == $field ) {
 385+ return new SQLiteField( $row, $tableName );
 386+ }
 387+ }
 388+ return false;
 389+ }
 390+
 391+ function begin( $fname = '' ) {
 392+ if ( $this->mTrxLevel == 1 ) $this->commit();
 393+ $this->mConn->beginTransaction();
 394+ $this->mTrxLevel = 1;
 395+ }
 396+
 397+ function commit( $fname = '' ) {
 398+ if ( $this->mTrxLevel == 0 ) return;
 399+ $this->mConn->commit();
 400+ $this->mTrxLevel = 0;
 401+ }
 402+
 403+ function rollback( $fname = '' ) {
 404+ if ( $this->mTrxLevel == 0 ) return;
 405+ $this->mConn->rollBack();
 406+ $this->mTrxLevel = 0;
 407+ }
 408+
 409+ function limitResultForUpdate( $sql, $num ) {
 410+ return $this->limitResult( $sql, $num );
 411+ }
 412+
 413+ function strencode( $s ) {
 414+ return substr( $this->addQuotes( $s ), 1, - 1 );
 415+ }
 416+
 417+ function encodeBlob( $b ) {
 418+ return new Blob( $b );
 419+ }
 420+
 421+ function decodeBlob( $b ) {
 422+ if ( $b instanceof Blob ) {
 423+ $b = $b->fetch();
 424+ }
 425+ return $b;
 426+ }
 427+
 428+ function addQuotes( $s ) {
 429+ if ( $s instanceof Blob ) {
 430+ return "x'" . bin2hex( $s->fetch() ) . "'";
 431+ } else {
 432+ return $this->mConn->quote( $s );
 433+ }
 434+ }
 435+
 436+ function quote_ident( $s ) {
 437+ return $s;
 438+ }
 439+
 440+ function buildLike() {
 441+ $params = func_get_args();
 442+ if ( count( $params ) > 0 && is_array( $params[0] ) ) {
 443+ $params = $params[0];
 444+ }
 445+ return parent::buildLike( $params ) . "ESCAPE '\' ";
 446+ }
 447+
 448+ /**
 449+ * How lagged is this slave?
 450+ */
 451+ public function getLag() {
 452+ return 0;
 453+ }
 454+
 455+ /**
 456+ * Called by the installer script (when modified according to the MediaWikiLite installation instructions)
 457+ * - this is the same way PostgreSQL works, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
 458+ */
 459+ public function setup_database() {
 460+ global $IP;
 461+
 462+ # Process common MySQL/SQLite table definitions
 463+ $err = $this->sourceFile( "$IP/maintenance/tables.sql" );
 464+ if ( $err !== true ) {
 465+ $this->reportQueryError( $err, 0, $sql, __FUNCTION__ );
 466+ exit( 1 );
 467+ }
 468+
 469+ # Use DatabasePostgres's code to populate interwiki from MySQL template
 470+ $f = fopen( "$IP/maintenance/interwiki.sql", 'r' );
 471+ if ( $f == false ) dieout( "<li>Could not find the interwiki.sql file" );
 472+ $sql = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
 473+ while ( !feof( $f ) ) {
 474+ $line = fgets( $f, 1024 );
 475+ $matches = array();
 476+ if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) continue;
 477+ $this->query( "$sql $matches[1],$matches[2])" );
 478+ }
 479+ }
 480+
 481+ public function getSearchEngine() {
 482+ return "SearchSqlite";
 483+ }
 484+
 485+ /**
 486+ * No-op version of deadlockLoop
 487+ */
 488+ public function deadlockLoop( /*...*/ ) {
 489+ $args = func_get_args();
 490+ $function = array_shift( $args );
 491+ return call_user_func_array( $function, $args );
 492+ }
 493+
 494+ protected function replaceVars( $s ) {
 495+ $s = parent::replaceVars( $s );
 496+ if ( preg_match( '/^\s*CREATE TABLE/i', $s ) ) {
 497+ // CREATE TABLE hacks to allow schema file sharing with MySQL
 498+
 499+ // binary/varbinary column type -> blob
 500+ $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'blob\1', $s );
 501+ // no such thing as unsigned
 502+ $s = preg_replace( '/\bunsigned\b/i', '', $s );
 503+ // INT -> INTEGER for primary keys
 504+ $s = preg_replacE( '/\bint\b/i', 'integer', $s );
 505+ // No ENUM type
 506+ $s = preg_replace( '/enum\([^)]*\)/i', 'blob', $s );
 507+ // binary collation type -> nothing
 508+ $s = preg_replace( '/\bbinary\b/i', '', $s );
 509+ // auto_increment -> autoincrement
 510+ $s = preg_replace( '/\bauto_increment\b/i', 'autoincrement', $s );
 511+ // No explicit options
 512+ $s = preg_replace( '/\)[^)]*$/', ')', $s );
 513+ } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
 514+ // No truncated indexes
 515+ $s = preg_replace( '/\(\d+\)/', '', $s );
 516+ // No FULLTEXT
 517+ $s = preg_replace( '/\bfulltext\b/i', '', $s );
 518+ }
 519+ return $s;
 520+ }
 521+
 522+ /*
 523+ * Build a concatenation list to feed into a SQL query
 524+ */
 525+ function buildConcat( $stringList ) {
 526+ return '(' . implode( ') || (', $stringList ) . ')';
 527+ }
 528+
 529+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) {
 530+ $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name='$oldName' AND type='table'", $fname );
 531+ $sql = $this->fetchObject( $res )->sql;
 532+ $sql = preg_replace( '/\b' . preg_quote( $oldName ) . '\b/', $newName, $sql, 1 );
 533+ return $this->query( $sql, $fname );
 534+ }
 535+
 536+} // end DatabaseSqlite class
 537+
 538+/**
 539+ * @ingroup Database
 540+ */
 541+class SQLiteField {
 542+ private $info, $tableName;
 543+ function __construct( $info, $tableName ) {
 544+ $this->info = $info;
 545+ $this->tableName = $tableName;
 546+ }
 547+
 548+ function name() {
 549+ return $this->info->name;
 550+ }
 551+
 552+ function tableName() {
 553+ return $this->tableName;
 554+ }
 555+
 556+ function defaultValue() {
 557+ if ( is_string( $this->info->dflt_value ) ) {
 558+ // Typically quoted
 559+ if ( preg_match( '/^\'(.*)\'$', $this->info->dflt_value ) ) {
 560+ return str_replace( "''", "'", $this->info->dflt_value );
 561+ }
 562+ }
 563+ return $this->info->dflt_value;
 564+ }
 565+
 566+ function maxLength() {
 567+ return -1;
 568+ }
 569+
 570+ function nullable() {
 571+ // SQLite dynamic types are always nullable
 572+ return true;
 573+ }
 574+
 575+ # isKey(), isMultipleKey() not implemented, MySQL-specific concept.
 576+ # Suggest removal from base class [TS]
 577+
 578+ function type() {
 579+ return $this->info->type;
 580+ }
 581+
 582+} // end SQLiteField
Property changes on: trunk/extensions/MSSQLBackCompat/db/DatabaseSqlite.php
___________________________________________________________________
Added: svn:eol-style
1583 + native
Index: trunk/extensions/MSSQLBackCompat/db/LoadBalancer.php
@@ -0,0 +1,919 @@
 2+<?php
 3+/**
 4+ * @file
 5+ * @ingroup Database
 6+ */
 7+
 8+/**
 9+ * Database load balancing object
 10+ *
 11+ * @todo document
 12+ * @ingroup Database
 13+ */
 14+class LoadBalancer {
 15+ /* private */ var $mServers, $mConns, $mLoads, $mGroupLoads;
 16+ /* private */ var $mFailFunction, $mErrorConnection;
 17+ /* private */ var $mReadIndex, $mAllowLagged;
 18+ /* private */ var $mWaitForPos, $mWaitTimeout;
 19+ /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
 20+ /* private */ var $mParentInfo, $mLagTimes;
 21+ /* private */ var $mLoadMonitorClass, $mLoadMonitor;
 22+
 23+ /**
 24+ * @param array $params Array with keys:
 25+ * servers Required. Array of server info structures.
 26+ * failFunction Deprecated, use exceptions instead.
 27+ * masterWaitTimeout Replication lag wait timeout
 28+ * loadMonitor Name of a class used to fetch server lag and load.
 29+ */
 30+ function __construct( $params )
 31+ {
 32+ if ( !isset( $params['servers'] ) ) {
 33+ throw new MWException( __CLASS__.': missing servers parameter' );
 34+ }
 35+ $this->mServers = $params['servers'];
 36+
 37+ if ( isset( $params['failFunction'] ) ) {
 38+ $this->mFailFunction = $params['failFunction'];
 39+ } else {
 40+ $this->mFailFunction = false;
 41+ }
 42+ if ( isset( $params['waitTimeout'] ) ) {
 43+ $this->mWaitTimeout = $params['waitTimeout'];
 44+ } else {
 45+ $this->mWaitTimeout = 10;
 46+ }
 47+
 48+ $this->mReadIndex = -1;
 49+ $this->mWriteIndex = -1;
 50+ $this->mConns = array(
 51+ 'local' => array(),
 52+ 'foreignUsed' => array(),
 53+ 'foreignFree' => array() );
 54+ $this->mLoads = array();
 55+ $this->mWaitForPos = false;
 56+ $this->mLaggedSlaveMode = false;
 57+ $this->mErrorConnection = false;
 58+ $this->mAllowLag = false;
 59+ $this->mLoadMonitorClass = isset( $params['loadMonitor'] )
 60+ ? $params['loadMonitor'] : 'LoadMonitor_MySQL';
 61+
 62+ foreach( $params['servers'] as $i => $server ) {
 63+ $this->mLoads[$i] = $server['load'];
 64+ if ( isset( $server['groupLoads'] ) ) {
 65+ foreach ( $server['groupLoads'] as $group => $ratio ) {
 66+ if ( !isset( $this->mGroupLoads[$group] ) ) {
 67+ $this->mGroupLoads[$group] = array();
 68+ }
 69+ $this->mGroupLoads[$group][$i] = $ratio;
 70+ }
 71+ }
 72+ }
 73+ }
 74+
 75+ static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 )
 76+ {
 77+ return new LoadBalancer( $servers, $failFunction, $waitTimeout );
 78+ }
 79+
 80+ /**
 81+ * Get a LoadMonitor instance
 82+ */
 83+ function getLoadMonitor() {
 84+ if ( !isset( $this->mLoadMonitor ) ) {
 85+ $class = $this->mLoadMonitorClass;
 86+ $this->mLoadMonitor = new $class( $this );
 87+ }
 88+ return $this->mLoadMonitor;
 89+ }
 90+
 91+ /**
 92+ * Get or set arbitrary data used by the parent object, usually an LBFactory
 93+ */
 94+ function parentInfo( $x = null ) {
 95+ return wfSetVar( $this->mParentInfo, $x );
 96+ }
 97+
 98+ /**
 99+ * Given an array of non-normalised probabilities, this function will select
 100+ * an element and return the appropriate key
 101+ */
 102+ function pickRandom( $weights )
 103+ {
 104+ if ( !is_array( $weights ) || count( $weights ) == 0 ) {
 105+ return false;
 106+ }
 107+
 108+ $sum = array_sum( $weights );
 109+ if ( $sum == 0 ) {
 110+ # No loads on any of them
 111+ # In previous versions, this triggered an unweighted random selection,
 112+ # but this feature has been removed as of April 2006 to allow for strict
 113+ # separation of query groups.
 114+ return false;
 115+ }
 116+ $max = mt_getrandmax();
 117+ $rand = mt_rand(0, $max) / $max * $sum;
 118+
 119+ $sum = 0;
 120+ foreach ( $weights as $i => $w ) {
 121+ $sum += $w;
 122+ if ( $sum >= $rand ) {
 123+ break;
 124+ }
 125+ }
 126+ return $i;
 127+ }
 128+
 129+ function getRandomNonLagged( $loads, $wiki = false ) {
 130+ # Unset excessively lagged servers
 131+ $lags = $this->getLagTimes( $wiki );
 132+ foreach ( $lags as $i => $lag ) {
 133+ if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) ) {
 134+ if ( $lag === false ) {
 135+ wfDebug( "Server #$i is not replicating\n" );
 136+ unset( $loads[$i] );
 137+ } elseif ( $lag > $this->mServers[$i]['max lag'] ) {
 138+ wfDebug( "Server #$i is excessively lagged ($lag seconds)\n" );
 139+ unset( $loads[$i] );
 140+ }
 141+ }
 142+ }
 143+
 144+ # Find out if all the slaves with non-zero load are lagged
 145+ $sum = 0;
 146+ foreach ( $loads as $load ) {
 147+ $sum += $load;
 148+ }
 149+ if ( $sum == 0 ) {
 150+ # No appropriate DB servers except maybe the master and some slaves with zero load
 151+ # Do NOT use the master
 152+ # Instead, this function will return false, triggering read-only mode,
 153+ # and a lagged slave will be used instead.
 154+ return false;
 155+ }
 156+
 157+ if ( count( $loads ) == 0 ) {
 158+ return false;
 159+ }
 160+
 161+ #wfDebugLog( 'connect', var_export( $loads, true ) );
 162+
 163+ # Return a random representative of the remainder
 164+ return $this->pickRandom( $loads );
 165+ }
 166+
 167+ /**
 168+ * Get the index of the reader connection, which may be a slave
 169+ * This takes into account load ratios and lag times. It should
 170+ * always return a consistent index during a given invocation
 171+ *
 172+ * Side effect: opens connections to databases
 173+ */
 174+ function getReaderIndex( $group = false, $wiki = false ) {
 175+ global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype;
 176+
 177+ # FIXME: For now, only go through all this for mysql databases
 178+ if ($wgDBtype != 'mysql') {
 179+ return $this->getWriterIndex();
 180+ }
 181+
 182+ if ( count( $this->mServers ) == 1 ) {
 183+ # Skip the load balancing if there's only one server
 184+ return 0;
 185+ } elseif ( $group === false and $this->mReadIndex >= 0 ) {
 186+ # Shortcut if generic reader exists already
 187+ return $this->mReadIndex;
 188+ }
 189+
 190+ wfProfileIn( __METHOD__ );
 191+
 192+ $totalElapsed = 0;
 193+
 194+ # convert from seconds to microseconds
 195+ $timeout = $wgDBClusterTimeout * 1e6;
 196+
 197+ # Find the relevant load array
 198+ if ( $group !== false ) {
 199+ if ( isset( $this->mGroupLoads[$group] ) ) {
 200+ $nonErrorLoads = $this->mGroupLoads[$group];
 201+ } else {
 202+ # No loads for this group, return false and the caller can use some other group
 203+ wfDebug( __METHOD__.": no loads for group $group\n" );
 204+ wfProfileOut( __METHOD__ );
 205+ return false;
 206+ }
 207+ } else {
 208+ $nonErrorLoads = $this->mLoads;
 209+ }
 210+
 211+ if ( !$nonErrorLoads ) {
 212+ throw new MWException( "Empty server array given to LoadBalancer" );
 213+ }
 214+
 215+ # Scale the configured load ratios according to the dynamic load (if the load monitor supports it)
 216+ $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $wiki );
 217+
 218+ $i = false;
 219+ $found = false;
 220+ $laggedSlaveMode = false;
 221+
 222+ # First try quickly looking through the available servers for a server that
 223+ # meets our criteria
 224+ do {
 225+ $totalThreadsConnected = 0;
 226+ $overloadedServers = 0;
 227+ $currentLoads = $nonErrorLoads;
 228+ while ( count( $currentLoads ) ) {
 229+ if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) {
 230+ $i = $this->pickRandom( $currentLoads );
 231+ } else {
 232+ $i = $this->getRandomNonLagged( $currentLoads, $wiki );
 233+ if ( $i === false && count( $currentLoads ) != 0 ) {
 234+ # All slaves lagged. Switch to read-only mode
 235+ $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' );
 236+ $i = $this->pickRandom( $currentLoads );
 237+ $laggedSlaveMode = true;
 238+ }
 239+ }
 240+
 241+ if ( $i === false ) {
 242+ # pickRandom() returned false
 243+ # This is permanent and means the configuration or the load monitor
 244+ # wants us to return false.
 245+ wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" );
 246+ wfProfileOut( __METHOD__ );
 247+ return false;
 248+ }
 249+
 250+ wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
 251+ $conn = $this->openConnection( $i, $wiki );
 252+
 253+ if ( !$conn ) {
 254+ wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" );
 255+ unset( $nonErrorLoads[$i] );
 256+ unset( $currentLoads[$i] );
 257+ continue;
 258+ }
 259+
 260+ // Perform post-connection backoff
 261+ $threshold = isset( $this->mServers[$i]['max threads'] )
 262+ ? $this->mServers[$i]['max threads'] : false;
 263+ $backoff = $this->getLoadMonitor()->postConnectionBackoff( $conn, $threshold );
 264+
 265+ // Decrement reference counter, we are finished with this connection.
 266+ // It will be incremented for the caller later.
 267+ if ( $wiki !== false ) {
 268+ $this->reuseConnection( $conn );
 269+ }
 270+
 271+ if ( $backoff ) {
 272+ # Post-connection overload, don't use this server for now
 273+ $totalThreadsConnected += $backoff;
 274+ $overloadedServers++;
 275+ unset( $currentLoads[$i] );
 276+ } else {
 277+ # Return this server
 278+ break 2;
 279+ }
 280+ }
 281+
 282+ # No server found yet
 283+ $i = false;
 284+
 285+ # If all servers were down, quit now
 286+ if ( !count( $nonErrorLoads ) ) {
 287+ wfDebugLog( 'connect', "All servers down\n" );
 288+ break;
 289+ }
 290+
 291+ # Some servers must have been overloaded
 292+ if ( $overloadedServers == 0 ) {
 293+ throw new MWException( __METHOD__.": unexpectedly found no overloaded servers" );
 294+ }
 295+ # Back off for a while
 296+ # Scale the sleep time by the number of connected threads, to produce a
 297+ # roughly constant global poll rate
 298+ $avgThreads = $totalThreadsConnected / $overloadedServers;
 299+ $totalElapsed += $this->sleep( $wgDBAvgStatusPoll * $avgThreads );
 300+ } while ( $totalElapsed < $timeout );
 301+
 302+ if ( $totalElapsed >= $timeout ) {
 303+ wfDebugLog( 'connect', "All servers busy\n" );
 304+ $this->mErrorConnection = false;
 305+ $this->mLastError = 'All servers busy';
 306+ }
 307+
 308+ if ( $i !== false ) {
 309+ # Slave connection successful
 310+ # Wait for the session master pos for a short time
 311+ if ( $this->mWaitForPos && $i > 0 ) {
 312+ if ( !$this->doWait( $i ) ) {
 313+ $this->mServers[$i]['slave pos'] = $conn->getSlavePos();
 314+ }
 315+ }
 316+ if ( $this->mReadIndex <=0 && $this->mLoads[$i]>0 && $i !== false ) {
 317+ $this->mReadIndex = $i;
 318+ }
 319+ }
 320+ wfProfileOut( __METHOD__ );
 321+ return $i;
 322+ }
 323+
 324+ /**
 325+ * Wait for a specified number of microseconds, and return the period waited
 326+ */
 327+ function sleep( $t ) {
 328+ wfProfileIn( __METHOD__ );
 329+ wfDebug( __METHOD__.": waiting $t us\n" );
 330+ usleep( $t );
 331+ wfProfileOut( __METHOD__ );
 332+ return $t;
 333+ }
 334+
 335+ /**
 336+ * Get a random server to use in a query group
 337+ * @deprecated use getReaderIndex
 338+ */
 339+ function getGroupIndex( $group ) {
 340+ return $this->getReaderIndex( $group );
 341+ }
 342+
 343+ /**
 344+ * Set the master wait position
 345+ * If a DB_SLAVE connection has been opened already, waits
 346+ * Otherwise sets a variable telling it to wait if such a connection is opened
 347+ */
 348+ public function waitFor( $pos ) {
 349+ wfProfileIn( __METHOD__ );
 350+ $this->mWaitForPos = $pos;
 351+ $i = $this->mReadIndex;
 352+
 353+ if ( $i > 0 ) {
 354+ if ( !$this->doWait( $i ) ) {
 355+ $this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
 356+ $this->mLaggedSlaveMode = true;
 357+ }
 358+ }
 359+ wfProfileOut( __METHOD__ );
 360+ }
 361+
 362+ /**
 363+ * Get any open connection to a given server index, local or foreign
 364+ * Returns false if there is no connection open
 365+ */
 366+ function getAnyOpenConnection( $i ) {
 367+ foreach ( $this->mConns as $type => $conns ) {
 368+ if ( !empty( $conns[$i] ) ) {
 369+ return reset( $conns[$i] );
 370+ }
 371+ }
 372+ return false;
 373+ }
 374+
 375+ /**
 376+ * Wait for a given slave to catch up to the master pos stored in $this
 377+ */
 378+ function doWait( $index ) {
 379+ # Find a connection to wait on
 380+ $conn = $this->getAnyOpenConnection( $index );
 381+ if ( !$conn ) {
 382+ wfDebug( __METHOD__ . ": no connection open\n" );
 383+ return false;
 384+ }
 385+
 386+ wfDebug( __METHOD__.": Waiting for slave #$index to catch up...\n" );
 387+ $result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout );
 388+
 389+ if ( $result == -1 || is_null( $result ) ) {
 390+ # Timed out waiting for slave, use master instead
 391+ wfDebug( __METHOD__.": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" );
 392+ return false;
 393+ } else {
 394+ wfDebug( __METHOD__.": Done\n" );
 395+ return true;
 396+ }
 397+ }
 398+
 399+ /**
 400+ * Get a connection by index
 401+ * This is the main entry point for this class.
 402+ * @param int $i Database
 403+ * @param array $groups Query groups
 404+ * @param string $wiki Wiki ID
 405+ */
 406+ public function &getConnection( $i, $groups = array(), $wiki = false ) {
 407+ global $wgDBtype;
 408+ wfProfileIn( __METHOD__ );
 409+
 410+ if ( $i == DB_LAST ) {
 411+ throw new MWException( 'Attempt to call ' . __METHOD__ . ' with deprecated server index DB_LAST' );
 412+ } elseif ( $i === null || $i === false ) {
 413+ throw new MWException( 'Attempt to call ' . __METHOD__ . ' with invalid server index' );
 414+ }
 415+
 416+ if ( $wiki === wfWikiID() ) {
 417+ $wiki = false;
 418+ }
 419+
 420+ # Query groups
 421+ if ( $i == DB_MASTER ) {
 422+ $i = $this->getWriterIndex();
 423+ } elseif ( !is_array( $groups ) ) {
 424+ $groupIndex = $this->getReaderIndex( $groups, $wiki );
 425+ if ( $groupIndex !== false ) {
 426+ $serverName = $this->getServerName( $groupIndex );
 427+ wfDebug( __METHOD__.": using server $serverName for group $groups\n" );
 428+ $i = $groupIndex;
 429+ }
 430+ } else {
 431+ foreach ( $groups as $group ) {
 432+ $groupIndex = $this->getReaderIndex( $group, $wiki );
 433+ if ( $groupIndex !== false ) {
 434+ $serverName = $this->getServerName( $groupIndex );
 435+ wfDebug( __METHOD__.": using server $serverName for group $group\n" );
 436+ $i = $groupIndex;
 437+ break;
 438+ }
 439+ }
 440+ }
 441+
 442+ # Operation-based index
 443+ if ( $i == DB_SLAVE ) {
 444+ $i = $this->getReaderIndex( false, $wiki );
 445+ # Couldn't find a working server in getReaderIndex()?
 446+ if ( $i === false ) {
 447+ $this->mLastError = 'No working slave server: ' . $this->mLastError;
 448+ $this->reportConnectionError( $this->mErrorConnection );
 449+ return false;
 450+ }
 451+ }
 452+
 453+ # Now we have an explicit index into the servers array
 454+ $conn = $this->openConnection( $i, $wiki );
 455+ if ( !$conn ) {
 456+ $this->reportConnectionError( $this->mErrorConnection );
 457+ }
 458+
 459+ wfProfileOut( __METHOD__ );
 460+ return $conn;
 461+ }
 462+
 463+ /**
 464+ * Mark a foreign connection as being available for reuse under a different
 465+ * DB name or prefix. This mechanism is reference-counted, and must be called
 466+ * the same number of times as getConnection() to work.
 467+ */
 468+ public function reuseConnection( $conn ) {
 469+ $serverIndex = $conn->getLBInfo('serverIndex');
 470+ $refCount = $conn->getLBInfo('foreignPoolRefCount');
 471+ $dbName = $conn->getDBname();
 472+ $prefix = $conn->tablePrefix();
 473+ if ( strval( $prefix ) !== '' ) {
 474+ $wiki = "$dbName-$prefix";
 475+ } else {
 476+ $wiki = $dbName;
 477+ }
 478+ if ( $serverIndex === null || $refCount === null ) {
 479+ wfDebug( __METHOD__.": this connection was not opened as a foreign connection\n" );
 480+ /**
 481+ * This can happen in code like:
 482+ * foreach ( $dbs as $db ) {
 483+ * $conn = $lb->getConnection( DB_SLAVE, array(), $db );
 484+ * ...
 485+ * $lb->reuseConnection( $conn );
 486+ * }
 487+ * When a connection to the local DB is opened in this way, reuseConnection()
 488+ * should be ignored
 489+ */
 490+ return;
 491+ }
 492+ if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
 493+ throw new MWException( __METHOD__.": connection not found, has the connection been freed already?" );
 494+ }
 495+ $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
 496+ if ( $refCount <= 0 ) {
 497+ $this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
 498+ unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
 499+ wfDebug( __METHOD__.": freed connection $serverIndex/$wiki\n" );
 500+ } else {
 501+ wfDebug( __METHOD__.": reference count for $serverIndex/$wiki reduced to $refCount\n" );
 502+ }
 503+ }
 504+
 505+ /**
 506+ * Open a connection to the server given by the specified index
 507+ * Index must be an actual index into the array.
 508+ * If the server is already open, returns it.
 509+ *
 510+ * On error, returns false, and the connection which caused the
 511+ * error will be available via $this->mErrorConnection.
 512+ *
 513+ * @param integer $i Server index
 514+ * @param string $wiki Wiki ID to open
 515+ * @return Database
 516+ *
 517+ * @access private
 518+ */
 519+ function openConnection( $i, $wiki = false ) {
 520+ wfProfileIn( __METHOD__ );
 521+ if ( $wiki !== false ) {
 522+ $conn = $this->openForeignConnection( $i, $wiki );
 523+ wfProfileOut( __METHOD__);
 524+ return $conn;
 525+ }
 526+ if ( isset( $this->mConns['local'][$i][0] ) ) {
 527+ $conn = $this->mConns['local'][$i][0];
 528+ } else {
 529+ $server = $this->mServers[$i];
 530+ $server['serverIndex'] = $i;
 531+ $conn = $this->reallyOpenConnection( $server, false );
 532+ if ( $conn->isOpen() ) {
 533+ $this->mConns['local'][$i][0] = $conn;
 534+ } else {
 535+ wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
 536+ $this->mErrorConnection = $conn;
 537+ $conn = false;
 538+ }
 539+ }
 540+ wfProfileOut( __METHOD__ );
 541+ return $conn;
 542+ }
 543+
 544+ /**
 545+ * Open a connection to a foreign DB, or return one if it is already open.
 546+ *
 547+ * Increments a reference count on the returned connection which locks the
 548+ * connection to the requested wiki. This reference count can be
 549+ * decremented by calling reuseConnection().
 550+ *
 551+ * If a connection is open to the appropriate server already, but with the wrong
 552+ * database, it will be switched to the right database and returned, as long as
 553+ * it has been freed first with reuseConnection().
 554+ *
 555+ * On error, returns false, and the connection which caused the
 556+ * error will be available via $this->mErrorConnection.
 557+ *
 558+ * @param integer $i Server index
 559+ * @param string $wiki Wiki ID to open
 560+ * @return Database
 561+ */
 562+ function openForeignConnection( $i, $wiki ) {
 563+ wfProfileIn(__METHOD__);
 564+ list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
 565+ if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
 566+ // Reuse an already-used connection
 567+ $conn = $this->mConns['foreignUsed'][$i][$wiki];
 568+ wfDebug( __METHOD__.": reusing connection $i/$wiki\n" );
 569+ } elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
 570+ // Reuse a free connection for the same wiki
 571+ $conn = $this->mConns['foreignFree'][$i][$wiki];
 572+ unset( $this->mConns['foreignFree'][$i][$wiki] );
 573+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
 574+ wfDebug( __METHOD__.": reusing free connection $i/$wiki\n" );
 575+ } elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
 576+ // Reuse a connection from another wiki
 577+ $conn = reset( $this->mConns['foreignFree'][$i] );
 578+ $oldWiki = key( $this->mConns['foreignFree'][$i] );
 579+
 580+ if ( !$conn->selectDB( $dbName ) ) {
 581+ $this->mLastError = "Error selecting database $dbName on server " .
 582+ $conn->getServer() . " from client host " . wfHostname() . "\n";
 583+ $this->mErrorConnection = $conn;
 584+ $conn = false;
 585+ } else {
 586+ $conn->tablePrefix( $prefix );
 587+ unset( $this->mConns['foreignFree'][$i][$oldWiki] );
 588+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
 589+ wfDebug( __METHOD__.": reusing free connection from $oldWiki for $wiki\n" );
 590+ }
 591+ } else {
 592+ // Open a new connection
 593+ $server = $this->mServers[$i];
 594+ $server['serverIndex'] = $i;
 595+ $server['foreignPoolRefCount'] = 0;
 596+ $conn = $this->reallyOpenConnection( $server, $dbName );
 597+ if ( !$conn->isOpen() ) {
 598+ wfDebug( __METHOD__.": error opening connection for $i/$wiki\n" );
 599+ $this->mErrorConnection = $conn;
 600+ $conn = false;
 601+ } else {
 602+ $conn->tablePrefix( $prefix );
 603+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
 604+ wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" );
 605+ }
 606+ }
 607+
 608+ // Increment reference count
 609+ if ( $conn ) {
 610+ $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
 611+ $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
 612+ }
 613+ wfProfileOut(__METHOD__);
 614+ return $conn;
 615+ }
 616+
 617+ /**
 618+ * Test if the specified index represents an open connection
 619+ * @access private
 620+ */
 621+ function isOpen( $index ) {
 622+ if( !is_integer( $index ) ) {
 623+ return false;
 624+ }
 625+ return (bool)$this->getAnyOpenConnection( $index );
 626+ }
 627+
 628+ /**
 629+ * Really opens a connection. Uncached.
 630+ * Returns a Database object whether or not the connection was successful.
 631+ * @access private
 632+ */
 633+ function reallyOpenConnection( $server, $dbNameOverride = false ) {
 634+ if( !is_array( $server ) ) {
 635+ throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
 636+ }
 637+
 638+ extract( $server );
 639+ if ( $dbNameOverride !== false ) {
 640+ $dbname = $dbNameOverride;
 641+ }
 642+
 643+ # Get class for this database type
 644+ $class = 'Database' . ucfirst( $type );
 645+
 646+ # Create object
 647+ wfDebug( "Connecting to $host $dbname...\n" );
 648+ $db = new $class( $host, $user, $password, $dbname, 1, $flags );
 649+ if ( $db->isOpen() ) {
 650+ wfDebug( "Connected\n" );
 651+ } else {
 652+ wfDebug( "Failed\n" );
 653+ }
 654+ $db->setLBInfo( $server );
 655+ if ( isset( $server['fakeSlaveLag'] ) ) {
 656+ $db->setFakeSlaveLag( $server['fakeSlaveLag'] );
 657+ }
 658+ if ( isset( $server['fakeMaster'] ) ) {
 659+ $db->setFakeMaster( true );
 660+ }
 661+ return $db;
 662+ }
 663+
 664+ function reportConnectionError( &$conn ) {
 665+ wfProfileIn( __METHOD__ );
 666+
 667+ if ( !is_object( $conn ) ) {
 668+ // No last connection, probably due to all servers being too busy
 669+ wfLogDBError( "LB failure with no last connection\n" );
 670+ $conn = new Database;
 671+ if ( $this->mFailFunction ) {
 672+ $conn->failFunction( $this->mFailFunction );
 673+ $conn->reportConnectionError( $this->mLastError );
 674+ } else {
 675+ // If all servers were busy, mLastError will contain something sensible
 676+ throw new DBConnectionError( $conn, $this->mLastError );
 677+ }
 678+ } else {
 679+ if ( $this->mFailFunction ) {
 680+ $conn->failFunction( $this->mFailFunction );
 681+ } else {
 682+ $conn->failFunction( false );
 683+ }
 684+ $server = $conn->getProperty( 'mServer' );
 685+ wfLogDBError( "Connection error: {$this->mLastError} ({$server})\n" );
 686+ $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
 687+ }
 688+ wfProfileOut( __METHOD__ );
 689+ }
 690+
 691+ function getWriterIndex() {
 692+ return 0;
 693+ }
 694+
 695+ /**
 696+ * Returns true if the specified index is a valid server index
 697+ */
 698+ function haveIndex( $i ) {
 699+ return array_key_exists( $i, $this->mServers );
 700+ }
 701+
 702+ /**
 703+ * Returns true if the specified index is valid and has non-zero load
 704+ */
 705+ function isNonZeroLoad( $i ) {
 706+ return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
 707+ }
 708+
 709+ /**
 710+ * Get the number of defined servers (not the number of open connections)
 711+ */
 712+ function getServerCount() {
 713+ return count( $this->mServers );
 714+ }
 715+
 716+ /**
 717+ * Get the host name or IP address of the server with the specified index
 718+ * Prefer a readable name if available.
 719+ */
 720+ function getServerName( $i ) {
 721+ if ( isset( $this->mServers[$i]['hostName'] ) ) {
 722+ return $this->mServers[$i]['hostName'];
 723+ } elseif ( isset( $this->mServers[$i]['host'] ) ) {
 724+ return $this->mServers[$i]['host'];
 725+ } else {
 726+ return '';
 727+ }
 728+ }
 729+
 730+ /**
 731+ * Return the server info structure for a given index, or false if the index is invalid.
 732+ */
 733+ function getServerInfo( $i ) {
 734+ if ( isset( $this->mServers[$i] ) ) {
 735+ return $this->mServers[$i];
 736+ } else {
 737+ return false;
 738+ }
 739+ }
 740+
 741+ /**
 742+ * Get the current master position for chronology control purposes
 743+ * @return mixed
 744+ */
 745+ function getMasterPos() {
 746+ # If this entire request was served from a slave without opening a connection to the
 747+ # master (however unlikely that may be), then we can fetch the position from the slave.
 748+ $masterConn = $this->getAnyOpenConnection( 0 );
 749+ if ( !$masterConn ) {
 750+ for ( $i = 1; $i < count( $this->mServers ); $i++ ) {
 751+ $conn = $this->getAnyOpenConnection( $i );
 752+ if ( $conn ) {
 753+ wfDebug( "Master pos fetched from slave\n" );
 754+ return $conn->getSlavePos();
 755+ }
 756+ }
 757+ } else {
 758+ wfDebug( "Master pos fetched from master\n" );
 759+ return $masterConn->getMasterPos();
 760+ }
 761+ return false;
 762+ }
 763+
 764+ /**
 765+ * Close all open connections
 766+ */
 767+ function closeAll() {
 768+ foreach ( $this->mConns as $conns2 ) {
 769+ foreach ( $conns2 as $conns3 ) {
 770+ foreach ( $conns3 as $conn ) {
 771+ $conn->close();
 772+ }
 773+ }
 774+ }
 775+ $this->mConns = array(
 776+ 'local' => array(),
 777+ 'foreignFree' => array(),
 778+ 'foreignUsed' => array(),
 779+ );
 780+ }
 781+
 782+ /**
 783+ * Close a connection
 784+ * Using this function makes sure the LoadBalancer knows the connection is closed.
 785+ * If you use $conn->close() directly, the load balancer won't update its state.
 786+ */
 787+ function closeConnecton( $conn ) {
 788+ $done = false;
 789+ foreach ( $this->mConns as $i1 => $conns2 ) {
 790+ foreach ( $conns2 as $i2 => $conns3 ) {
 791+ foreach ( $conns3 as $i3 => $candidateConn ) {
 792+ if ( $conn === $candidateConn ) {
 793+ $conn->close();
 794+ unset( $this->mConns[$i1][$i2][$i3] );
 795+ $done = true;
 796+ break;
 797+ }
 798+ }
 799+ }
 800+ }
 801+ if ( !$done ) {
 802+ $conn->close();
 803+ }
 804+ }
 805+
 806+ /**
 807+ * Commit transactions on all open connections
 808+ */
 809+ function commitAll() {
 810+ foreach ( $this->mConns as $conns2 ) {
 811+ foreach ( $conns2 as $conns3 ) {
 812+ foreach ( $conns3 as $conn ) {
 813+ $conn->commit();
 814+ }
 815+ }
 816+ }
 817+ }
 818+
 819+ /* Issue COMMIT only on master, only if queries were done on connection */
 820+ function commitMasterChanges() {
 821+ // Always 0, but who knows.. :)
 822+ $masterIndex = $this->getWriterIndex();
 823+ foreach ( $this->mConns as $type => $conns2 ) {
 824+ if ( empty( $conns2[$masterIndex] ) ) {
 825+ continue;
 826+ }
 827+ foreach ( $conns2[$masterIndex] as $conn ) {
 828+ if ( $conn->doneWrites() ) {
 829+ $conn->commit();
 830+ }
 831+ }
 832+ }
 833+ }
 834+
 835+ function waitTimeout( $value = null ) {
 836+ return wfSetVar( $this->mWaitTimeout, $value );
 837+ }
 838+
 839+ function getLaggedSlaveMode() {
 840+ return $this->mLaggedSlaveMode;
 841+ }
 842+
 843+ /* Disables/enables lag checks */
 844+ function allowLagged($mode=null) {
 845+ if ($mode===null)
 846+ return $this->mAllowLagged;
 847+ $this->mAllowLagged=$mode;
 848+ }
 849+
 850+ function pingAll() {
 851+ $success = true;
 852+ foreach ( $this->mConns as $conns2 ) {
 853+ foreach ( $conns2 as $conns3 ) {
 854+ foreach ( $conns3 as $conn ) {
 855+ if ( !$conn->ping() ) {
 856+ $success = false;
 857+ }
 858+ }
 859+ }
 860+ }
 861+ return $success;
 862+ }
 863+
 864+ /**
 865+ * Call a function with each open connection object
 866+ */
 867+ function forEachOpenConnection( $callback, $params = array() ) {
 868+ foreach ( $this->mConns as $conns2 ) {
 869+ foreach ( $conns2 as $conns3 ) {
 870+ foreach ( $conns3 as $conn ) {
 871+ $mergedParams = array_merge( array( $conn ), $params );
 872+ call_user_func_array( $callback, $mergedParams );
 873+ }
 874+ }
 875+ }
 876+ }
 877+
 878+ /**
 879+ * Get the hostname and lag time of the most-lagged slave.
 880+ * This is useful for maintenance scripts that need to throttle their updates.
 881+ * May attempt to open connections to slaves on the default DB.
 882+ * @param $wiki string Wiki ID, or false for the default database
 883+ */
 884+ function getMaxLag( $wiki = false ) {
 885+ $maxLag = -1;
 886+ $host = '';
 887+ foreach ( $this->mServers as $i => $conn ) {
 888+ $conn = false;
 889+ if ( $wiki === false ) {
 890+ $conn = $this->getAnyOpenConnection( $i );
 891+ }
 892+ if ( !$conn ) {
 893+ $conn = $this->openConnection( $i, $wiki );
 894+ }
 895+ if ( !$conn ) {
 896+ continue;
 897+ }
 898+ $lag = $conn->getLag();
 899+ if ( $lag > $maxLag ) {
 900+ $maxLag = $lag;
 901+ $host = $this->mServers[$i]['host'];
 902+ }
 903+ }
 904+ return array( $host, $maxLag );
 905+ }
 906+
 907+ /**
 908+ * Get lag time for each server
 909+ * Results are cached for a short time in memcached, and indefinitely in the process cache
 910+ */
 911+ function getLagTimes( $wiki = false ) {
 912+ # Try process cache
 913+ if ( isset( $this->mLagTimes ) ) {
 914+ return $this->mLagTimes;
 915+ }
 916+ # No, send the request to the load monitor
 917+ $this->mLagTimes = $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki );
 918+ return $this->mLagTimes;
 919+ }
 920+}
Property changes on: trunk/extensions/MSSQLBackCompat/db/LoadBalancer.php
___________________________________________________________________
Added: svn:keywords
1921 + Author Date Id Revision
Added: svn:eol-style
2922 + native
Index: trunk/extensions/MSSQLBackCompat/db/LoadMonitor.php
@@ -0,0 +1,124 @@
 2+<?php
 3+
 4+/**
 5+ * An interface for database load monitoring
 6+ */
 7+
 8+interface LoadMonitor {
 9+ /**
 10+ * Construct a new LoadMonitor with a given LoadBalancer parent
 11+ */
 12+ function __construct( $parent );
 13+
 14+ /**
 15+ * Perform pre-connection load ratio adjustment.
 16+ * @param array $loads
 17+ * @param string $group The selected query group
 18+ * @param string $wiki
 19+ */
 20+ function scaleLoads( &$loads, $group = false, $wiki = false );
 21+
 22+ /**
 23+ * Perform post-connection backoff.
 24+ *
 25+ * If the connection is in overload, this should return a backoff factor
 26+ * which will be used to control polling time. The number of threads
 27+ * connected is a good measure.
 28+ *
 29+ * If there is no overload, zero can be returned.
 30+ *
 31+ * A threshold thread count is given, the concrete class may compare this
 32+ * to the running thread count. The threshold may be false, which indicates
 33+ * that the sysadmin has not configured this feature.
 34+ *
 35+ * @param Database $conn
 36+ * @param float $threshold
 37+ */
 38+ function postConnectionBackoff( $conn, $threshold );
 39+
 40+ /**
 41+ * Return an estimate of replication lag for each server
 42+ */
 43+ function getLagTimes( $serverIndexes, $wiki );
 44+}
 45+
 46+
 47+/**
 48+ * Basic MySQL load monitor with no external dependencies
 49+ * Uses memcached to cache the replication lag for a short time
 50+ */
 51+
 52+class LoadMonitor_MySQL implements LoadMonitor {
 53+ var $parent; // LoadBalancer
 54+
 55+ function __construct( $parent ) {
 56+ $this->parent = $parent;
 57+ }
 58+
 59+ function scaleLoads( &$loads, $group = false, $wiki = false ) {
 60+ }
 61+
 62+ function getLagTimes( $serverIndexes, $wiki ) {
 63+ wfProfileIn( __METHOD__ );
 64+ $expiry = 5;
 65+ $requestRate = 10;
 66+
 67+ global $wgMemc;
 68+ if ( empty( $wgMemc ) )
 69+ $wgMemc = wfGetMainCache();
 70+
 71+ $masterName = $this->parent->getServerName( 0 );
 72+ $memcKey = wfMemcKey( 'lag_times', $masterName );
 73+ $times = $wgMemc->get( $memcKey );
 74+ if ( $times ) {
 75+ # Randomly recache with probability rising over $expiry
 76+ $elapsed = time() - $times['timestamp'];
 77+ $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
 78+ if ( mt_rand( 0, $chance ) != 0 ) {
 79+ unset( $times['timestamp'] );
 80+ wfProfileOut( __METHOD__ );
 81+ return $times;
 82+ }
 83+ wfIncrStats( 'lag_cache_miss_expired' );
 84+ } else {
 85+ wfIncrStats( 'lag_cache_miss_absent' );
 86+ }
 87+
 88+ # Cache key missing or expired
 89+
 90+ $times = array();
 91+ foreach ( $serverIndexes as $i ) {
 92+ if ($i == 0) { # Master
 93+ $times[$i] = 0;
 94+ } elseif ( false !== ( $conn = $this->parent->getAnyOpenConnection( $i ) ) ) {
 95+ $times[$i] = $conn->getLag();
 96+ } elseif ( false !== ( $conn = $this->parent->openConnection( $i, $wiki ) ) ) {
 97+ $times[$i] = $conn->getLag();
 98+ }
 99+ }
 100+
 101+ # Add a timestamp key so we know when it was cached
 102+ $times['timestamp'] = time();
 103+ $wgMemc->set( $memcKey, $times, $expiry );
 104+
 105+ # But don't give the timestamp to the caller
 106+ unset($times['timestamp']);
 107+ $lagTimes = $times;
 108+
 109+ wfProfileOut( __METHOD__ );
 110+ return $lagTimes;
 111+ }
 112+
 113+ function postConnectionBackoff( $conn, $threshold ) {
 114+ if ( !$threshold ) {
 115+ return 0;
 116+ }
 117+ $status = $conn->getStatus("Thread%");
 118+ if ( $status['Threads_running'] > $threshold ) {
 119+ return $status['Threads_connected'];
 120+ } else {
 121+ return 0;
 122+ }
 123+ }
 124+}
 125+
Property changes on: trunk/extensions/MSSQLBackCompat/db/LoadMonitor.php
___________________________________________________________________
Added: svn:eol-style
1126 + native
Property changes on: trunk/extensions/MSSQLBackCompat/db
___________________________________________________________________
Added: svn:mergeinfo
2127 Merged /branches/sqlite/includes/db:r58211-58321
3128 Merged /branches/wmf-deployment/includes/db:r53381
4129 Merged /branches/REL1_15/phase3/includes/db:r51646

Follow-up revisions

RevisionCommit summaryAuthorDate
r85162Extracting DatabaseMssqlOld from r85161reedy22:41, 1 April 2011

Status & tagging log