MediaWiki
REL1_24
|
00001 <?php 00031 class DatabaseMssql extends DatabaseBase { 00032 protected $mInsertId = null; 00033 protected $mLastResult = null; 00034 protected $mAffectedRows = null; 00035 protected $mSubqueryId = 0; 00036 protected $mScrollableCursor = true; 00037 protected $mPrepareStatements = true; 00038 protected $mBinaryColumnCache = null; 00039 protected $mBitColumnCache = null; 00040 protected $mIgnoreDupKeyErrors = false; 00041 00042 protected $mPort; 00043 00044 public function cascadingDeletes() { 00045 return true; 00046 } 00047 00048 public function cleanupTriggers() { 00049 return false; 00050 } 00051 00052 public function strictIPs() { 00053 return false; 00054 } 00055 00056 public function realTimestamps() { 00057 return false; 00058 } 00059 00060 public function implicitGroupby() { 00061 return false; 00062 } 00063 00064 public function implicitOrderby() { 00065 return false; 00066 } 00067 00068 public function functionalIndexes() { 00069 return true; 00070 } 00071 00072 public function unionSupportsOrderAndLimit() { 00073 return false; 00074 } 00075 00085 public function open( $server, $user, $password, $dbName ) { 00086 # Test for driver support, to avoid suppressed fatal error 00087 if ( !function_exists( 'sqlsrv_connect' ) ) { 00088 throw new DBConnectionError( 00089 $this, 00090 "Microsoft SQL Server Native (sqlsrv) functions missing. 00091 You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" 00092 ); 00093 } 00094 00095 global $wgDBport, $wgDBWindowsAuthentication; 00096 00097 # e.g. the class is being loaded 00098 if ( !strlen( $user ) ) { 00099 return null; 00100 } 00101 00102 $this->close(); 00103 $this->mServer = $server; 00104 $this->mPort = $wgDBport; 00105 $this->mUser = $user; 00106 $this->mPassword = $password; 00107 $this->mDBname = $dbName; 00108 00109 $connectionInfo = array(); 00110 00111 if ( $dbName ) { 00112 $connectionInfo['Database'] = $dbName; 00113 } 00114 00115 // Decide which auth scenerio to use 00116 // if we are using Windows auth, don't add credentials to $connectionInfo 00117 if ( !$wgDBWindowsAuthentication ) { 00118 $connectionInfo['UID'] = $user; 00119 $connectionInfo['PWD'] = $password; 00120 } 00121 00122 wfSuppressWarnings(); 00123 $this->mConn = sqlsrv_connect( $server, $connectionInfo ); 00124 wfRestoreWarnings(); 00125 00126 if ( $this->mConn === false ) { 00127 throw new DBConnectionError( $this, $this->lastError() ); 00128 } 00129 00130 $this->mOpened = true; 00131 00132 return $this->mConn; 00133 } 00134 00140 protected function closeConnection() { 00141 return sqlsrv_close( $this->mConn ); 00142 } 00143 00148 public function resultObject( $result ) { 00149 if ( empty( $result ) ) { 00150 return false; 00151 } elseif ( $result instanceof MssqlResultWrapper ) { 00152 return $result; 00153 } elseif ( $result === true ) { 00154 // Successful write query 00155 return $result; 00156 } else { 00157 return new MssqlResultWrapper( $this, $result ); 00158 } 00159 } 00160 00166 protected function doQuery( $sql ) { 00167 if ( $this->debug() ) { 00168 wfDebug( "SQL: [$sql]\n" ); 00169 } 00170 $this->offset = 0; 00171 00172 // several extensions seem to think that all databases support limits 00173 // via LIMIT N after the WHERE clause well, MSSQL uses SELECT TOP N, 00174 // so to catch any of those extensions we'll do a quick check for a 00175 // LIMIT clause and pass $sql through $this->LimitToTopN() which parses 00176 // the limit clause and passes the result to $this->limitResult(); 00177 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) { 00178 // massage LIMIT -> TopN 00179 $sql = $this->LimitToTopN( $sql ); 00180 } 00181 00182 // MSSQL doesn't have EXTRACT(epoch FROM XXX) 00183 if ( preg_match( '#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) { 00184 // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970 00185 $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql ); 00186 } 00187 00188 // perform query 00189 00190 // SQLSRV_CURSOR_STATIC is slower than SQLSRV_CURSOR_CLIENT_BUFFERED (one of the two is 00191 // needed if we want to be able to seek around the result set), however CLIENT_BUFFERED 00192 // has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty 00193 // strings make php throw a fatal error "Severe error translating Unicode" 00194 if ( $this->mScrollableCursor ) { 00195 $scrollArr = array( 'Scrollable' => SQLSRV_CURSOR_STATIC ); 00196 } else { 00197 $scrollArr = array(); 00198 } 00199 00200 if ( $this->mPrepareStatements ) { 00201 // we do prepare + execute so we can get its field metadata for later usage if desired 00202 $stmt = sqlsrv_prepare( $this->mConn, $sql, array(), $scrollArr ); 00203 $success = sqlsrv_execute( $stmt ); 00204 } else { 00205 $stmt = sqlsrv_query( $this->mConn, $sql, array(), $scrollArr ); 00206 $success = (bool)$stmt; 00207 } 00208 00209 if ( $this->mIgnoreDupKeyErrors ) { 00210 // ignore duplicate key errors, but nothing else 00211 // this emulates INSERT IGNORE in MySQL 00212 if ( $success === false ) { 00213 $errors = sqlsrv_errors( SQLSRV_ERR_ERRORS ); 00214 $success = true; 00215 00216 foreach ( $errors as $err ) { 00217 if ( $err['SQLSTATE'] == '23000' && $err['code'] == '2601' ) { 00218 continue; // duplicate key error caused by unique index 00219 } elseif ( $err['SQLSTATE'] == '23000' && $err['code'] == '2627' ) { 00220 continue; // duplicate key error caused by primary key 00221 } elseif ( $err['SQLSTATE'] == '01000' && $err['code'] == '3621' ) { 00222 continue; // generic "the statement has been terminated" error 00223 } 00224 00225 $success = false; // getting here means we got an error we weren't expecting 00226 break; 00227 } 00228 00229 if ( $success ) { 00230 $this->mAffectedRows = 0; 00231 return $stmt; 00232 } 00233 } 00234 } 00235 00236 if ( $success === false ) { 00237 return false; 00238 } 00239 // remember number of rows affected 00240 $this->mAffectedRows = sqlsrv_rows_affected( $stmt ); 00241 00242 return $stmt; 00243 } 00244 00245 public function freeResult( $res ) { 00246 if ( $res instanceof ResultWrapper ) { 00247 $res = $res->result; 00248 } 00249 00250 sqlsrv_free_stmt( $res ); 00251 } 00252 00257 public function fetchObject( $res ) { 00258 // $res is expected to be an instance of MssqlResultWrapper here 00259 return $res->fetchObject(); 00260 } 00261 00266 public function fetchRow( $res ) { 00267 return $res->fetchRow(); 00268 } 00269 00274 public function numRows( $res ) { 00275 if ( $res instanceof ResultWrapper ) { 00276 $res = $res->result; 00277 } 00278 00279 return sqlsrv_num_rows( $res ); 00280 } 00281 00286 public function numFields( $res ) { 00287 if ( $res instanceof ResultWrapper ) { 00288 $res = $res->result; 00289 } 00290 00291 return sqlsrv_num_fields( $res ); 00292 } 00293 00299 public function fieldName( $res, $n ) { 00300 if ( $res instanceof ResultWrapper ) { 00301 $res = $res->result; 00302 } 00303 00304 $metadata = sqlsrv_field_metadata( $res ); 00305 return $metadata[$n]['Name']; 00306 } 00307 00312 public function insertId() { 00313 return $this->mInsertId; 00314 } 00315 00321 public function dataSeek( $res, $row ) { 00322 return $res->seek( $row ); 00323 } 00324 00328 public function lastError() { 00329 $strRet = ''; 00330 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL ); 00331 if ( $retErrors != null ) { 00332 foreach ( $retErrors as $arrError ) { 00333 $strRet .= $this->formatError( $arrError ) . "\n"; 00334 } 00335 } else { 00336 $strRet = "No errors found"; 00337 } 00338 00339 return $strRet; 00340 } 00341 00346 private function formatError( $err ) { 00347 return '[SQLSTATE ' . $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message']; 00348 } 00349 00353 public function lastErrno() { 00354 $err = sqlsrv_errors( SQLSRV_ERR_ALL ); 00355 if ( $err !== null && isset( $err[0] ) ) { 00356 return $err[0]['code']; 00357 } else { 00358 return 0; 00359 } 00360 } 00361 00365 public function affectedRows() { 00366 return $this->mAffectedRows; 00367 } 00368 00384 public function select( $table, $vars, $conds = '', $fname = __METHOD__, 00385 $options = array(), $join_conds = array() 00386 ) { 00387 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); 00388 if ( isset( $options['EXPLAIN'] ) ) { 00389 try { 00390 $this->mScrollableCursor = false; 00391 $this->mPrepareStatements = false; 00392 $this->query( "SET SHOWPLAN_ALL ON" ); 00393 $ret = $this->query( $sql, $fname ); 00394 $this->query( "SET SHOWPLAN_ALL OFF" ); 00395 } catch ( DBQueryError $dqe ) { 00396 if ( isset( $options['FOR COUNT'] ) ) { 00397 // likely don't have privs for SHOWPLAN, so run a select count instead 00398 $this->query( "SET SHOWPLAN_ALL OFF" ); 00399 unset( $options['EXPLAIN'] ); 00400 $ret = $this->select( 00401 $table, 00402 'COUNT(*) AS EstimateRows', 00403 $conds, 00404 $fname, 00405 $options, 00406 $join_conds 00407 ); 00408 } else { 00409 // someone actually wanted the query plan instead of an est row count 00410 // let them know of the error 00411 $this->mScrollableCursor = true; 00412 $this->mPrepareStatements = true; 00413 throw $dqe; 00414 } 00415 } 00416 $this->mScrollableCursor = true; 00417 $this->mPrepareStatements = true; 00418 return $ret; 00419 } 00420 return $this->query( $sql, $fname ); 00421 } 00422 00436 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, 00437 $options = array(), $join_conds = array() 00438 ) { 00439 if ( isset( $options['EXPLAIN'] ) ) { 00440 unset( $options['EXPLAIN'] ); 00441 } 00442 00443 $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); 00444 00445 // try to rewrite aggregations of bit columns (currently MAX and MIN) 00446 if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) { 00447 $bitColumns = array(); 00448 if ( is_array( $table ) ) { 00449 foreach ( $table as $t ) { 00450 $bitColumns += $this->getBitColumns( $this->tableName( $t ) ); 00451 } 00452 } else { 00453 $bitColumns = $this->getBitColumns( $this->tableName( $table ) ); 00454 } 00455 00456 foreach ( $bitColumns as $col => $info ) { 00457 $replace = array( 00458 "MAX({$col})" => "MAX(CAST({$col} AS tinyint))", 00459 "MIN({$col})" => "MIN(CAST({$col} AS tinyint))", 00460 ); 00461 $sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql ); 00462 } 00463 } 00464 00465 return $sql; 00466 } 00467 00468 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, 00469 $fname = __METHOD__ 00470 ) { 00471 $this->mScrollableCursor = false; 00472 try { 00473 parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname ); 00474 } catch ( Exception $e ) { 00475 $this->mScrollableCursor = true; 00476 throw $e; 00477 } 00478 $this->mScrollableCursor = true; 00479 } 00480 00481 public function delete( $table, $conds, $fname = __METHOD__ ) { 00482 $this->mScrollableCursor = false; 00483 try { 00484 parent::delete( $table, $conds, $fname ); 00485 } catch ( Exception $e ) { 00486 $this->mScrollableCursor = true; 00487 throw $e; 00488 } 00489 $this->mScrollableCursor = true; 00490 } 00491 00505 public function estimateRowCount( $table, $vars = '*', $conds = '', 00506 $fname = __METHOD__, $options = array() 00507 ) { 00508 // http://msdn2.microsoft.com/en-us/library/aa259203.aspx 00509 $options['EXPLAIN'] = true; 00510 $options['FOR COUNT'] = true; 00511 $res = $this->select( $table, $vars, $conds, $fname, $options ); 00512 00513 $rows = -1; 00514 if ( $res ) { 00515 $row = $this->fetchRow( $res ); 00516 00517 if ( isset( $row['EstimateRows'] ) ) { 00518 $rows = $row['EstimateRows']; 00519 } 00520 } 00521 00522 return $rows; 00523 } 00524 00533 public function indexInfo( $table, $index, $fname = __METHOD__ ) { 00534 # This does not return the same info as MYSQL would, but that's OK 00535 # because MediaWiki never uses the returned value except to check for 00536 # the existance of indexes. 00537 $sql = "sp_helpindex '" . $table . "'"; 00538 $res = $this->query( $sql, $fname ); 00539 if ( !$res ) { 00540 return null; 00541 } 00542 00543 $result = array(); 00544 foreach ( $res as $row ) { 00545 if ( $row->index_name == $index ) { 00546 $row->Non_unique = !stristr( $row->index_description, "unique" ); 00547 $cols = explode( ", ", $row->index_keys ); 00548 foreach ( $cols as $col ) { 00549 $row->Column_name = trim( $col ); 00550 $result[] = clone $row; 00551 } 00552 } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) { 00553 $row->Non_unique = 0; 00554 $cols = explode( ", ", $row->index_keys ); 00555 foreach ( $cols as $col ) { 00556 $row->Column_name = trim( $col ); 00557 $result[] = clone $row; 00558 } 00559 } 00560 } 00561 00562 return empty( $result ) ? false : $result; 00563 } 00564 00580 public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) { 00581 # No rows to insert, easy just return now 00582 if ( !count( $arrToInsert ) ) { 00583 return true; 00584 } 00585 00586 if ( !is_array( $options ) ) { 00587 $options = array( $options ); 00588 } 00589 00590 $table = $this->tableName( $table ); 00591 00592 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row 00593 $arrToInsert = array( 0 => $arrToInsert ); // make everything multi row compatible 00594 } 00595 00596 // We know the table we're inserting into, get its identity column 00597 $identity = null; 00598 // strip matching square brackets and the db/schema from table name 00599 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); 00600 $tableRaw = array_pop( $tableRawArr ); 00601 $res = $this->doQuery( 00602 "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " . 00603 "WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" 00604 ); 00605 if ( $res && sqlsrv_has_rows( $res ) ) { 00606 // There is an identity for this table. 00607 $identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC ); 00608 $identity = array_pop( $identityArr ); 00609 } 00610 sqlsrv_free_stmt( $res ); 00611 00612 // Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF 00613 $binaryColumns = $this->getBinaryColumns( $table ); 00614 00615 // INSERT IGNORE is not supported by SQL Server 00616 // remove IGNORE from options list and set ignore flag to true 00617 if ( in_array( 'IGNORE', $options ) ) { 00618 $options = array_diff( $options, array( 'IGNORE' ) ); 00619 $this->mIgnoreDupKeyErrors = true; 00620 } 00621 00622 foreach ( $arrToInsert as $a ) { 00623 // start out with empty identity column, this is so we can return 00624 // it as a result of the insert logic 00625 $sqlPre = ''; 00626 $sqlPost = ''; 00627 $identityClause = ''; 00628 00629 // if we have an identity column 00630 if ( $identity ) { 00631 // iterate through 00632 foreach ( $a as $k => $v ) { 00633 if ( $k == $identity ) { 00634 if ( !is_null( $v ) ) { 00635 // there is a value being passed to us, 00636 // we need to turn on and off inserted identity 00637 $sqlPre = "SET IDENTITY_INSERT $table ON;"; 00638 $sqlPost = ";SET IDENTITY_INSERT $table OFF;"; 00639 } else { 00640 // we can't insert NULL into an identity column, 00641 // so remove the column from the insert. 00642 unset( $a[$k] ); 00643 } 00644 } 00645 } 00646 00647 // we want to output an identity column as result 00648 $identityClause = "OUTPUT INSERTED.$identity "; 00649 } 00650 00651 $keys = array_keys( $a ); 00652 00653 // Build the actual query 00654 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) . 00655 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES ("; 00656 00657 $first = true; 00658 foreach ( $a as $key => $value ) { 00659 if ( isset( $binaryColumns[$key] ) ) { 00660 $value = new MssqlBlob( $value ); 00661 } 00662 if ( $first ) { 00663 $first = false; 00664 } else { 00665 $sql .= ','; 00666 } 00667 if ( is_null( $value ) ) { 00668 $sql .= 'null'; 00669 } elseif ( is_array( $value ) || is_object( $value ) ) { 00670 if ( is_object( $value ) && $value instanceof Blob ) { 00671 $sql .= $this->addQuotes( $value ); 00672 } else { 00673 $sql .= $this->addQuotes( serialize( $value ) ); 00674 } 00675 } else { 00676 $sql .= $this->addQuotes( $value ); 00677 } 00678 } 00679 $sql .= ')' . $sqlPost; 00680 00681 // Run the query 00682 $this->mScrollableCursor = false; 00683 try { 00684 $ret = $this->query( $sql ); 00685 } catch ( Exception $e ) { 00686 $this->mScrollableCursor = true; 00687 $this->mIgnoreDupKeyErrors = false; 00688 throw $e; 00689 } 00690 $this->mScrollableCursor = true; 00691 00692 if ( !is_null( $identity ) ) { 00693 // then we want to get the identity column value we were assigned and save it off 00694 $row = $ret->fetchObject(); 00695 if( is_object( $row ) ){ 00696 $this->mInsertId = $row->$identity; 00697 } 00698 } 00699 } 00700 $this->mIgnoreDupKeyErrors = false; 00701 return $ret; 00702 } 00703 00719 public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, 00720 $insertOptions = array(), $selectOptions = array() 00721 ) { 00722 $this->mScrollableCursor = false; 00723 try { 00724 $ret = parent::insertSelect( 00725 $destTable, 00726 $srcTable, 00727 $varMap, 00728 $conds, 00729 $fname, 00730 $insertOptions, 00731 $selectOptions 00732 ); 00733 } catch ( Exception $e ) { 00734 $this->mScrollableCursor = true; 00735 throw $e; 00736 } 00737 $this->mScrollableCursor = true; 00738 00739 return $ret; 00740 } 00741 00765 function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { 00766 $table = $this->tableName( $table ); 00767 $binaryColumns = $this->getBinaryColumns( $table ); 00768 00769 $opts = $this->makeUpdateOptions( $options ); 00770 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns ); 00771 00772 if ( $conds !== array() && $conds !== '*' ) { 00773 $sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns ); 00774 } 00775 00776 $this->mScrollableCursor = false; 00777 try { 00778 $ret = $this->query( $sql ); 00779 } catch ( Exception $e ) { 00780 $this->mScrollableCursor = true; 00781 throw $e; 00782 } 00783 $this->mScrollableCursor = true; 00784 return true; 00785 } 00786 00803 public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = array() ) { 00804 if ( !is_array( $a ) ) { 00805 throw new DBUnexpectedError( $this, 00806 'DatabaseBase::makeList called with incorrect parameters' ); 00807 } 00808 00809 $first = true; 00810 $list = ''; 00811 00812 foreach ( $a as $field => $value ) { 00813 if ( $mode != LIST_NAMES && isset( $binaryColumns[$field] ) ) { 00814 if ( is_array( $value ) ) { 00815 foreach ( $value as &$v ) { 00816 $v = new MssqlBlob( $v ); 00817 } 00818 } else { 00819 $value = new MssqlBlob( $value ); 00820 } 00821 } 00822 00823 if ( !$first ) { 00824 if ( $mode == LIST_AND ) { 00825 $list .= ' AND '; 00826 } elseif ( $mode == LIST_OR ) { 00827 $list .= ' OR '; 00828 } else { 00829 $list .= ','; 00830 } 00831 } else { 00832 $first = false; 00833 } 00834 00835 if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) { 00836 $list .= "($value)"; 00837 } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) { 00838 $list .= "$value"; 00839 } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) { 00840 if ( count( $value ) == 0 ) { 00841 throw new MWException( __METHOD__ . ": empty input for field $field" ); 00842 } elseif ( count( $value ) == 1 ) { 00843 // Special-case single values, as IN isn't terribly efficient 00844 // Don't necessarily assume the single key is 0; we don't 00845 // enforce linear numeric ordering on other arrays here. 00846 $value = array_values( $value ); 00847 $list .= $field . " = " . $this->addQuotes( $value[0] ); 00848 } else { 00849 $list .= $field . " IN (" . $this->makeList( $value ) . ") "; 00850 } 00851 } elseif ( $value === null ) { 00852 if ( $mode == LIST_AND || $mode == LIST_OR ) { 00853 $list .= "$field IS "; 00854 } elseif ( $mode == LIST_SET ) { 00855 $list .= "$field = "; 00856 } 00857 $list .= 'NULL'; 00858 } else { 00859 if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { 00860 $list .= "$field = "; 00861 } 00862 $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value ); 00863 } 00864 } 00865 00866 return $list; 00867 } 00868 00874 public function textFieldSize( $table, $field ) { 00875 $table = $this->tableName( $table ); 00876 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns 00877 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'"; 00878 $res = $this->query( $sql ); 00879 $row = $this->fetchRow( $res ); 00880 $size = -1; 00881 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) { 00882 $size = $row['CHARACTER_MAXIMUM_LENGTH']; 00883 } 00884 00885 return $size; 00886 } 00887 00897 public function limitResult( $sql, $limit, $offset = false ) { 00898 if ( $offset === false || $offset == 0 ) { 00899 if ( strpos( $sql, "SELECT" ) === false ) { 00900 return "TOP {$limit} " . $sql; 00901 } else { 00902 return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi', 00903 'SELECT$1 TOP ' . $limit, $sql, 1 ); 00904 } 00905 } else { 00906 // This one is fun, we need to pull out the select list as well as any ORDER BY clause 00907 $select = $orderby = array(); 00908 $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select ); 00909 $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby ); 00910 $overOrder = $postOrder = ''; 00911 $first = $offset + 1; 00912 $last = $offset + $limit; 00913 $sub1 = 'sub_' . $this->mSubqueryId; 00914 $sub2 = 'sub_' . ( $this->mSubqueryId + 1 ); 00915 $this->mSubqueryId += 2; 00916 if ( !$s1 ) { 00917 // wat 00918 throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" ); 00919 } 00920 if ( !$s2 ) { 00921 // no ORDER BY 00922 $overOrder = 'ORDER BY (SELECT 1)'; 00923 } else { 00924 if ( !isset( $orderby[2] ) || !$orderby[2] ) { 00925 // don't need to strip it out if we're using a FOR XML clause 00926 $sql = str_replace( $orderby[1], '', $sql ); 00927 } 00928 $overOrder = $orderby[1]; 00929 $postOrder = ' ' . $overOrder; 00930 } 00931 $sql = "SELECT {$select[1]} 00932 FROM ( 00933 SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, * 00934 FROM ({$sql}) {$sub1} 00935 ) {$sub2} 00936 WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}"; 00937 00938 return $sql; 00939 } 00940 } 00941 00952 public function LimitToTopN( $sql ) { 00953 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset} 00954 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i'; 00955 if ( preg_match( $pattern, $sql, $matches ) ) { 00956 // row_count = $matches[4] 00957 $row_count = $matches[4]; 00958 // offset = $matches[3] OR $matches[6] 00959 $offset = $matches[3] or 00960 $offset = $matches[6] or 00961 $offset = false; 00962 00963 // strip the matching LIMIT clause out 00964 $sql = str_replace( $matches[0], '', $sql ); 00965 00966 return $this->limitResult( $sql, $row_count, $offset ); 00967 } 00968 00969 return $sql; 00970 } 00971 00975 public function getSoftwareLink() { 00976 return "[{{int:version-db-mssql-url}} MS SQL Server]"; 00977 } 00978 00982 public function getServerVersion() { 00983 $server_info = sqlsrv_server_info( $this->mConn ); 00984 $version = 'Error'; 00985 if ( isset( $server_info['SQLServerVersion'] ) ) { 00986 $version = $server_info['SQLServerVersion']; 00987 } 00988 00989 return $version; 00990 } 00991 00997 public function tableExists( $table, $fname = __METHOD__ ) { 00998 list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); 00999 01000 if ( $db !== false ) { 01001 // remote database 01002 wfDebug( "Attempting to call tableExists on a remote table" ); 01003 return false; 01004 } 01005 01006 if ( $schema === false ) { 01007 global $wgDBmwschema; 01008 $schema = $wgDBmwschema; 01009 } 01010 01011 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES 01012 WHERE TABLE_TYPE = 'BASE TABLE' 01013 AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" ); 01014 01015 if ( $res->numRows() ) { 01016 return true; 01017 } else { 01018 return false; 01019 } 01020 } 01021 01029 public function fieldExists( $table, $field, $fname = __METHOD__ ) { 01030 list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); 01031 01032 if ( $db !== false ) { 01033 // remote database 01034 wfDebug( "Attempting to call fieldExists on a remote table" ); 01035 return false; 01036 } 01037 01038 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS 01039 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); 01040 01041 if ( $res->numRows() ) { 01042 return true; 01043 } else { 01044 return false; 01045 } 01046 } 01047 01048 public function fieldInfo( $table, $field ) { 01049 list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); 01050 01051 if ( $db !== false ) { 01052 // remote database 01053 wfDebug( "Attempting to call fieldInfo on a remote table" ); 01054 return false; 01055 } 01056 01057 $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS 01058 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); 01059 01060 $meta = $res->fetchRow(); 01061 if ( $meta ) { 01062 return new MssqlField( $meta ); 01063 } 01064 01065 return false; 01066 } 01067 01072 protected function doBegin( $fname = __METHOD__ ) { 01073 sqlsrv_begin_transaction( $this->mConn ); 01074 $this->mTrxLevel = 1; 01075 } 01076 01081 protected function doCommit( $fname = __METHOD__ ) { 01082 sqlsrv_commit( $this->mConn ); 01083 $this->mTrxLevel = 0; 01084 } 01085 01091 protected function doRollback( $fname = __METHOD__ ) { 01092 sqlsrv_rollback( $this->mConn ); 01093 $this->mTrxLevel = 0; 01094 } 01095 01104 private function escapeIdentifier( $identifier ) { 01105 if ( strlen( $identifier ) == 0 ) { 01106 throw new MWException( "An identifier must not be empty" ); 01107 } 01108 if ( strlen( $identifier ) > 128 ) { 01109 throw new MWException( "The identifier '$identifier' is too long (max. 128)" ); 01110 } 01111 if ( ( strpos( $identifier, '[' ) !== false ) 01112 || ( strpos( $identifier, ']' ) !== false ) 01113 ) { 01114 // It may be allowed if you quoted with double quotation marks, but 01115 // that would break if QUOTED_IDENTIFIER is OFF 01116 throw new MWException( "Square brackets are not allowed in '$identifier'" ); 01117 } 01118 01119 return "[$identifier]"; 01120 } 01121 01126 public function strencode( $s ) { # Should not be called by us 01127 return str_replace( "'", "''", $s ); 01128 } 01129 01134 public function addQuotes( $s ) { 01135 if ( $s instanceof MssqlBlob ) { 01136 return $s->fetch(); 01137 } elseif ( $s instanceof Blob ) { 01138 // this shouldn't really ever be called, but it's here if needed 01139 // (and will quite possibly make the SQL error out) 01140 $blob = new MssqlBlob( $s->fetch() ); 01141 return $blob->fetch(); 01142 } else { 01143 if ( is_bool( $s ) ) { 01144 $s = $s ? 1 : 0; 01145 } 01146 return parent::addQuotes( $s ); 01147 } 01148 } 01149 01154 public function addIdentifierQuotes( $s ) { 01155 // http://msdn.microsoft.com/en-us/library/aa223962.aspx 01156 return '[' . $s . ']'; 01157 } 01158 01163 public function isQuotedIdentifier( $name ) { 01164 return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']'; 01165 } 01166 01171 public function selectDB( $db ) { 01172 try { 01173 $this->mDBname = $db; 01174 $this->query( "USE $db" ); 01175 return true; 01176 } catch ( Exception $e ) { 01177 return false; 01178 } 01179 } 01180 01186 public function makeSelectOptions( $options ) { 01187 $tailOpts = ''; 01188 $startOpts = ''; 01189 01190 $noKeyOptions = array(); 01191 foreach ( $options as $key => $option ) { 01192 if ( is_numeric( $key ) ) { 01193 $noKeyOptions[$option] = true; 01194 } 01195 } 01196 01197 $tailOpts .= $this->makeGroupByWithHaving( $options ); 01198 01199 $tailOpts .= $this->makeOrderBy( $options ); 01200 01201 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { 01202 $startOpts .= 'DISTINCT'; 01203 } 01204 01205 if ( isset( $noKeyOptions['FOR XML'] ) ) { 01206 // used in group concat field emulation 01207 $tailOpts .= " FOR XML PATH('')"; 01208 } 01209 01210 // we want this to be compatible with the output of parent::makeSelectOptions() 01211 return array( $startOpts, '', $tailOpts, '' ); 01212 } 01213 01218 public function getType() { 01219 return 'mssql'; 01220 } 01221 01226 public function buildConcat( $stringList ) { 01227 return implode( ' + ', $stringList ); 01228 } 01229 01247 public function buildGroupConcatField( $delim, $table, $field, $conds = '', 01248 $join_conds = array() 01249 ) { 01250 $gcsq = 'gcsq_' . $this->mSubqueryId; 01251 $this->mSubqueryId++; 01252 01253 $delimLen = strlen( $delim ); 01254 $fld = "{$field} + {$this->addQuotes( $delim )}"; 01255 $sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM (" 01256 . $this->selectSQLText( $table, $fld, $conds, null, array( 'FOR XML' ), $join_conds ) 01257 . ") {$gcsq} ({$field}))"; 01258 01259 return $sql; 01260 } 01261 01265 public function getSearchEngine() { 01266 return "SearchMssql"; 01267 } 01268 01275 private function getBinaryColumns( $table ) { 01276 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); 01277 $tableRaw = array_pop( $tableRawArr ); 01278 01279 if ( $this->mBinaryColumnCache === null ) { 01280 $this->populateColumnCaches(); 01281 } 01282 01283 return isset( $this->mBinaryColumnCache[$tableRaw] ) 01284 ? $this->mBinaryColumnCache[$tableRaw] 01285 : array(); 01286 } 01287 01292 private function getBitColumns( $table ) { 01293 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); 01294 $tableRaw = array_pop( $tableRawArr ); 01295 01296 if ( $this->mBitColumnCache === null ) { 01297 $this->populateColumnCaches(); 01298 } 01299 01300 return isset( $this->mBitColumnCache[$tableRaw] ) 01301 ? $this->mBitColumnCache[$tableRaw] 01302 : array(); 01303 } 01304 01305 private function populateColumnCaches() { 01306 $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*', 01307 array( 01308 'TABLE_CATALOG' => $this->mDBname, 01309 'TABLE_SCHEMA' => $this->mSchema, 01310 'DATA_TYPE' => array( 'varbinary', 'binary', 'image', 'bit' ) 01311 ) ); 01312 01313 $this->mBinaryColumnCache = array(); 01314 $this->mBitColumnCache = array(); 01315 foreach ( $res as $row ) { 01316 if ( $row->DATA_TYPE == 'bit' ) { 01317 $this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; 01318 } else { 01319 $this->mBinaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; 01320 } 01321 } 01322 } 01323 01329 function tableName( $name, $format = 'quoted' ) { 01330 # Replace reserved words with better ones 01331 switch ( $name ) { 01332 case 'user': 01333 return $this->realTableName( 'mwuser', $format ); 01334 default: 01335 return $this->realTableName( $name, $format ); 01336 } 01337 } 01338 01345 function realTableName( $name, $format = 'quoted' ) { 01346 $table = parent::tableName( $name, $format ); 01347 if ( $format == 'split' ) { 01348 // Used internally, we want the schema split off from the table name and returned 01349 // as a list with 3 elements (database, schema, table) 01350 $table = explode( '.', $table ); 01351 while ( count( $table ) < 3 ) { 01352 array_unshift( $table, false ); 01353 } 01354 } 01355 return $table; 01356 } 01357 01364 public function prepareStatements( $value = null ) { 01365 return wfSetVar( $this->mPrepareStatements, $value ); 01366 } 01367 01374 public function scrollableCursor( $value = null ) { 01375 return wfSetVar( $this->mScrollableCursor, $value ); 01376 } 01377 } // end DatabaseMssql class 01378 01384 class MssqlField implements Field { 01385 private $name, $tableName, $default, $max_length, $nullable, $type; 01386 01387 function __construct( $info ) { 01388 $this->name = $info['COLUMN_NAME']; 01389 $this->tableName = $info['TABLE_NAME']; 01390 $this->default = $info['COLUMN_DEFAULT']; 01391 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; 01392 $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' ); 01393 $this->type = $info['DATA_TYPE']; 01394 } 01395 01396 function name() { 01397 return $this->name; 01398 } 01399 01400 function tableName() { 01401 return $this->tableName; 01402 } 01403 01404 function defaultValue() { 01405 return $this->default; 01406 } 01407 01408 function maxLength() { 01409 return $this->max_length; 01410 } 01411 01412 function isNullable() { 01413 return $this->nullable; 01414 } 01415 01416 function type() { 01417 return $this->type; 01418 } 01419 } 01420 01421 class MssqlBlob extends Blob { 01422 public function __construct( $data ) { 01423 if ( $data instanceof MssqlBlob ) { 01424 return $data; 01425 } elseif ( $data instanceof Blob ) { 01426 $this->mData = $data->fetch(); 01427 } elseif ( is_array( $data ) && is_object( $data ) ) { 01428 $this->mData = serialize( $data ); 01429 } else { 01430 $this->mData = $data; 01431 } 01432 } 01433 01439 public function fetch() { 01440 if ( $this->mData === null ) { 01441 return 'null'; 01442 } 01443 01444 $ret = '0x'; 01445 $dataLength = strlen( $this->mData ); 01446 for ( $i = 0; $i < $dataLength; $i++ ) { 01447 $ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) ); 01448 } 01449 01450 return $ret; 01451 } 01452 } 01453 01454 class MssqlResultWrapper extends ResultWrapper { 01455 private $mSeekTo = null; 01456 01460 public function fetchObject() { 01461 $res = $this->result; 01462 01463 if ( $this->mSeekTo !== null ) { 01464 $result = sqlsrv_fetch_object( $res, 'stdClass', array(), 01465 SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); 01466 $this->mSeekTo = null; 01467 } else { 01468 $result = sqlsrv_fetch_object( $res ); 01469 } 01470 01471 // MediaWiki expects us to return boolean false when there are no more rows instead of null 01472 if ( $result === null ) { 01473 return false; 01474 } 01475 01476 return $result; 01477 } 01478 01482 public function fetchRow() { 01483 $res = $this->result; 01484 01485 if ( $this->mSeekTo !== null ) { 01486 $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH, 01487 SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); 01488 $this->mSeekTo = null; 01489 } else { 01490 $result = sqlsrv_fetch_array( $res ); 01491 } 01492 01493 // MediaWiki expects us to return boolean false when there are no more rows instead of null 01494 if ( $result === null ) { 01495 return false; 01496 } 01497 01498 return $result; 01499 } 01500 01505 public function seek( $row ) { 01506 $res = $this->result; 01507 01508 // check bounds 01509 $numRows = $this->db->numRows( $res ); 01510 $row = intval( $row ); 01511 01512 if ( $numRows === 0 ) { 01513 return false; 01514 } elseif ( $row < 0 || $row > $numRows - 1 ) { 01515 return false; 01516 } 01517 01518 // Unlike MySQL, the seek actually happens on the next access 01519 $this->mSeekTo = $row; 01520 return true; 01521 } 01522 }