[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * This is the MS SQL Server Native database abstraction layer. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Database 22 * @author Joel Penner <a-joelpe at microsoft dot com> 23 * @author Chris Pucci <a-cpucci at microsoft dot com> 24 * @author Ryan Biesemeyer <v-ryanbi at microsoft dot com> 25 * @author Ryan Schmidt <skizzerz at gmail dot com> 26 */ 27 28 /** 29 * @ingroup Database 30 */ 31 class DatabaseMssql extends DatabaseBase { 32 protected $mInsertId = null; 33 protected $mLastResult = null; 34 protected $mAffectedRows = null; 35 protected $mSubqueryId = 0; 36 protected $mScrollableCursor = true; 37 protected $mPrepareStatements = true; 38 protected $mBinaryColumnCache = null; 39 protected $mBitColumnCache = null; 40 protected $mIgnoreDupKeyErrors = false; 41 42 protected $mPort; 43 44 public function cascadingDeletes() { 45 return true; 46 } 47 48 public function cleanupTriggers() { 49 return false; 50 } 51 52 public function strictIPs() { 53 return false; 54 } 55 56 public function realTimestamps() { 57 return false; 58 } 59 60 public function implicitGroupby() { 61 return false; 62 } 63 64 public function implicitOrderby() { 65 return false; 66 } 67 68 public function functionalIndexes() { 69 return true; 70 } 71 72 public function unionSupportsOrderAndLimit() { 73 return false; 74 } 75 76 /** 77 * Usually aborts on failure 78 * @param string $server 79 * @param string $user 80 * @param string $password 81 * @param string $dbName 82 * @throws DBConnectionError 83 * @return bool|DatabaseBase|null 84 */ 85 public function open( $server, $user, $password, $dbName ) { 86 # Test for driver support, to avoid suppressed fatal error 87 if ( !function_exists( 'sqlsrv_connect' ) ) { 88 throw new DBConnectionError( 89 $this, 90 "Microsoft SQL Server Native (sqlsrv) functions missing. 91 You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" 92 ); 93 } 94 95 global $wgDBport, $wgDBWindowsAuthentication; 96 97 # e.g. the class is being loaded 98 if ( !strlen( $user ) ) { 99 return null; 100 } 101 102 $this->close(); 103 $this->mServer = $server; 104 $this->mPort = $wgDBport; 105 $this->mUser = $user; 106 $this->mPassword = $password; 107 $this->mDBname = $dbName; 108 109 $connectionInfo = array(); 110 111 if ( $dbName ) { 112 $connectionInfo['Database'] = $dbName; 113 } 114 115 // Decide which auth scenerio to use 116 // if we are using Windows auth, don't add credentials to $connectionInfo 117 if ( !$wgDBWindowsAuthentication ) { 118 $connectionInfo['UID'] = $user; 119 $connectionInfo['PWD'] = $password; 120 } 121 122 wfSuppressWarnings(); 123 $this->mConn = sqlsrv_connect( $server, $connectionInfo ); 124 wfRestoreWarnings(); 125 126 if ( $this->mConn === false ) { 127 throw new DBConnectionError( $this, $this->lastError() ); 128 } 129 130 $this->mOpened = true; 131 132 return $this->mConn; 133 } 134 135 /** 136 * Closes a database connection, if it is open 137 * Returns success, true if already closed 138 * @return bool 139 */ 140 protected function closeConnection() { 141 return sqlsrv_close( $this->mConn ); 142 } 143 144 /** 145 * @param bool|MssqlResultWrapper|resource $result 146 * @return bool|MssqlResultWrapper 147 */ 148 public function resultObject( $result ) { 149 if ( empty( $result ) ) { 150 return false; 151 } elseif ( $result instanceof MssqlResultWrapper ) { 152 return $result; 153 } elseif ( $result === true ) { 154 // Successful write query 155 return $result; 156 } else { 157 return new MssqlResultWrapper( $this, $result ); 158 } 159 } 160 161 /** 162 * @param string $sql 163 * @return bool|MssqlResult 164 * @throws DBUnexpectedError 165 */ 166 protected function doQuery( $sql ) { 167 if ( $this->debug() ) { 168 wfDebug( "SQL: [$sql]\n" ); 169 } 170 $this->offset = 0; 171 172 // several extensions seem to think that all databases support limits 173 // via LIMIT N after the WHERE clause well, MSSQL uses SELECT TOP N, 174 // so to catch any of those extensions we'll do a quick check for a 175 // LIMIT clause and pass $sql through $this->LimitToTopN() which parses 176 // the limit clause and passes the result to $this->limitResult(); 177 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) { 178 // massage LIMIT -> TopN 179 $sql = $this->LimitToTopN( $sql ); 180 } 181 182 // MSSQL doesn't have EXTRACT(epoch FROM XXX) 183 if ( preg_match( '#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) { 184 // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970 185 $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql ); 186 } 187 188 // perform query 189 190 // SQLSRV_CURSOR_STATIC is slower than SQLSRV_CURSOR_CLIENT_BUFFERED (one of the two is 191 // needed if we want to be able to seek around the result set), however CLIENT_BUFFERED 192 // has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty 193 // strings make php throw a fatal error "Severe error translating Unicode" 194 if ( $this->mScrollableCursor ) { 195 $scrollArr = array( 'Scrollable' => SQLSRV_CURSOR_STATIC ); 196 } else { 197 $scrollArr = array(); 198 } 199 200 if ( $this->mPrepareStatements ) { 201 // we do prepare + execute so we can get its field metadata for later usage if desired 202 $stmt = sqlsrv_prepare( $this->mConn, $sql, array(), $scrollArr ); 203 $success = sqlsrv_execute( $stmt ); 204 } else { 205 $stmt = sqlsrv_query( $this->mConn, $sql, array(), $scrollArr ); 206 $success = (bool)$stmt; 207 } 208 209 if ( $this->mIgnoreDupKeyErrors ) { 210 // ignore duplicate key errors, but nothing else 211 // this emulates INSERT IGNORE in MySQL 212 if ( $success === false ) { 213 $errors = sqlsrv_errors( SQLSRV_ERR_ERRORS ); 214 $success = true; 215 216 foreach ( $errors as $err ) { 217 if ( $err['SQLSTATE'] == '23000' && $err['code'] == '2601' ) { 218 continue; // duplicate key error caused by unique index 219 } elseif ( $err['SQLSTATE'] == '23000' && $err['code'] == '2627' ) { 220 continue; // duplicate key error caused by primary key 221 } elseif ( $err['SQLSTATE'] == '01000' && $err['code'] == '3621' ) { 222 continue; // generic "the statement has been terminated" error 223 } 224 225 $success = false; // getting here means we got an error we weren't expecting 226 break; 227 } 228 229 if ( $success ) { 230 $this->mAffectedRows = 0; 231 return $stmt; 232 } 233 } 234 } 235 236 if ( $success === false ) { 237 return false; 238 } 239 // remember number of rows affected 240 $this->mAffectedRows = sqlsrv_rows_affected( $stmt ); 241 242 return $stmt; 243 } 244 245 public function freeResult( $res ) { 246 if ( $res instanceof ResultWrapper ) { 247 $res = $res->result; 248 } 249 250 sqlsrv_free_stmt( $res ); 251 } 252 253 /** 254 * @param MssqlResultWrapper $res 255 * @return stdClass 256 */ 257 public function fetchObject( $res ) { 258 // $res is expected to be an instance of MssqlResultWrapper here 259 return $res->fetchObject(); 260 } 261 262 /** 263 * @param MssqlResultWrapper $res 264 * @return array 265 */ 266 public function fetchRow( $res ) { 267 return $res->fetchRow(); 268 } 269 270 /** 271 * @param mixed $res 272 * @return int 273 */ 274 public function numRows( $res ) { 275 if ( $res instanceof ResultWrapper ) { 276 $res = $res->result; 277 } 278 279 return sqlsrv_num_rows( $res ); 280 } 281 282 /** 283 * @param mixed $res 284 * @return int 285 */ 286 public function numFields( $res ) { 287 if ( $res instanceof ResultWrapper ) { 288 $res = $res->result; 289 } 290 291 return sqlsrv_num_fields( $res ); 292 } 293 294 /** 295 * @param mixed $res 296 * @param int $n 297 * @return int 298 */ 299 public function fieldName( $res, $n ) { 300 if ( $res instanceof ResultWrapper ) { 301 $res = $res->result; 302 } 303 304 $metadata = sqlsrv_field_metadata( $res ); 305 return $metadata[$n]['Name']; 306 } 307 308 /** 309 * This must be called after nextSequenceVal 310 * @return int|null 311 */ 312 public function insertId() { 313 return $this->mInsertId; 314 } 315 316 /** 317 * @param MssqlResultWrapper $res 318 * @param int $row 319 * @return bool 320 */ 321 public function dataSeek( $res, $row ) { 322 return $res->seek( $row ); 323 } 324 325 /** 326 * @return string 327 */ 328 public function lastError() { 329 $strRet = ''; 330 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL ); 331 if ( $retErrors != null ) { 332 foreach ( $retErrors as $arrError ) { 333 $strRet .= $this->formatError( $arrError ) . "\n"; 334 } 335 } else { 336 $strRet = "No errors found"; 337 } 338 339 return $strRet; 340 } 341 342 /** 343 * @param array $err 344 * @return string 345 */ 346 private function formatError( $err ) { 347 return '[SQLSTATE ' . $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message']; 348 } 349 350 /** 351 * @return string 352 */ 353 public function lastErrno() { 354 $err = sqlsrv_errors( SQLSRV_ERR_ALL ); 355 if ( $err !== null && isset( $err[0] ) ) { 356 return $err[0]['code']; 357 } else { 358 return 0; 359 } 360 } 361 362 /** 363 * @return int 364 */ 365 public function affectedRows() { 366 return $this->mAffectedRows; 367 } 368 369 /** 370 * SELECT wrapper 371 * 372 * @param mixed $table Array or string, table name(s) (prefix auto-added) 373 * @param mixed $vars Array or string, field name(s) to be retrieved 374 * @param mixed $conds Array or string, condition(s) for WHERE 375 * @param string $fname Calling function name (use __METHOD__) for logs/profiling 376 * @param array $options Associative array of options (e.g. 377 * array('GROUP BY' => 'page_title')), see Database::makeSelectOptions 378 * code for list of supported stuff 379 * @param array $join_conds Associative array of table join conditions 380 * (optional) (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) 381 * @return mixed Database result resource (feed to Database::fetchObject 382 * or whatever), or false on failure 383 */ 384 public function select( $table, $vars, $conds = '', $fname = __METHOD__, 385 $options = array(), $join_conds = array() 386 ) { 387 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); 388 if ( isset( $options['EXPLAIN'] ) ) { 389 try { 390 $this->mScrollableCursor = false; 391 $this->mPrepareStatements = false; 392 $this->query( "SET SHOWPLAN_ALL ON" ); 393 $ret = $this->query( $sql, $fname ); 394 $this->query( "SET SHOWPLAN_ALL OFF" ); 395 } catch ( DBQueryError $dqe ) { 396 if ( isset( $options['FOR COUNT'] ) ) { 397 // likely don't have privs for SHOWPLAN, so run a select count instead 398 $this->query( "SET SHOWPLAN_ALL OFF" ); 399 unset( $options['EXPLAIN'] ); 400 $ret = $this->select( 401 $table, 402 'COUNT(*) AS EstimateRows', 403 $conds, 404 $fname, 405 $options, 406 $join_conds 407 ); 408 } else { 409 // someone actually wanted the query plan instead of an est row count 410 // let them know of the error 411 $this->mScrollableCursor = true; 412 $this->mPrepareStatements = true; 413 throw $dqe; 414 } 415 } 416 $this->mScrollableCursor = true; 417 $this->mPrepareStatements = true; 418 return $ret; 419 } 420 return $this->query( $sql, $fname ); 421 } 422 423 /** 424 * SELECT wrapper 425 * 426 * @param mixed $table Array or string, table name(s) (prefix auto-added) 427 * @param mixed $vars Array or string, field name(s) to be retrieved 428 * @param mixed $conds Array or string, condition(s) for WHERE 429 * @param string $fname Calling function name (use __METHOD__) for logs/profiling 430 * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), 431 * see Database::makeSelectOptions code for list of supported stuff 432 * @param array $join_conds Associative array of table join conditions (optional) 433 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) 434 * @return string The SQL text 435 */ 436 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, 437 $options = array(), $join_conds = array() 438 ) { 439 if ( isset( $options['EXPLAIN'] ) ) { 440 unset( $options['EXPLAIN'] ); 441 } 442 443 $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); 444 445 // try to rewrite aggregations of bit columns (currently MAX and MIN) 446 if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) { 447 $bitColumns = array(); 448 if ( is_array( $table ) ) { 449 foreach ( $table as $t ) { 450 $bitColumns += $this->getBitColumns( $this->tableName( $t ) ); 451 } 452 } else { 453 $bitColumns = $this->getBitColumns( $this->tableName( $table ) ); 454 } 455 456 foreach ( $bitColumns as $col => $info ) { 457 $replace = array( 458 "MAX({$col})" => "MAX(CAST({$col} AS tinyint))", 459 "MIN({$col})" => "MIN(CAST({$col} AS tinyint))", 460 ); 461 $sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql ); 462 } 463 } 464 465 return $sql; 466 } 467 468 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, 469 $fname = __METHOD__ 470 ) { 471 $this->mScrollableCursor = false; 472 try { 473 parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname ); 474 } catch ( Exception $e ) { 475 $this->mScrollableCursor = true; 476 throw $e; 477 } 478 $this->mScrollableCursor = true; 479 } 480 481 public function delete( $table, $conds, $fname = __METHOD__ ) { 482 $this->mScrollableCursor = false; 483 try { 484 parent::delete( $table, $conds, $fname ); 485 } catch ( Exception $e ) { 486 $this->mScrollableCursor = true; 487 throw $e; 488 } 489 $this->mScrollableCursor = true; 490 } 491 492 /** 493 * Estimate rows in dataset 494 * Returns estimated count, based on SHOWPLAN_ALL output 495 * This is not necessarily an accurate estimate, so use sparingly 496 * Returns -1 if count cannot be found 497 * Takes same arguments as Database::select() 498 * @param string $table 499 * @param string $vars 500 * @param string $conds 501 * @param string $fname 502 * @param array $options 503 * @return int 504 */ 505 public function estimateRowCount( $table, $vars = '*', $conds = '', 506 $fname = __METHOD__, $options = array() 507 ) { 508 // http://msdn2.microsoft.com/en-us/library/aa259203.aspx 509 $options['EXPLAIN'] = true; 510 $options['FOR COUNT'] = true; 511 $res = $this->select( $table, $vars, $conds, $fname, $options ); 512 513 $rows = -1; 514 if ( $res ) { 515 $row = $this->fetchRow( $res ); 516 517 if ( isset( $row['EstimateRows'] ) ) { 518 $rows = $row['EstimateRows']; 519 } 520 } 521 522 return $rows; 523 } 524 525 /** 526 * Returns information about an index 527 * If errors are explicitly ignored, returns NULL on failure 528 * @param string $table 529 * @param string $index 530 * @param string $fname 531 * @return array|bool|null 532 */ 533 public function indexInfo( $table, $index, $fname = __METHOD__ ) { 534 # This does not return the same info as MYSQL would, but that's OK 535 # because MediaWiki never uses the returned value except to check for 536 # the existance of indexes. 537 $sql = "sp_helpindex '" . $table . "'"; 538 $res = $this->query( $sql, $fname ); 539 if ( !$res ) { 540 return null; 541 } 542 543 $result = array(); 544 foreach ( $res as $row ) { 545 if ( $row->index_name == $index ) { 546 $row->Non_unique = !stristr( $row->index_description, "unique" ); 547 $cols = explode( ", ", $row->index_keys ); 548 foreach ( $cols as $col ) { 549 $row->Column_name = trim( $col ); 550 $result[] = clone $row; 551 } 552 } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) { 553 $row->Non_unique = 0; 554 $cols = explode( ", ", $row->index_keys ); 555 foreach ( $cols as $col ) { 556 $row->Column_name = trim( $col ); 557 $result[] = clone $row; 558 } 559 } 560 } 561 562 return empty( $result ) ? false : $result; 563 } 564 565 /** 566 * INSERT wrapper, inserts an array into a table 567 * 568 * $arrToInsert may be a single associative array, or an array of these with numeric keys, for 569 * multi-row insert. 570 * 571 * Usually aborts on failure 572 * If errors are explicitly ignored, returns success 573 * @param string $table 574 * @param array $arrToInsert 575 * @param string $fname 576 * @param array $options 577 * @throws DBQueryError 578 * @return bool 579 */ 580 public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) { 581 # No rows to insert, easy just return now 582 if ( !count( $arrToInsert ) ) { 583 return true; 584 } 585 586 if ( !is_array( $options ) ) { 587 $options = array( $options ); 588 } 589 590 $table = $this->tableName( $table ); 591 592 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row 593 $arrToInsert = array( 0 => $arrToInsert ); // make everything multi row compatible 594 } 595 596 // We know the table we're inserting into, get its identity column 597 $identity = null; 598 // strip matching square brackets and the db/schema from table name 599 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); 600 $tableRaw = array_pop( $tableRawArr ); 601 $res = $this->doQuery( 602 "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " . 603 "WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" 604 ); 605 if ( $res && sqlsrv_has_rows( $res ) ) { 606 // There is an identity for this table. 607 $identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC ); 608 $identity = array_pop( $identityArr ); 609 } 610 sqlsrv_free_stmt( $res ); 611 612 // Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF 613 $binaryColumns = $this->getBinaryColumns( $table ); 614 615 // INSERT IGNORE is not supported by SQL Server 616 // remove IGNORE from options list and set ignore flag to true 617 if ( in_array( 'IGNORE', $options ) ) { 618 $options = array_diff( $options, array( 'IGNORE' ) ); 619 $this->mIgnoreDupKeyErrors = true; 620 } 621 622 foreach ( $arrToInsert as $a ) { 623 // start out with empty identity column, this is so we can return 624 // it as a result of the insert logic 625 $sqlPre = ''; 626 $sqlPost = ''; 627 $identityClause = ''; 628 629 // if we have an identity column 630 if ( $identity ) { 631 // iterate through 632 foreach ( $a as $k => $v ) { 633 if ( $k == $identity ) { 634 if ( !is_null( $v ) ) { 635 // there is a value being passed to us, 636 // we need to turn on and off inserted identity 637 $sqlPre = "SET IDENTITY_INSERT $table ON;"; 638 $sqlPost = ";SET IDENTITY_INSERT $table OFF;"; 639 } else { 640 // we can't insert NULL into an identity column, 641 // so remove the column from the insert. 642 unset( $a[$k] ); 643 } 644 } 645 } 646 647 // we want to output an identity column as result 648 $identityClause = "OUTPUT INSERTED.$identity "; 649 } 650 651 $keys = array_keys( $a ); 652 653 // Build the actual query 654 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) . 655 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES ("; 656 657 $first = true; 658 foreach ( $a as $key => $value ) { 659 if ( isset( $binaryColumns[$key] ) ) { 660 $value = new MssqlBlob( $value ); 661 } 662 if ( $first ) { 663 $first = false; 664 } else { 665 $sql .= ','; 666 } 667 if ( is_null( $value ) ) { 668 $sql .= 'null'; 669 } elseif ( is_array( $value ) || is_object( $value ) ) { 670 if ( is_object( $value ) && $value instanceof Blob ) { 671 $sql .= $this->addQuotes( $value ); 672 } else { 673 $sql .= $this->addQuotes( serialize( $value ) ); 674 } 675 } else { 676 $sql .= $this->addQuotes( $value ); 677 } 678 } 679 $sql .= ')' . $sqlPost; 680 681 // Run the query 682 $this->mScrollableCursor = false; 683 try { 684 $ret = $this->query( $sql ); 685 } catch ( Exception $e ) { 686 $this->mScrollableCursor = true; 687 $this->mIgnoreDupKeyErrors = false; 688 throw $e; 689 } 690 $this->mScrollableCursor = true; 691 692 if ( !is_null( $identity ) ) { 693 // then we want to get the identity column value we were assigned and save it off 694 $row = $ret->fetchObject(); 695 if( is_object( $row ) ){ 696 $this->mInsertId = $row->$identity; 697 } 698 } 699 } 700 $this->mIgnoreDupKeyErrors = false; 701 return $ret; 702 } 703 704 /** 705 * INSERT SELECT wrapper 706 * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...) 707 * Source items may be literals rather than field names, but strings should 708 * be quoted with Database::addQuotes(). 709 * @param string $destTable 710 * @param array|string $srcTable May be an array of tables. 711 * @param array $varMap 712 * @param array $conds May be "*" to copy the whole table. 713 * @param string $fname 714 * @param array $insertOptions 715 * @param array $selectOptions 716 * @throws DBQueryError 717 * @return null|ResultWrapper 718 */ 719 public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, 720 $insertOptions = array(), $selectOptions = array() 721 ) { 722 $this->mScrollableCursor = false; 723 try { 724 $ret = parent::insertSelect( 725 $destTable, 726 $srcTable, 727 $varMap, 728 $conds, 729 $fname, 730 $insertOptions, 731 $selectOptions 732 ); 733 } catch ( Exception $e ) { 734 $this->mScrollableCursor = true; 735 throw $e; 736 } 737 $this->mScrollableCursor = true; 738 739 return $ret; 740 } 741 742 /** 743 * UPDATE wrapper. Takes a condition array and a SET array. 744 * 745 * @param string $table Name of the table to UPDATE. This will be passed through 746 * DatabaseBase::tableName(). 747 * 748 * @param array $values An array of values to SET. For each array element, 749 * the key gives the field name, and the value gives the data 750 * to set that field to. The data will be quoted by 751 * DatabaseBase::addQuotes(). 752 * 753 * @param array $conds An array of conditions (WHERE). See 754 * DatabaseBase::select() for the details of the format of 755 * condition arrays. Use '*' to update all rows. 756 * 757 * @param string $fname The function name of the caller (from __METHOD__), 758 * for logging and profiling. 759 * 760 * @param array $options An array of UPDATE options, can be: 761 * - IGNORE: Ignore unique key conflicts 762 * - LOW_PRIORITY: MySQL-specific, see MySQL manual. 763 * @return bool 764 */ 765 function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { 766 $table = $this->tableName( $table ); 767 $binaryColumns = $this->getBinaryColumns( $table ); 768 769 $opts = $this->makeUpdateOptions( $options ); 770 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns ); 771 772 if ( $conds !== array() && $conds !== '*' ) { 773 $sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns ); 774 } 775 776 $this->mScrollableCursor = false; 777 try { 778 $ret = $this->query( $sql ); 779 } catch ( Exception $e ) { 780 $this->mScrollableCursor = true; 781 throw $e; 782 } 783 $this->mScrollableCursor = true; 784 return true; 785 } 786 787 /** 788 * Makes an encoded list of strings from an array 789 * @param array $a Containing the data 790 * @param int $mode Constant 791 * - LIST_COMMA: comma separated, no field names 792 * - LIST_AND: ANDed WHERE clause (without the WHERE). See 793 * the documentation for $conds in DatabaseBase::select(). 794 * - LIST_OR: ORed WHERE clause (without the WHERE) 795 * - LIST_SET: comma separated with field names, like a SET clause 796 * - LIST_NAMES: comma separated field names 797 * @param array $binaryColumns Contains a list of column names that are binary types 798 * This is a custom parameter only present for MS SQL. 799 * 800 * @throws MWException|DBUnexpectedError 801 * @return string 802 */ 803 public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = array() ) { 804 if ( !is_array( $a ) ) { 805 throw new DBUnexpectedError( $this, 806 'DatabaseBase::makeList called with incorrect parameters' ); 807 } 808 809 $first = true; 810 $list = ''; 811 812 foreach ( $a as $field => $value ) { 813 if ( $mode != LIST_NAMES && isset( $binaryColumns[$field] ) ) { 814 if ( is_array( $value ) ) { 815 foreach ( $value as &$v ) { 816 $v = new MssqlBlob( $v ); 817 } 818 } else { 819 $value = new MssqlBlob( $value ); 820 } 821 } 822 823 if ( !$first ) { 824 if ( $mode == LIST_AND ) { 825 $list .= ' AND '; 826 } elseif ( $mode == LIST_OR ) { 827 $list .= ' OR '; 828 } else { 829 $list .= ','; 830 } 831 } else { 832 $first = false; 833 } 834 835 if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) { 836 $list .= "($value)"; 837 } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) { 838 $list .= "$value"; 839 } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) { 840 if ( count( $value ) == 0 ) { 841 throw new MWException( __METHOD__ . ": empty input for field $field" ); 842 } elseif ( count( $value ) == 1 ) { 843 // Special-case single values, as IN isn't terribly efficient 844 // Don't necessarily assume the single key is 0; we don't 845 // enforce linear numeric ordering on other arrays here. 846 $value = array_values( $value ); 847 $list .= $field . " = " . $this->addQuotes( $value[0] ); 848 } else { 849 $list .= $field . " IN (" . $this->makeList( $value ) . ") "; 850 } 851 } elseif ( $value === null ) { 852 if ( $mode == LIST_AND || $mode == LIST_OR ) { 853 $list .= "$field IS "; 854 } elseif ( $mode == LIST_SET ) { 855 $list .= "$field = "; 856 } 857 $list .= 'NULL'; 858 } else { 859 if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { 860 $list .= "$field = "; 861 } 862 $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value ); 863 } 864 } 865 866 return $list; 867 } 868 869 /** 870 * @param string $table 871 * @param string $field 872 * @return int Returns the size of a text field, or -1 for "unlimited" 873 */ 874 public function textFieldSize( $table, $field ) { 875 $table = $this->tableName( $table ); 876 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns 877 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'"; 878 $res = $this->query( $sql ); 879 $row = $this->fetchRow( $res ); 880 $size = -1; 881 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) { 882 $size = $row['CHARACTER_MAXIMUM_LENGTH']; 883 } 884 885 return $size; 886 } 887 888 /** 889 * Construct a LIMIT query with optional offset 890 * This is used for query pages 891 * 892 * @param string $sql SQL query we will append the limit too 893 * @param int $limit The SQL limit 894 * @param bool|int $offset The SQL offset (default false) 895 * @return array|string 896 */ 897 public function limitResult( $sql, $limit, $offset = false ) { 898 if ( $offset === false || $offset == 0 ) { 899 if ( strpos( $sql, "SELECT" ) === false ) { 900 return "TOP {$limit} " . $sql; 901 } else { 902 return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi', 903 'SELECT$1 TOP ' . $limit, $sql, 1 ); 904 } 905 } else { 906 // This one is fun, we need to pull out the select list as well as any ORDER BY clause 907 $select = $orderby = array(); 908 $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select ); 909 $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby ); 910 $overOrder = $postOrder = ''; 911 $first = $offset + 1; 912 $last = $offset + $limit; 913 $sub1 = 'sub_' . $this->mSubqueryId; 914 $sub2 = 'sub_' . ( $this->mSubqueryId + 1 ); 915 $this->mSubqueryId += 2; 916 if ( !$s1 ) { 917 // wat 918 throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" ); 919 } 920 if ( !$s2 ) { 921 // no ORDER BY 922 $overOrder = 'ORDER BY (SELECT 1)'; 923 } else { 924 if ( !isset( $orderby[2] ) || !$orderby[2] ) { 925 // don't need to strip it out if we're using a FOR XML clause 926 $sql = str_replace( $orderby[1], '', $sql ); 927 } 928 $overOrder = $orderby[1]; 929 $postOrder = ' ' . $overOrder; 930 } 931 $sql = "SELECT {$select[1]} 932 FROM ( 933 SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, * 934 FROM ({$sql}) {$sub1} 935 ) {$sub2} 936 WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}"; 937 938 return $sql; 939 } 940 } 941 942 /** 943 * If there is a limit clause, parse it, strip it, and pass the remaining 944 * SQL through limitResult() with the appropriate parameters. Not the 945 * prettiest solution, but better than building a whole new parser. This 946 * exists becase there are still too many extensions that don't use dynamic 947 * sql generation. 948 * 949 * @param string $sql 950 * @return array|mixed|string 951 */ 952 public function LimitToTopN( $sql ) { 953 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset} 954 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i'; 955 if ( preg_match( $pattern, $sql, $matches ) ) { 956 // row_count = $matches[4] 957 $row_count = $matches[4]; 958 // offset = $matches[3] OR $matches[6] 959 $offset = $matches[3] or 960 $offset = $matches[6] or 961 $offset = false; 962 963 // strip the matching LIMIT clause out 964 $sql = str_replace( $matches[0], '', $sql ); 965 966 return $this->limitResult( $sql, $row_count, $offset ); 967 } 968 969 return $sql; 970 } 971 972 /** 973 * @return string Wikitext of a link to the server software's web site 974 */ 975 public function getSoftwareLink() { 976 return "[{{int:version-db-mssql-url}} MS SQL Server]"; 977 } 978 979 /** 980 * @return string Version information from the database 981 */ 982 public function getServerVersion() { 983 $server_info = sqlsrv_server_info( $this->mConn ); 984 $version = 'Error'; 985 if ( isset( $server_info['SQLServerVersion'] ) ) { 986 $version = $server_info['SQLServerVersion']; 987 } 988 989 return $version; 990 } 991 992 /** 993 * @param string $table 994 * @param string $fname 995 * @return bool 996 */ 997 public function tableExists( $table, $fname = __METHOD__ ) { 998 list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); 999 1000 if ( $db !== false ) { 1001 // remote database 1002 wfDebug( "Attempting to call tableExists on a remote table" ); 1003 return false; 1004 } 1005 1006 if ( $schema === false ) { 1007 global $wgDBmwschema; 1008 $schema = $wgDBmwschema; 1009 } 1010 1011 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES 1012 WHERE TABLE_TYPE = 'BASE TABLE' 1013 AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" ); 1014 1015 if ( $res->numRows() ) { 1016 return true; 1017 } else { 1018 return false; 1019 } 1020 } 1021 1022 /** 1023 * Query whether a given column exists in the mediawiki schema 1024 * @param string $table 1025 * @param string $field 1026 * @param string $fname 1027 * @return bool 1028 */ 1029 public function fieldExists( $table, $field, $fname = __METHOD__ ) { 1030 list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); 1031 1032 if ( $db !== false ) { 1033 // remote database 1034 wfDebug( "Attempting to call fieldExists on a remote table" ); 1035 return false; 1036 } 1037 1038 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS 1039 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); 1040 1041 if ( $res->numRows() ) { 1042 return true; 1043 } else { 1044 return false; 1045 } 1046 } 1047 1048 public function fieldInfo( $table, $field ) { 1049 list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); 1050 1051 if ( $db !== false ) { 1052 // remote database 1053 wfDebug( "Attempting to call fieldInfo on a remote table" ); 1054 return false; 1055 } 1056 1057 $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS 1058 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); 1059 1060 $meta = $res->fetchRow(); 1061 if ( $meta ) { 1062 return new MssqlField( $meta ); 1063 } 1064 1065 return false; 1066 } 1067 1068 /** 1069 * Begin a transaction, committing any previously open transaction 1070 * @param string $fname 1071 */ 1072 protected function doBegin( $fname = __METHOD__ ) { 1073 sqlsrv_begin_transaction( $this->mConn ); 1074 $this->mTrxLevel = 1; 1075 } 1076 1077 /** 1078 * End a transaction 1079 * @param string $fname 1080 */ 1081 protected function doCommit( $fname = __METHOD__ ) { 1082 sqlsrv_commit( $this->mConn ); 1083 $this->mTrxLevel = 0; 1084 } 1085 1086 /** 1087 * Rollback a transaction. 1088 * No-op on non-transactional databases. 1089 * @param string $fname 1090 */ 1091 protected function doRollback( $fname = __METHOD__ ) { 1092 sqlsrv_rollback( $this->mConn ); 1093 $this->mTrxLevel = 0; 1094 } 1095 1096 /** 1097 * Escapes a identifier for use inm SQL. 1098 * Throws an exception if it is invalid. 1099 * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx 1100 * @param string $identifier 1101 * @throws MWException 1102 * @return string 1103 */ 1104 private function escapeIdentifier( $identifier ) { 1105 if ( strlen( $identifier ) == 0 ) { 1106 throw new MWException( "An identifier must not be empty" ); 1107 } 1108 if ( strlen( $identifier ) > 128 ) { 1109 throw new MWException( "The identifier '$identifier' is too long (max. 128)" ); 1110 } 1111 if ( ( strpos( $identifier, '[' ) !== false ) 1112 || ( strpos( $identifier, ']' ) !== false ) 1113 ) { 1114 // It may be allowed if you quoted with double quotation marks, but 1115 // that would break if QUOTED_IDENTIFIER is OFF 1116 throw new MWException( "Square brackets are not allowed in '$identifier'" ); 1117 } 1118 1119 return "[$identifier]"; 1120 } 1121 1122 /** 1123 * @param string $s 1124 * @return string 1125 */ 1126 public function strencode( $s ) { # Should not be called by us 1127 return str_replace( "'", "''", $s ); 1128 } 1129 1130 /** 1131 * @param string $s 1132 * @return string 1133 */ 1134 public function addQuotes( $s ) { 1135 if ( $s instanceof MssqlBlob ) { 1136 return $s->fetch(); 1137 } elseif ( $s instanceof Blob ) { 1138 // this shouldn't really ever be called, but it's here if needed 1139 // (and will quite possibly make the SQL error out) 1140 $blob = new MssqlBlob( $s->fetch() ); 1141 return $blob->fetch(); 1142 } else { 1143 if ( is_bool( $s ) ) { 1144 $s = $s ? 1 : 0; 1145 } 1146 return parent::addQuotes( $s ); 1147 } 1148 } 1149 1150 /** 1151 * @param string $s 1152 * @return string 1153 */ 1154 public function addIdentifierQuotes( $s ) { 1155 // http://msdn.microsoft.com/en-us/library/aa223962.aspx 1156 return '[' . $s . ']'; 1157 } 1158 1159 /** 1160 * @param string $name 1161 * @return bool 1162 */ 1163 public function isQuotedIdentifier( $name ) { 1164 return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']'; 1165 } 1166 1167 /** 1168 * @param string $db 1169 * @return bool 1170 */ 1171 public function selectDB( $db ) { 1172 try { 1173 $this->mDBname = $db; 1174 $this->query( "USE $db" ); 1175 return true; 1176 } catch ( Exception $e ) { 1177 return false; 1178 } 1179 } 1180 1181 /** 1182 * @param array $options An associative array of options to be turned into 1183 * an SQL query, valid keys are listed in the function. 1184 * @return array 1185 */ 1186 public function makeSelectOptions( $options ) { 1187 $tailOpts = ''; 1188 $startOpts = ''; 1189 1190 $noKeyOptions = array(); 1191 foreach ( $options as $key => $option ) { 1192 if ( is_numeric( $key ) ) { 1193 $noKeyOptions[$option] = true; 1194 } 1195 } 1196 1197 $tailOpts .= $this->makeGroupByWithHaving( $options ); 1198 1199 $tailOpts .= $this->makeOrderBy( $options ); 1200 1201 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { 1202 $startOpts .= 'DISTINCT'; 1203 } 1204 1205 if ( isset( $noKeyOptions['FOR XML'] ) ) { 1206 // used in group concat field emulation 1207 $tailOpts .= " FOR XML PATH('')"; 1208 } 1209 1210 // we want this to be compatible with the output of parent::makeSelectOptions() 1211 return array( $startOpts, '', $tailOpts, '' ); 1212 } 1213 1214 /** 1215 * Get the type of the DBMS, as it appears in $wgDBtype. 1216 * @return string 1217 */ 1218 public function getType() { 1219 return 'mssql'; 1220 } 1221 1222 /** 1223 * @param array $stringList 1224 * @return string 1225 */ 1226 public function buildConcat( $stringList ) { 1227 return implode( ' + ', $stringList ); 1228 } 1229 1230 /** 1231 * Build a GROUP_CONCAT or equivalent statement for a query. 1232 * MS SQL doesn't have GROUP_CONCAT so we emulate it with other stuff (and boy is it nasty) 1233 * 1234 * This is useful for combining a field for several rows into a single string. 1235 * NULL values will not appear in the output, duplicated values will appear, 1236 * and the resulting delimiter-separated values have no defined sort order. 1237 * Code using the results may need to use the PHP unique() or sort() methods. 1238 * 1239 * @param string $delim Glue to bind the results together 1240 * @param string|array $table Table name 1241 * @param string $field Field name 1242 * @param string|array $conds Conditions 1243 * @param string|array $join_conds Join conditions 1244 * @return string SQL text 1245 * @since 1.23 1246 */ 1247 public function buildGroupConcatField( $delim, $table, $field, $conds = '', 1248 $join_conds = array() 1249 ) { 1250 $gcsq = 'gcsq_' . $this->mSubqueryId; 1251 $this->mSubqueryId++; 1252 1253 $delimLen = strlen( $delim ); 1254 $fld = "{$field} + {$this->addQuotes( $delim )}"; 1255 $sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM (" 1256 . $this->selectSQLText( $table, $fld, $conds, null, array( 'FOR XML' ), $join_conds ) 1257 . ") {$gcsq} ({$field}))"; 1258 1259 return $sql; 1260 } 1261 1262 /** 1263 * @return string 1264 */ 1265 public function getSearchEngine() { 1266 return "SearchMssql"; 1267 } 1268 1269 /** 1270 * Returns an associative array for fields that are of type varbinary, binary, or image 1271 * $table can be either a raw table name or passed through tableName() first 1272 * @param string $table 1273 * @return array 1274 */ 1275 private function getBinaryColumns( $table ) { 1276 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); 1277 $tableRaw = array_pop( $tableRawArr ); 1278 1279 if ( $this->mBinaryColumnCache === null ) { 1280 $this->populateColumnCaches(); 1281 } 1282 1283 return isset( $this->mBinaryColumnCache[$tableRaw] ) 1284 ? $this->mBinaryColumnCache[$tableRaw] 1285 : array(); 1286 } 1287 1288 /** 1289 * @param string $table 1290 * @return array 1291 */ 1292 private function getBitColumns( $table ) { 1293 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); 1294 $tableRaw = array_pop( $tableRawArr ); 1295 1296 if ( $this->mBitColumnCache === null ) { 1297 $this->populateColumnCaches(); 1298 } 1299 1300 return isset( $this->mBitColumnCache[$tableRaw] ) 1301 ? $this->mBitColumnCache[$tableRaw] 1302 : array(); 1303 } 1304 1305 private function populateColumnCaches() { 1306 $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*', 1307 array( 1308 'TABLE_CATALOG' => $this->mDBname, 1309 'TABLE_SCHEMA' => $this->mSchema, 1310 'DATA_TYPE' => array( 'varbinary', 'binary', 'image', 'bit' ) 1311 ) ); 1312 1313 $this->mBinaryColumnCache = array(); 1314 $this->mBitColumnCache = array(); 1315 foreach ( $res as $row ) { 1316 if ( $row->DATA_TYPE == 'bit' ) { 1317 $this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; 1318 } else { 1319 $this->mBinaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; 1320 } 1321 } 1322 } 1323 1324 /** 1325 * @param string $name 1326 * @param string $format 1327 * @return string 1328 */ 1329 function tableName( $name, $format = 'quoted' ) { 1330 # Replace reserved words with better ones 1331 switch ( $name ) { 1332 case 'user': 1333 return $this->realTableName( 'mwuser', $format ); 1334 default: 1335 return $this->realTableName( $name, $format ); 1336 } 1337 } 1338 1339 /** 1340 * call this instead of tableName() in the updater when renaming tables 1341 * @param string $name 1342 * @param string $format One of quoted, raw, or split 1343 * @return string 1344 */ 1345 function realTableName( $name, $format = 'quoted' ) { 1346 $table = parent::tableName( $name, $format ); 1347 if ( $format == 'split' ) { 1348 // Used internally, we want the schema split off from the table name and returned 1349 // as a list with 3 elements (database, schema, table) 1350 $table = explode( '.', $table ); 1351 while ( count( $table ) < 3 ) { 1352 array_unshift( $table, false ); 1353 } 1354 } 1355 return $table; 1356 } 1357 1358 /** 1359 * Called in the installer and updater. 1360 * Probably doesn't need to be called anywhere else in the codebase. 1361 * @param bool|null $value 1362 * @return bool|null 1363 */ 1364 public function prepareStatements( $value = null ) { 1365 return wfSetVar( $this->mPrepareStatements, $value ); 1366 } 1367 1368 /** 1369 * Called in the installer and updater. 1370 * Probably doesn't need to be called anywhere else in the codebase. 1371 * @param bool|null $value 1372 * @return bool|null 1373 */ 1374 public function scrollableCursor( $value = null ) { 1375 return wfSetVar( $this->mScrollableCursor, $value ); 1376 } 1377 } // end DatabaseMssql class 1378 1379 /** 1380 * Utility class. 1381 * 1382 * @ingroup Database 1383 */ 1384 class MssqlField implements Field { 1385 private $name, $tableName, $default, $max_length, $nullable, $type; 1386 1387 function __construct( $info ) { 1388 $this->name = $info['COLUMN_NAME']; 1389 $this->tableName = $info['TABLE_NAME']; 1390 $this->default = $info['COLUMN_DEFAULT']; 1391 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; 1392 $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' ); 1393 $this->type = $info['DATA_TYPE']; 1394 } 1395 1396 function name() { 1397 return $this->name; 1398 } 1399 1400 function tableName() { 1401 return $this->tableName; 1402 } 1403 1404 function defaultValue() { 1405 return $this->default; 1406 } 1407 1408 function maxLength() { 1409 return $this->max_length; 1410 } 1411 1412 function isNullable() { 1413 return $this->nullable; 1414 } 1415 1416 function type() { 1417 return $this->type; 1418 } 1419 } 1420 1421 class MssqlBlob extends Blob { 1422 public function __construct( $data ) { 1423 if ( $data instanceof MssqlBlob ) { 1424 return $data; 1425 } elseif ( $data instanceof Blob ) { 1426 $this->mData = $data->fetch(); 1427 } elseif ( is_array( $data ) && is_object( $data ) ) { 1428 $this->mData = serialize( $data ); 1429 } else { 1430 $this->mData = $data; 1431 } 1432 } 1433 1434 /** 1435 * Returns an unquoted hex representation of a binary string 1436 * for insertion into varbinary-type fields 1437 * @return string 1438 */ 1439 public function fetch() { 1440 if ( $this->mData === null ) { 1441 return 'null'; 1442 } 1443 1444 $ret = '0x'; 1445 $dataLength = strlen( $this->mData ); 1446 for ( $i = 0; $i < $dataLength; $i++ ) { 1447 $ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) ); 1448 } 1449 1450 return $ret; 1451 } 1452 } 1453 1454 class MssqlResultWrapper extends ResultWrapper { 1455 private $mSeekTo = null; 1456 1457 /** 1458 * @return stdClass|bool 1459 */ 1460 public function fetchObject() { 1461 $res = $this->result; 1462 1463 if ( $this->mSeekTo !== null ) { 1464 $result = sqlsrv_fetch_object( $res, 'stdClass', array(), 1465 SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); 1466 $this->mSeekTo = null; 1467 } else { 1468 $result = sqlsrv_fetch_object( $res ); 1469 } 1470 1471 // MediaWiki expects us to return boolean false when there are no more rows instead of null 1472 if ( $result === null ) { 1473 return false; 1474 } 1475 1476 return $result; 1477 } 1478 1479 /** 1480 * @return array|bool 1481 */ 1482 public function fetchRow() { 1483 $res = $this->result; 1484 1485 if ( $this->mSeekTo !== null ) { 1486 $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH, 1487 SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); 1488 $this->mSeekTo = null; 1489 } else { 1490 $result = sqlsrv_fetch_array( $res ); 1491 } 1492 1493 // MediaWiki expects us to return boolean false when there are no more rows instead of null 1494 if ( $result === null ) { 1495 return false; 1496 } 1497 1498 return $result; 1499 } 1500 1501 /** 1502 * @param int $row 1503 * @return bool 1504 */ 1505 public function seek( $row ) { 1506 $res = $this->result; 1507 1508 // check bounds 1509 $numRows = $this->db->numRows( $res ); 1510 $row = intval( $row ); 1511 1512 if ( $numRows === 0 ) { 1513 return false; 1514 } elseif ( $row < 0 || $row > $numRows - 1 ) { 1515 return false; 1516 } 1517 1518 // Unlike MySQL, the seek actually happens on the next access 1519 $this->mSeekTo = $row; 1520 return true; 1521 } 1522 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |