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 |
1 | 455 | + 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 |
1 | 1203 | + 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 |
1 | 1478 | + Author Date Id Revision |
Added: svn:eol-style |
2 | 1479 | + 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 |
1 | 245 | + 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 |
1 | 1839 | + 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 |
1 | 2863 | + Author Date Id Revision |
Added: svn:eol-style |
2 | 2864 | + 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 |
1 | 970 | + 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 |
1 | 273 | + 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 |
1 | 583 | + 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 |
1 | 921 | + Author Date Id Revision |
Added: svn:eol-style |
2 | 922 | + 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 |
1 | 126 | + native |
Property changes on: trunk/extensions/MSSQLBackCompat/db |
___________________________________________________________________ |
Added: svn:mergeinfo |
2 | 127 | Merged /branches/sqlite/includes/db:r58211-58321 |
3 | 128 | Merged /branches/wmf-deployment/includes/db:r53381 |
4 | 129 | Merged /branches/REL1_15/phase3/includes/db:r51646 |