MediaWiki
REL1_23
|
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 true; 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 00345 private function formatError( $err ) { 00346 return '[SQLSTATE ' . $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message']; 00347 } 00348 00352 public function lastErrno() { 00353 $err = sqlsrv_errors( SQLSRV_ERR_ALL ); 00354 if ( $err !== null && isset( $err[0] ) ) { 00355 return $err[0]['code']; 00356 } else { 00357 return 0; 00358 } 00359 } 00360 00364 public function affectedRows() { 00365 return $this->mAffectedRows; 00366 } 00367 00383 public function select( $table, $vars, $conds = '', $fname = __METHOD__, 00384 $options = array(), $join_conds = array() 00385 ) { 00386 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); 00387 if ( isset( $options['EXPLAIN'] ) ) { 00388 try { 00389 $this->mScrollableCursor = false; 00390 $this->mPrepareStatements = false; 00391 $this->query( "SET SHOWPLAN_ALL ON" ); 00392 $ret = $this->query( $sql, $fname ); 00393 $this->query( "SET SHOWPLAN_ALL OFF" ); 00394 } catch ( DBQueryError $dqe ) { 00395 if ( isset( $options['FOR COUNT'] ) ) { 00396 // likely don't have privs for SHOWPLAN, so run a select count instead 00397 $this->query( "SET SHOWPLAN_ALL OFF" ); 00398 unset( $options['EXPLAIN'] ); 00399 $ret = $this->select( 00400 $table, 00401 'COUNT(*) AS EstimateRows', 00402 $conds, 00403 $fname, 00404 $options, 00405 $join_conds 00406 ); 00407 } else { 00408 // someone actually wanted the query plan instead of an est row count 00409 // let them know of the error 00410 $this->mScrollableCursor = true; 00411 $this->mPrepareStatements = true; 00412 throw $dqe; 00413 } 00414 } 00415 $this->mScrollableCursor = true; 00416 $this->mPrepareStatements = true; 00417 00418 return $ret; 00419 } 00420 00421 return $this->query( $sql, $fname ); 00422 } 00423 00437 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, 00438 $options = array(), $join_conds = array() 00439 ) { 00440 if ( isset( $options['EXPLAIN'] ) ) { 00441 unset( $options['EXPLAIN'] ); 00442 } 00443 00444 $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); 00445 00446 // try to rewrite aggregations of bit columns (currently MAX and MIN) 00447 if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) { 00448 $bitColumns = array(); 00449 if ( is_array( $table ) ) { 00450 foreach ( $table as $t ) { 00451 $bitColumns += $this->getBitColumns( $this->tableName( $t ) ); 00452 } 00453 } else { 00454 $bitColumns = $this->getBitColumns( $this->tableName( $table ) ); 00455 } 00456 00457 foreach ( $bitColumns as $col => $info ) { 00458 $replace = array( 00459 "MAX({$col})" => "MAX(CAST({$col} AS tinyint))", 00460 "MIN({$col})" => "MIN(CAST({$col} AS tinyint))", 00461 ); 00462 $sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql ); 00463 } 00464 } 00465 00466 return $sql; 00467 } 00468 00469 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, 00470 $fname = __METHOD__ 00471 ) { 00472 $this->mScrollableCursor = false; 00473 try { 00474 parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname ); 00475 } catch ( Exception $e ) { 00476 $this->mScrollableCursor = true; 00477 throw $e; 00478 } 00479 $this->mScrollableCursor = true; 00480 } 00481 00482 public function delete( $table, $conds, $fname = __METHOD__ ) { 00483 $this->mScrollableCursor = false; 00484 try { 00485 parent::delete( $table, $conds, $fname ); 00486 } catch ( Exception $e ) { 00487 $this->mScrollableCursor = true; 00488 throw $e; 00489 } 00490 $this->mScrollableCursor = true; 00491 } 00492 00506 public function estimateRowCount( $table, $vars = '*', $conds = '', 00507 $fname = __METHOD__, $options = array() 00508 ) { 00509 // http://msdn2.microsoft.com/en-us/library/aa259203.aspx 00510 $options['EXPLAIN'] = true; 00511 $options['FOR COUNT'] = true; 00512 $res = $this->select( $table, $vars, $conds, $fname, $options ); 00513 00514 $rows = -1; 00515 if ( $res ) { 00516 $row = $this->fetchRow( $res ); 00517 00518 if ( isset( $row['EstimateRows'] ) ) { 00519 $rows = $row['EstimateRows']; 00520 } 00521 } 00522 00523 return $rows; 00524 } 00525 00534 public function indexInfo( $table, $index, $fname = __METHOD__ ) { 00535 # This does not return the same info as MYSQL would, but that's OK 00536 # because MediaWiki never uses the returned value except to check for 00537 # the existance of indexes. 00538 $sql = "sp_helpindex '" . $table . "'"; 00539 $res = $this->query( $sql, $fname ); 00540 if ( !$res ) { 00541 return null; 00542 } 00543 00544 $result = array(); 00545 foreach ( $res as $row ) { 00546 if ( $row->index_name == $index ) { 00547 $row->Non_unique = !stristr( $row->index_description, "unique" ); 00548 $cols = explode( ", ", $row->index_keys ); 00549 foreach ( $cols as $col ) { 00550 $row->Column_name = trim( $col ); 00551 $result[] = clone $row; 00552 } 00553 } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) { 00554 $row->Non_unique = 0; 00555 $cols = explode( ", ", $row->index_keys ); 00556 foreach ( $cols as $col ) { 00557 $row->Column_name = trim( $col ); 00558 $result[] = clone $row; 00559 } 00560 } 00561 } 00562 00563 return empty( $result ) ? false : $result; 00564 } 00565 00581 public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) { 00582 # No rows to insert, easy just return now 00583 if ( !count( $arrToInsert ) ) { 00584 return true; 00585 } 00586 00587 if ( !is_array( $options ) ) { 00588 $options = array( $options ); 00589 } 00590 00591 $table = $this->tableName( $table ); 00592 00593 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row 00594 $arrToInsert = array( 0 => $arrToInsert ); // make everything multi row compatible 00595 } 00596 00597 // We know the table we're inserting into, get its identity column 00598 $identity = null; 00599 // strip matching square brackets and the db/schema from table name 00600 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); 00601 $tableRaw = array_pop( $tableRawArr ); 00602 $res = $this->doQuery( 00603 "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " . 00604 "WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" 00605 ); 00606 if ( $res && sqlsrv_has_rows( $res ) ) { 00607 // There is an identity for this table. 00608 $identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC ); 00609 $identity = array_pop( $identityArr ); 00610 } 00611 sqlsrv_free_stmt( $res ); 00612 00613 // Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF 00614 $binaryColumns = $this->getBinaryColumns( $table ); 00615 00616 foreach ( $arrToInsert as $a ) { 00617 // start out with empty identity column, this is so we can return 00618 // it as a result of the insert logic 00619 $sqlPre = ''; 00620 $sqlPost = ''; 00621 $identityClause = ''; 00622 00623 // if we have an identity column 00624 if ( $identity ) { 00625 // iterate through 00626 foreach ( $a as $k => $v ) { 00627 if ( $k == $identity ) { 00628 if ( !is_null( $v ) ) { 00629 // there is a value being passed to us, 00630 // we need to turn on and off inserted identity 00631 $sqlPre = "SET IDENTITY_INSERT $table ON;"; 00632 $sqlPost = ";SET IDENTITY_INSERT $table OFF;"; 00633 } else { 00634 // we can't insert NULL into an identity column, 00635 // so remove the column from the insert. 00636 unset( $a[$k] ); 00637 } 00638 } 00639 } 00640 00641 // we want to output an identity column as result 00642 $identityClause = "OUTPUT INSERTED.$identity "; 00643 } 00644 00645 $keys = array_keys( $a ); 00646 00647 // INSERT IGNORE is not supported by SQL Server 00648 // remove IGNORE from options list and set ignore flag to true 00649 $ignoreClause = false; 00650 if ( in_array( 'IGNORE', $options ) ) { 00651 $options = array_diff( $options, array( 'IGNORE' ) ); 00652 $this->mIgnoreDupKeyErrors = true; 00653 } 00654 00655 // Build the actual query 00656 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) . 00657 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES ("; 00658 00659 $first = true; 00660 foreach ( $a as $key => $value ) { 00661 if ( isset( $binaryColumns[$key] ) ) { 00662 $value = new MssqlBlob( $value ); 00663 } 00664 if ( $first ) { 00665 $first = false; 00666 } else { 00667 $sql .= ','; 00668 } 00669 if ( is_null( $value ) ) { 00670 $sql .= 'null'; 00671 } elseif ( is_array( $value ) || is_object( $value ) ) { 00672 if ( is_object( $value ) && $value instanceof Blob ) { 00673 $sql .= $this->addQuotes( $value ); 00674 } else { 00675 $sql .= $this->addQuotes( serialize( $value ) ); 00676 } 00677 } else { 00678 $sql .= $this->addQuotes( $value ); 00679 } 00680 } 00681 $sql .= ')' . $sqlPost; 00682 00683 // Run the query 00684 $this->mScrollableCursor = false; 00685 try { 00686 $ret = $this->query( $sql ); 00687 } catch ( Exception $e ) { 00688 $this->mScrollableCursor = true; 00689 $this->mIgnoreDupKeyErrors = false; 00690 throw $e; 00691 } 00692 $this->mScrollableCursor = true; 00693 $this->mIgnoreDupKeyErrors = false; 00694 00695 if ( !is_null( $identity ) ) { 00696 // then we want to get the identity column value we were assigned and save it off 00697 $row = $ret->fetchObject(); 00698 $this->mInsertId = $row->$identity; 00699 } 00700 } 00701 00702 return $ret; 00703 } 00704 00720 public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, 00721 $insertOptions = array(), $selectOptions = array() 00722 ) { 00723 $this->mScrollableCursor = false; 00724 try { 00725 $ret = parent::insertSelect( 00726 $destTable, 00727 $srcTable, 00728 $varMap, 00729 $conds, 00730 $fname, 00731 $insertOptions, 00732 $selectOptions 00733 ); 00734 } catch ( Exception $e ) { 00735 $this->mScrollableCursor = true; 00736 throw $e; 00737 } 00738 $this->mScrollableCursor = true; 00739 00740 return $ret; 00741 } 00742 00766 function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { 00767 $table = $this->tableName( $table ); 00768 $binaryColumns = $this->getBinaryColumns( $table ); 00769 00770 $opts = $this->makeUpdateOptions( $options ); 00771 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns ); 00772 00773 if ( $conds !== array() && $conds !== '*' ) { 00774 $sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns ); 00775 } 00776 00777 $this->mScrollableCursor = false; 00778 try { 00779 $ret = $this->query( $sql ); 00780 } catch ( Exception $e ) { 00781 $this->mScrollableCursor = true; 00782 throw $e; 00783 } 00784 $this->mScrollableCursor = true; 00785 return true; 00786 } 00787 00804 public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = array() ) { 00805 if ( !is_array( $a ) ) { 00806 throw new DBUnexpectedError( $this, 00807 'DatabaseBase::makeList called with incorrect parameters' ); 00808 } 00809 00810 $first = true; 00811 $list = ''; 00812 00813 foreach ( $a as $field => $value ) { 00814 if ( $mode != LIST_NAMES && isset( $binaryColumns[$field] ) ) { 00815 if ( is_array( $value ) ) { 00816 foreach ( $value as &$v ) { 00817 $v = new MssqlBlob( $v ); 00818 } 00819 } else { 00820 $value = new MssqlBlob( $value ); 00821 } 00822 } 00823 00824 if ( !$first ) { 00825 if ( $mode == LIST_AND ) { 00826 $list .= ' AND '; 00827 } elseif ( $mode == LIST_OR ) { 00828 $list .= ' OR '; 00829 } else { 00830 $list .= ','; 00831 } 00832 } else { 00833 $first = false; 00834 } 00835 00836 if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) { 00837 $list .= "($value)"; 00838 } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) { 00839 $list .= "$value"; 00840 } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) { 00841 if ( count( $value ) == 0 ) { 00842 throw new MWException( __METHOD__ . ": empty input for field $field" ); 00843 } elseif ( count( $value ) == 1 ) { 00844 // Special-case single values, as IN isn't terribly efficient 00845 // Don't necessarily assume the single key is 0; we don't 00846 // enforce linear numeric ordering on other arrays here. 00847 $value = array_values( $value ); 00848 $list .= $field . " = " . $this->addQuotes( $value[0] ); 00849 } else { 00850 $list .= $field . " IN (" . $this->makeList( $value ) . ") "; 00851 } 00852 } elseif ( $value === null ) { 00853 if ( $mode == LIST_AND || $mode == LIST_OR ) { 00854 $list .= "$field IS "; 00855 } elseif ( $mode == LIST_SET ) { 00856 $list .= "$field = "; 00857 } 00858 $list .= 'NULL'; 00859 } else { 00860 if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { 00861 $list .= "$field = "; 00862 } 00863 $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value ); 00864 } 00865 } 00866 00867 return $list; 00868 } 00869 00875 public function textFieldSize( $table, $field ) { 00876 $table = $this->tableName( $table ); 00877 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns 00878 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'"; 00879 $res = $this->query( $sql ); 00880 $row = $this->fetchRow( $res ); 00881 $size = -1; 00882 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) { 00883 $size = $row['CHARACTER_MAXIMUM_LENGTH']; 00884 } 00885 00886 return $size; 00887 } 00888 00898 public function limitResult( $sql, $limit, $offset = false ) { 00899 if ( $offset === false || $offset == 0 ) { 00900 if ( strpos( $sql, "SELECT" ) === false ) { 00901 return "TOP {$limit} " . $sql; 00902 } else { 00903 return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi', 00904 'SELECT$1 TOP ' . $limit, $sql, 1 ); 00905 } 00906 } else { 00907 // This one is fun, we need to pull out the select list as well as any ORDER BY clause 00908 $select = $orderby = array(); 00909 $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select ); 00910 $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby ); 00911 $overOrder = $postOrder = ''; 00912 $first = $offset + 1; 00913 $last = $offset + $limit; 00914 $sub1 = 'sub_' . $this->mSubqueryId; 00915 $sub2 = 'sub_' . ( $this->mSubqueryId + 1 ); 00916 $this->mSubqueryId += 2; 00917 if ( !$s1 ) { 00918 // wat 00919 throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" ); 00920 } 00921 if ( !$s2 ) { 00922 // no ORDER BY 00923 $overOrder = 'ORDER BY 1'; 00924 } else { 00925 if ( !isset( $orderby[2] ) || !$orderby[2] ) { 00926 // don't need to strip it out if we're using a FOR XML clause 00927 $sql = str_replace( $orderby[1], '', $sql ); 00928 } 00929 $overOrder = $orderby[1]; 00930 $postOrder = ' ' . $overOrder; 00931 } 00932 $sql = "SELECT {$select[1]} 00933 FROM ( 00934 SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, * 00935 FROM ({$sql}) {$sub1} 00936 ) {$sub2} 00937 WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}"; 00938 00939 return $sql; 00940 } 00941 } 00942 00953 public function LimitToTopN( $sql ) { 00954 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset} 00955 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i'; 00956 if ( preg_match( $pattern, $sql, $matches ) ) { 00957 // row_count = $matches[4] 00958 $row_count = $matches[4]; 00959 // offset = $matches[3] OR $matches[6] 00960 $offset = $matches[3] or 00961 $offset = $matches[6] or 00962 $offset = false; 00963 00964 // strip the matching LIMIT clause out 00965 $sql = str_replace( $matches[0], '', $sql ); 00966 00967 return $this->limitResult( $sql, $row_count, $offset ); 00968 } 00969 00970 return $sql; 00971 } 00972 00976 public function getSoftwareLink() { 00977 return "[{{int:version-db-mssql-url}} MS SQL Server]"; 00978 } 00979 00983 public function getServerVersion() { 00984 $server_info = sqlsrv_server_info( $this->mConn ); 00985 $version = 'Error'; 00986 if ( isset( $server_info['SQLServerVersion'] ) ) { 00987 $version = $server_info['SQLServerVersion']; 00988 } 00989 00990 return $version; 00991 } 00992 00998 public function tableExists( $table, $fname = __METHOD__ ) { 00999 list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); 01000 01001 if ( $db !== false ) { 01002 // remote database 01003 wfDebug( "Attempting to call tableExists on a remote table" ); 01004 return false; 01005 } 01006 01007 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES 01008 WHERE TABLE_TYPE = 'BASE TABLE' 01009 AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" ); 01010 01011 if ( $res->numRows() ) { 01012 return true; 01013 } else { 01014 return false; 01015 } 01016 } 01017 01025 public function fieldExists( $table, $field, $fname = __METHOD__ ) { 01026 list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); 01027 01028 if ( $db !== false ) { 01029 // remote database 01030 wfDebug( "Attempting to call fieldExists on a remote table" ); 01031 return false; 01032 } 01033 01034 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS 01035 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); 01036 01037 if ( $res->numRows() ) { 01038 return true; 01039 } else { 01040 return false; 01041 } 01042 } 01043 01044 public function fieldInfo( $table, $field ) { 01045 list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); 01046 01047 if ( $db !== false ) { 01048 // remote database 01049 wfDebug( "Attempting to call fieldInfo on a remote table" ); 01050 return false; 01051 } 01052 01053 $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS 01054 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); 01055 01056 $meta = $res->fetchRow(); 01057 if ( $meta ) { 01058 return new MssqlField( $meta ); 01059 } 01060 01061 return false; 01062 } 01063 01067 protected function doBegin( $fname = __METHOD__ ) { 01068 sqlsrv_begin_transaction( $this->mConn ); 01069 $this->mTrxLevel = 1; 01070 } 01071 01075 protected function doCommit( $fname = __METHOD__ ) { 01076 sqlsrv_commit( $this->mConn ); 01077 $this->mTrxLevel = 0; 01078 } 01079 01084 protected function doRollback( $fname = __METHOD__ ) { 01085 sqlsrv_rollback( $this->mConn ); 01086 $this->mTrxLevel = 0; 01087 } 01088 01097 private function escapeIdentifier( $identifier ) { 01098 if ( strlen( $identifier ) == 0 ) { 01099 throw new MWException( "An identifier must not be empty" ); 01100 } 01101 if ( strlen( $identifier ) > 128 ) { 01102 throw new MWException( "The identifier '$identifier' is too long (max. 128)" ); 01103 } 01104 if ( ( strpos( $identifier, '[' ) !== false ) 01105 || ( strpos( $identifier, ']' ) !== false ) 01106 ) { 01107 // It may be allowed if you quoted with double quotation marks, but 01108 // that would break if QUOTED_IDENTIFIER is OFF 01109 throw new MWException( "Square brackets are not allowed in '$identifier'" ); 01110 } 01111 01112 return "[$identifier]"; 01113 } 01114 01119 public function strencode( $s ) { # Should not be called by us 01120 return str_replace( "'", "''", $s ); 01121 } 01122 01127 public function addQuotes( $s ) { 01128 if ( $s instanceof MssqlBlob ) { 01129 return $s->fetch(); 01130 } elseif ( $s instanceof Blob ) { 01131 // this shouldn't really ever be called, but it's here if needed 01132 // (and will quite possibly make the SQL error out) 01133 $blob = new MssqlBlob( $s->fetch() ); 01134 return $blob->fetch(); 01135 } else { 01136 if ( is_bool( $s ) ) { 01137 $s = $s ? 1 : 0; 01138 } 01139 return parent::addQuotes( $s ); 01140 } 01141 } 01142 01147 public function addIdentifierQuotes( $s ) { 01148 // http://msdn.microsoft.com/en-us/library/aa223962.aspx 01149 return '[' . $s . ']'; 01150 } 01151 01156 public function isQuotedIdentifier( $name ) { 01157 return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']'; 01158 } 01159 01164 public function selectDB( $db ) { 01165 try { 01166 $this->mDBname = $db; 01167 $this->query( "USE $db" ); 01168 return true; 01169 } catch ( Exception $e ) { 01170 return false; 01171 } 01172 } 01173 01179 public function makeSelectOptions( $options ) { 01180 $tailOpts = ''; 01181 $startOpts = ''; 01182 01183 $noKeyOptions = array(); 01184 foreach ( $options as $key => $option ) { 01185 if ( is_numeric( $key ) ) { 01186 $noKeyOptions[$option] = true; 01187 } 01188 } 01189 01190 $tailOpts .= $this->makeGroupByWithHaving( $options ); 01191 01192 $tailOpts .= $this->makeOrderBy( $options ); 01193 01194 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { 01195 $startOpts .= 'DISTINCT'; 01196 } 01197 01198 if ( isset( $noKeyOptions['FOR XML'] ) ) { 01199 // used in group concat field emulation 01200 $tailOpts .= " FOR XML PATH('')"; 01201 } 01202 01203 // we want this to be compatible with the output of parent::makeSelectOptions() 01204 return array( $startOpts, '', $tailOpts, '' ); 01205 } 01206 01211 public function getType() { 01212 return 'mssql'; 01213 } 01214 01219 public function buildConcat( $stringList ) { 01220 return implode( ' + ', $stringList ); 01221 } 01222 01240 public function buildGroupConcatField( $delim, $table, $field, $conds = '', 01241 $join_conds = array() 01242 ) { 01243 $gcsq = 'gcsq_' . $this->mSubqueryId; 01244 $this->mSubqueryId++; 01245 01246 $delimLen = strlen( $delim ); 01247 $fld = "{$field} + {$this->addQuotes( $delim )}"; 01248 $sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM (" 01249 . $this->selectSQLText( $table, $fld, $conds, null, array( 'FOR XML' ), $join_conds ) 01250 . ") {$gcsq} ({$field}))"; 01251 01252 return $sql; 01253 } 01254 01258 public function getSearchEngine() { 01259 return "SearchMssql"; 01260 } 01261 01268 private function getBinaryColumns( $table ) { 01269 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); 01270 $tableRaw = array_pop( $tableRawArr ); 01271 01272 if ( $this->mBinaryColumnCache === null ) { 01273 $this->populateColumnCaches(); 01274 } 01275 01276 return isset( $this->mBinaryColumnCache[$tableRaw] ) 01277 ? $this->mBinaryColumnCache[$tableRaw] 01278 : array(); 01279 } 01280 01285 private function getBitColumns( $table ) { 01286 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); 01287 $tableRaw = array_pop( $tableRawArr ); 01288 01289 if ( $this->mBitColumnCache === null ) { 01290 $this->populateColumnCaches(); 01291 } 01292 01293 return isset( $this->mBitColumnCache[$tableRaw] ) 01294 ? $this->mBitColumnCache[$tableRaw] 01295 : array(); 01296 } 01297 01301 private function populateColumnCaches() { 01302 $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*', 01303 array( 01304 'TABLE_CATALOG' => $this->mDBname, 01305 'TABLE_SCHEMA' => $this->mSchema, 01306 'DATA_TYPE' => array( 'varbinary', 'binary', 'image', 'bit' ) 01307 ) ); 01308 01309 $this->mBinaryColumnCache = array(); 01310 $this->mBitColumnCache = array(); 01311 foreach ( $res as $row ) { 01312 if ( $row->DATA_TYPE == 'bit' ) { 01313 $this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; 01314 } else { 01315 $this->mBinaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; 01316 } 01317 } 01318 } 01319 01325 function tableName( $name, $format = 'quoted' ) { 01326 # Replace reserved words with better ones 01327 switch ( $name ) { 01328 case 'user': 01329 return $this->realTableName( 'mwuser', $format ); 01330 default: 01331 return $this->realTableName( $name, $format ); 01332 } 01333 } 01334 01341 function realTableName( $name, $format = 'quoted' ) { 01342 $table = parent::tableName( $name, $format ); 01343 if ( $format == 'split' ) { 01344 // Used internally, we want the schema split off from the table name and returned 01345 // as a list with 3 elements (database, schema, table) 01346 $table = explode( '.', $table ); 01347 if ( count( $table ) == 2 ) { 01348 array_unshift( $table, false ); 01349 } 01350 } 01351 return $table; 01352 } 01353 01360 public function prepareStatements( $value = null ) { 01361 return wfSetVar( $this->mPrepareStatements, $value ); 01362 } 01363 01370 public function scrollableCursor( $value = null ) { 01371 return wfSetVar( $this->mScrollableCursor, $value ); 01372 } 01373 } // end DatabaseMssql class 01374 01380 class MssqlField implements Field { 01381 private $name, $tableName, $default, $max_length, $nullable, $type; 01382 01383 function __construct( $info ) { 01384 $this->name = $info['COLUMN_NAME']; 01385 $this->tableName = $info['TABLE_NAME']; 01386 $this->default = $info['COLUMN_DEFAULT']; 01387 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; 01388 $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' ); 01389 $this->type = $info['DATA_TYPE']; 01390 } 01391 01392 function name() { 01393 return $this->name; 01394 } 01395 01396 function tableName() { 01397 return $this->tableName; 01398 } 01399 01400 function defaultValue() { 01401 return $this->default; 01402 } 01403 01404 function maxLength() { 01405 return $this->max_length; 01406 } 01407 01408 function isNullable() { 01409 return $this->nullable; 01410 } 01411 01412 function type() { 01413 return $this->type; 01414 } 01415 } 01416 01417 class MssqlBlob extends Blob { 01418 public function __construct( $data ) { 01419 if ( $data instanceof MssqlBlob ) { 01420 return $data; 01421 } elseif ( $data instanceof Blob ) { 01422 $this->mData = $data->fetch(); 01423 } elseif ( is_array( $data ) && is_object( $data ) ) { 01424 $this->mData = serialize( $data ); 01425 } else { 01426 $this->mData = $data; 01427 } 01428 } 01429 01435 public function fetch() { 01436 if ( $this->mData === null ) { 01437 return 'null'; 01438 } 01439 01440 $ret = '0x'; 01441 $dataLength = strlen( $this->mData ); 01442 for ( $i = 0; $i < $dataLength; $i++ ) { 01443 $ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) ); 01444 } 01445 01446 return $ret; 01447 } 01448 } 01449 01450 class MssqlResultWrapper extends ResultWrapper { 01451 private $mSeekTo = null; 01452 01456 public function fetchObject() { 01457 $res = $this->result; 01458 01459 if ( $this->mSeekTo !== null ) { 01460 $result = sqlsrv_fetch_object( $res, 'stdClass', array(), 01461 SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); 01462 $this->mSeekTo = null; 01463 } else { 01464 $result = sqlsrv_fetch_object( $res ); 01465 } 01466 01467 // MediaWiki expects us to return boolean false when there are no more rows instead of null 01468 if ( $result === null ) { 01469 return false; 01470 } 01471 01472 return $result; 01473 } 01474 01478 public function fetchRow() { 01479 $res = $this->result; 01480 01481 if ( $this->mSeekTo !== null ) { 01482 $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH, 01483 SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); 01484 $this->mSeekTo = null; 01485 } else { 01486 $result = sqlsrv_fetch_array( $res ); 01487 } 01488 01489 // MediaWiki expects us to return boolean false when there are no more rows instead of null 01490 if ( $result === null ) { 01491 return false; 01492 } 01493 01494 return $result; 01495 } 01496 01501 public function seek( $row ) { 01502 $res = $this->result; 01503 01504 // check bounds 01505 $numRows = $this->db->numRows( $res ); 01506 $row = intval( $row ); 01507 01508 if ( $numRows === 0 ) { 01509 return false; 01510 } elseif ( $row < 0 || $row > $numRows - 1 ) { 01511 return false; 01512 } 01513 01514 // Unlike MySQL, the seek actually happens on the next access 01515 $this->mSeekTo = $row; 01516 return true; 01517 } 01518 }