MediaWiki  REL1_24
DatabaseMssql.php
Go to the documentation of this file.
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 }