MediaWiki  REL1_20
DatabaseMssql.php
Go to the documentation of this file.
00001 <?php
00030 class DatabaseMssql extends DatabaseBase {
00031         var $mInsertId = NULL;
00032         var $mLastResult = NULL;
00033         var $mAffectedRows = NULL;
00034 
00035         var $mPort;
00036 
00037         function cascadingDeletes() {
00038                 return true;
00039         }
00040         function cleanupTriggers() {
00041                 return true;
00042         }
00043         function strictIPs() {
00044                 return true;
00045         }
00046         function realTimestamps() {
00047                 return true;
00048         }
00049         function implicitGroupby() {
00050                 return false;
00051         }
00052         function implicitOrderby() {
00053                 return false;
00054         }
00055         function functionalIndexes() {
00056                 return true;
00057         }
00058         function unionSupportsOrderAndLimit() {
00059                 return false;
00060         }
00061 
00066         function open( $server, $user, $password, $dbName ) {
00067                 # Test for driver support, to avoid suppressed fatal error
00068                 if ( !function_exists( 'sqlsrv_connect' ) ) {
00069                         throw new DBConnectionError( $this, "MS Sql Server Native (sqlsrv) functions missing. You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" );
00070                 }
00071 
00072                 global $wgDBport;
00073 
00074                 if ( !strlen( $user ) ) { # e.g. the class is being loaded
00075                         return;
00076                 }
00077 
00078                 $this->close();
00079                 $this->mServer = $server;
00080                 $this->mPort = $wgDBport;
00081                 $this->mUser = $user;
00082                 $this->mPassword = $password;
00083                 $this->mDBname = $dbName;
00084 
00085                 $connectionInfo = array();
00086 
00087                 if( $dbName ) {
00088                         $connectionInfo['Database'] = $dbName;
00089                 }
00090 
00091                 // Start NT Auth Hack
00092                 // Quick and dirty work around to provide NT Auth designation support.
00093                 // Current solution requires installer to know to input 'ntauth' for both username and password
00094                 // to trigger connection via NT Auth. - ugly, ugly, ugly
00095                 // TO-DO: Make this better and add NT Auth choice to MW installer when SQL Server option is chosen.
00096                 $ntAuthUserTest = strtolower( $user );
00097                 $ntAuthPassTest = strtolower( $password );
00098 
00099                 // Decide which auth scenerio to use
00100                 if( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ){
00101                         // Don't add credentials to $connectionInfo
00102                 } else {
00103                         $connectionInfo['UID'] = $user;
00104                         $connectionInfo['PWD'] = $password;
00105                 }
00106                 // End NT Auth Hack
00107 
00108                 wfSuppressWarnings();
00109                 $this->mConn = sqlsrv_connect( $server, $connectionInfo );
00110                 wfRestoreWarnings();
00111 
00112                 if ( $this->mConn === false ) {
00113                         wfDebug( "DB connection error\n" );
00114                         wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
00115                         wfDebug( $this->lastError() . "\n" );
00116                         return false;
00117                 }
00118 
00119                 $this->mOpened = true;
00120                 return $this->mConn;
00121         }
00122 
00128         protected function closeConnection() {
00129                 return sqlsrv_close( $this->mConn );
00130         }
00131 
00132         protected function doQuery( $sql ) {
00133                 wfDebug( "SQL: [$sql]\n" );
00134                 $this->offset = 0;
00135 
00136                 // several extensions seem to think that all databases support limits via LIMIT N after the WHERE clause
00137                 // well, MSSQL uses SELECT TOP N, so to catch any of those extensions we'll do a quick check for a LIMIT
00138                 // clause and pass $sql through $this->LimitToTopN() which parses the limit clause and passes the result to
00139                 // $this->limitResult();
00140                 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
00141                         // massage LIMIT -> TopN
00142                         $sql = $this->LimitToTopN( $sql ) ;
00143                 }
00144 
00145                 // MSSQL doesn't have EXTRACT(epoch FROM XXX)
00146                 if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
00147                         // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
00148                         $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
00149                 }
00150 
00151                 // perform query
00152                 $stmt = sqlsrv_query( $this->mConn, $sql );
00153                 if ( $stmt == false ) {
00154                         $message = "A database error has occurred.  Did you forget to run maintenance/update.php after upgrading?  See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
00155                                 "Query: " . htmlentities( $sql ) . "\n" .
00156                                 "Function: " . __METHOD__ . "\n";
00157                         // process each error (our driver will give us an array of errors unlike other providers)
00158                         foreach ( sqlsrv_errors() as $error ) {
00159                                 $message .= $message . "ERROR[" . $error['code'] . "] " . $error['message'] . "\n";
00160                         }
00161 
00162                         throw new DBUnexpectedError( $this, $message );
00163                 }
00164                 // remember number of rows affected
00165                 $this->mAffectedRows = sqlsrv_rows_affected( $stmt );
00166 
00167                 // if it is a SELECT statement, or an insert with a request to output something we want to return a row.
00168                 if ( ( preg_match( '#\bSELECT\s#i', $sql ) ) ||
00169                         ( preg_match( '#\bINSERT\s#i', $sql ) && preg_match( '#\bOUTPUT\s+INSERTED\b#i', $sql ) ) ) {
00170                         // this is essentially a rowset, but Mediawiki calls these 'result'
00171                         // the rowset owns freeing the statement
00172                         $res = new MssqlResult( $stmt );
00173                 } else {
00174                         // otherwise we simply return it was successful, failure throws an exception
00175                         $res = true;
00176                 }
00177                 return $res;
00178         }
00179 
00180         function freeResult( $res ) {
00181                 if ( $res instanceof ResultWrapper ) {
00182                         $res = $res->result;
00183                 }
00184                 $res->free();
00185         }
00186 
00187         function fetchObject( $res ) {
00188                 if ( $res instanceof ResultWrapper ) {
00189                         $res = $res->result;
00190                 }
00191                 $row = $res->fetch( 'OBJECT' );
00192                 return $row;
00193         }
00194 
00195         function getErrors() {
00196                 $strRet = '';
00197                 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
00198                 if ( $retErrors != null ) {
00199                         foreach ( $retErrors as $arrError ) {
00200                                 $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n";
00201                                 $strRet .= "Error Code: " . $arrError[ 'code'] . "\n";
00202                                 $strRet .= "Message: " . $arrError[ 'message'] . "\n";
00203                         }
00204                 } else {
00205                         $strRet = "No errors found";
00206                 }
00207                 return $strRet;
00208         }
00209 
00210         function fetchRow( $res ) {
00211                 if ( $res instanceof ResultWrapper ) {
00212                         $res = $res->result;
00213                 }
00214                 $row = $res->fetch( SQLSRV_FETCH_BOTH );
00215                 return $row;
00216         }
00217 
00218         function numRows( $res ) {
00219                 if ( $res instanceof ResultWrapper ) {
00220                         $res = $res->result;
00221                 }
00222                 return ( $res ) ? $res->numrows() : 0;
00223         }
00224 
00225         function numFields( $res ) {
00226                 if ( $res instanceof ResultWrapper ) {
00227                         $res = $res->result;
00228                 }
00229                 return ( $res ) ? $res->numfields() : 0;
00230         }
00231 
00232         function fieldName( $res, $n ) {
00233                 if ( $res instanceof ResultWrapper ) {
00234                         $res = $res->result;
00235                 }
00236                 return ( $res ) ? $res->fieldname( $n ) : 0;
00237         }
00238 
00243         function insertId() {
00244                 return $this->mInsertId;
00245         }
00246 
00247         function dataSeek( $res, $row ) {
00248                 if ( $res instanceof ResultWrapper ) {
00249                         $res = $res->result;
00250                 }
00251                 return ( $res ) ? $res->seek( $row ) : false;
00252         }
00253 
00254         function lastError() {
00255                 if ( $this->mConn ) {
00256                         return $this->getErrors();
00257                 } else {
00258                         return "No database connection";
00259                 }
00260         }
00261 
00262         function lastErrno() {
00263                 $err = sqlsrv_errors( SQLSRV_ERR_ALL );
00264                 if ( $err[0] ) {
00265                         return $err[0]['code'];
00266                 } else {
00267                         return 0;
00268                 }
00269         }
00270 
00271         function affectedRows() {
00272                 return $this->mAffectedRows;
00273         }
00274 
00288         function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() )
00289         {
00290                 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
00291                 if ( isset( $options['EXPLAIN'] ) ) {
00292                         sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL ON;" );
00293                         $ret = $this->query( $sql, $fname );
00294                         sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" );
00295                         return $ret;
00296                 }
00297                 return $this->query( $sql, $fname );
00298         }
00299 
00313         function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
00314                 if ( isset( $options['EXPLAIN'] ) ) {
00315                         unset( $options['EXPLAIN'] );
00316                 }
00317                 return parent::selectSQLText(  $table, $vars, $conds, $fname, $options, $join_conds );
00318         }
00319 
00328         function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
00329                 $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
00330                 $res = $this->select( $table, $vars, $conds, $fname, $options );
00331 
00332                 $rows = -1;
00333                 if ( $res ) {
00334                         $row = $this->fetchRow( $res );
00335                         if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
00336                 }
00337                 return $rows;
00338         }
00339 
00345         function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
00346                 # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
00347                 # returned value except to check for the existance of indexes.
00348                 $sql = "sp_helpindex '" . $table . "'";
00349                 $res = $this->query( $sql, $fname );
00350                 if ( !$res ) {
00351                         return NULL;
00352                 }
00353 
00354                 $result = array();
00355                 foreach ( $res as $row ) {
00356                         if ( $row->index_name == $index ) {
00357                                 $row->Non_unique = !stristr( $row->index_description, "unique" );
00358                                 $cols = explode( ", ", $row->index_keys );
00359                                 foreach ( $cols as $col ) {
00360                                         $row->Column_name = trim( $col );
00361                                         $result[] = clone $row;
00362                                 }
00363                         } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
00364                                 $row->Non_unique = 0;
00365                                 $cols = explode( ", ", $row->index_keys );
00366                                 foreach ( $cols as $col ) {
00367                                         $row->Column_name = trim( $col );
00368                                         $result[] = clone $row;
00369                                 }
00370                         }
00371                 }
00372                 return empty( $result ) ? false : $result;
00373         }
00374 
00385         function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
00386                 # No rows to insert, easy just return now
00387                 if ( !count( $arrToInsert ) ) {
00388                         return true;
00389                 }
00390 
00391                 if ( !is_array( $options ) ) {
00392                         $options = array( $options );
00393                 }
00394 
00395                 $table = $this->tableName( $table );
00396 
00397                 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row
00398                         $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible
00399                 }
00400 
00401                 $allOk = true;
00402 
00403                 // We know the table we're inserting into, get its identity column
00404                 $identity = null;
00405                 $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name
00406                 $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" );
00407                 if( $res && $res->numrows() ){
00408                         // There is an identity for this table.
00409                         $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) );
00410                 }
00411                 unset( $res );
00412 
00413                 foreach ( $arrToInsert as $a ) {
00414                         // start out with empty identity column, this is so we can return it as a result of the insert logic
00415                         $sqlPre = '';
00416                         $sqlPost = '';
00417                         $identityClause = '';
00418 
00419                         // if we have an identity column
00420                         if( $identity ) {
00421                                 // iterate through
00422                                 foreach ($a as $k => $v ) {
00423                                         if ( $k == $identity ) {
00424                                                 if( !is_null($v) ){
00425                                                         // there is a value being passed to us, we need to turn on and off inserted identity
00426                                                         $sqlPre = "SET IDENTITY_INSERT $table ON;" ;
00427                                                         $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
00428 
00429                                                 } else {
00430                                                         // we can't insert NULL into an identity column, so remove the column from the insert.
00431                                                         unset( $a[$k] );
00432                                                 }
00433                                         }
00434                                 }
00435                                 $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result
00436                         }
00437 
00438                         $keys = array_keys( $a );
00439 
00440                         // INSERT IGNORE is not supported by SQL Server
00441                         // remove IGNORE from options list and set ignore flag to true
00442                         $ignoreClause = false;
00443                         foreach ( $options as $k => $v ) {
00444                                 if ( strtoupper( $v ) == "IGNORE" ) {
00445                                         unset( $options[$k] );
00446                                         $ignoreClause = true;
00447                                 }
00448                         }
00449 
00450                         // translate MySQL INSERT IGNORE to something SQL Server can use
00451                         // example:
00452                         // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
00453                         // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
00454                         if ( $ignoreClause ) {
00455                                 $prival = $a[$keys[0]];
00456                                 $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')";
00457                         }
00458 
00459                         // Build the actual query
00460                         $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
00461                                 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
00462 
00463                         $first = true;
00464                         foreach ( $a as $value ) {
00465                                 if ( $first ) {
00466                                         $first = false;
00467                                 } else {
00468                                         $sql .= ',';
00469                                 }
00470                                 if ( is_string( $value ) ) {
00471                                         $sql .= $this->addQuotes( $value );
00472                                 } elseif ( is_null( $value ) ) {
00473                                         $sql .= 'null';
00474                                 } elseif ( is_array( $value ) || is_object( $value ) ) {
00475                                         if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) {
00476                                                 $sql .= $this->addQuotes( $value );
00477                                         }  else {
00478                                                 $sql .= $this->addQuotes( serialize( $value ) );
00479                                         }
00480                                 } else {
00481                                         $sql .= $value;
00482                                 }
00483                         }
00484                         $sql .= ')' . $sqlPost;
00485 
00486                         // Run the query
00487                         $ret = sqlsrv_query( $this->mConn, $sql );
00488 
00489                         if ( $ret === false ) {
00490                                 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
00491                         } elseif ( $ret != NULL ) {
00492                                 // remember number of rows affected
00493                                 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
00494                                 if ( !is_null($identity) ) {
00495                                         // then we want to get the identity column value we were assigned and save it off
00496                                         $row = sqlsrv_fetch_object( $ret );
00497                                         $this->mInsertId = $row->$identity;
00498                                 }
00499                                 sqlsrv_free_stmt( $ret );
00500                                 continue;
00501                         }
00502                         $allOk = false;
00503                 }
00504                 return $allOk;
00505         }
00506 
00515         function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
00516                 $insertOptions = array(), $selectOptions = array() ) {
00517                 $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
00518 
00519                 if ( $ret === false ) {
00520                         throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), /*$sql*/ '', $fname );
00521                 } elseif ( $ret != NULL ) {
00522                         // remember number of rows affected
00523                         $this->mAffectedRows = sqlsrv_rows_affected( $ret );
00524                         return $ret;
00525                 }
00526                 return NULL;
00527         }
00528 
00533         function nextSequenceValue( $seqName ) {
00534                 if ( !$this->tableExists( 'sequence_' . $seqName ) ) {
00535                         sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" );
00536                 }
00537                 sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" );
00538                 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
00539                 $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't
00540 
00541                 sqlsrv_free_stmt( $ret );
00542                 $this->mInsertId = $row['id'];
00543                 return $row['id'];
00544         }
00545 
00550         function currentSequenceValue( $seqName ) {
00551                 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
00552                 if ( $ret !== false ) {
00553                         $row = sqlsrv_fetch_array( $ret );
00554                         sqlsrv_free_stmt( $ret );
00555                         return $row['id'];
00556                 } else {
00557                         return $this->nextSequenceValue( $seqName );
00558                 }
00559         }
00560 
00561         # Returns the size of a text field, or -1 for "unlimited"
00562         function textFieldSize( $table, $field ) {
00563                 $table = $this->tableName( $table );
00564                 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
00565                         WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
00566                 $res = $this->query( $sql );
00567                 $row = $this->fetchRow( $res );
00568                 $size = -1;
00569                 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) {
00570                         $size = $row['CHARACTER_MAXIMUM_LENGTH'];
00571                 }
00572                 return $size;
00573         }
00574 
00583         function limitResult( $sql, $limit, $offset = false ) {
00584                 if ( $offset === false || $offset == 0 ) {
00585                         if ( strpos( $sql, "SELECT" ) === false ) {
00586                                 return "TOP {$limit} " . $sql;
00587                         } else {
00588                                 return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 );
00589                         }
00590                 } else {
00591                         $sql = '
00592                                 SELECT * FROM (
00593                                   SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
00594                                         SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
00595                                   ) as sub2
00596                                 ) AS sub3
00597                                 WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit );
00598                         return $sql;
00599                 }
00600         }
00601 
00602         // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult()
00603         // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser.
00604         // This exists becase there are still too many extensions that don't use dynamic sql generation.
00605         function LimitToTopN( $sql ) {
00606                 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
00607                 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
00608                 if ( preg_match( $pattern, $sql, $matches ) ) {
00609                         // row_count = $matches[4]
00610                         $row_count = $matches[4];
00611                         // offset = $matches[3] OR $matches[6]
00612                         $offset = $matches[3] or
00613                                 $offset = $matches[6] or
00614                                 $offset = false;
00615 
00616                         // strip the matching LIMIT clause out
00617                         $sql = str_replace( $matches[0], '', $sql );
00618                         return $this->limitResult( $sql, $row_count, $offset );
00619                 }
00620                 return $sql;
00621         }
00622 
00623         function timestamp( $ts = 0 ) {
00624                 return wfTimestamp( TS_ISO_8601, $ts );
00625         }
00626 
00630         public static function getSoftwareLink() {
00631                 return "[http://www.microsoft.com/sql/ MS SQL Server]";
00632         }
00633 
00637         function getServerVersion() {
00638                 $server_info = sqlsrv_server_info( $this->mConn );
00639                 $version = 'Error';
00640                 if ( isset( $server_info['SQLServerVersion'] ) ) {
00641                         $version = $server_info['SQLServerVersion'];
00642                 }
00643                 return $version;
00644         }
00645 
00646         function tableExists ( $table, $fname = __METHOD__, $schema = false ) {
00647                 $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
00648                         WHERE table_type='BASE TABLE' AND table_name = '$table'" );
00649                 if ( $res === false ) {
00650                         print( "Error in tableExists query: " . $this->getErrors() );
00651                         return false;
00652                 }
00653                 if ( sqlsrv_fetch( $res ) ) {
00654                         return true;
00655                 } else {
00656                         return false;
00657                 }
00658         }
00659 
00664         function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
00665                 $table = $this->tableName( $table );
00666                 $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
00667                         WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
00668                 if ( $res === false ) {
00669                         print( "Error in fieldExists query: " . $this->getErrors() );
00670                         return false;
00671                 }
00672                 if ( sqlsrv_fetch( $res ) ) {
00673                         return true;
00674                 } else {
00675                         return false;
00676                 }
00677         }
00678 
00679         function fieldInfo( $table, $field ) {
00680                 $table = $this->tableName( $table );
00681                 $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
00682                         WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
00683                 if ( $res === false ) {
00684                         print( "Error in fieldInfo query: " . $this->getErrors() );
00685                         return false;
00686                 }
00687                 $meta = $this->fetchRow( $res );
00688                 if ( $meta ) {
00689                         return new MssqlField( $meta );
00690                 }
00691                 return false;
00692         }
00693 
00697         protected function doBegin( $fname = 'DatabaseMssql::begin' ) {
00698                 sqlsrv_begin_transaction( $this->mConn );
00699                 $this->mTrxLevel = 1;
00700         }
00701 
00705         protected function doCommit( $fname = 'DatabaseMssql::commit' ) {
00706                 sqlsrv_commit( $this->mConn );
00707                 $this->mTrxLevel = 0;
00708         }
00709 
00714         protected function doRollback( $fname = 'DatabaseMssql::rollback' ) {
00715                 sqlsrv_rollback( $this->mConn );
00716                 $this->mTrxLevel = 0;
00717         }
00718 
00725         private function escapeIdentifier( $identifier ) {
00726                 if ( strlen( $identifier ) == 0 ) {
00727                         throw new MWException( "An identifier must not be empty" );
00728                 }
00729                 if ( strlen( $identifier ) > 128 ) {
00730                         throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
00731                 }
00732                 if ( ( strpos( $identifier, '[' ) !== false ) || ( strpos( $identifier, ']' ) !== false ) ) {
00733                         // It may be allowed if you quoted with double quotation marks, but that would break if QUOTED_IDENTIFIER is OFF
00734                         throw new MWException( "You can't use square brackers in the identifier '$identifier'" );
00735                 }
00736                 return "[$identifier]";
00737         }
00738 
00744         function initial_setup( $dbName, $newUser, $loginPassword ) {
00745                 $dbName = $this->escapeIdentifier( $dbName );
00746 
00747                 // It is not clear what can be used as a login,
00748                 // From http://msdn.microsoft.com/en-us/library/ms173463.aspx
00749                 // a sysname may be the same as an identifier.
00750                 $newUser = $this->escapeIdentifier( $newUser );
00751                 $loginPassword = $this->addQuotes( $loginPassword );
00752 
00753                 $this->doQuery("CREATE DATABASE $dbName;");
00754                 $this->doQuery("USE $dbName;");
00755                 $this->doQuery("CREATE SCHEMA $dbName;");
00756                 $this->doQuery("
00757                                                 CREATE
00758                                                         LOGIN $newUser
00759                                                 WITH
00760                                                         PASSWORD=$loginPassword
00761                                                 ;
00762                                         ");
00763                 $this->doQuery("
00764                                                 CREATE
00765                                                         USER $newUser
00766                                                 FOR
00767                                                         LOGIN $newUser
00768                                                 WITH
00769                                                         DEFAULT_SCHEMA=$dbName
00770                                                 ;
00771                                         ");
00772                 $this->doQuery("
00773                                                 GRANT
00774                                                         BACKUP DATABASE,
00775                                                         BACKUP LOG,
00776                                                         CREATE DEFAULT,
00777                                                         CREATE FUNCTION,
00778                                                         CREATE PROCEDURE,
00779                                                         CREATE RULE,
00780                                                         CREATE TABLE,
00781                                                         CREATE VIEW,
00782                                                         CREATE FULLTEXT CATALOG
00783                                                 ON
00784                                                         DATABASE::$dbName
00785                                                 TO $newUser
00786                                                 ;
00787                                         ");
00788                 $this->doQuery("
00789                                                 GRANT
00790                                                         CONTROL
00791                                                 ON
00792                                                         SCHEMA::$dbName
00793                                                 TO $newUser
00794                                                 ;
00795                                         ");
00796 
00797 
00798         }
00799 
00800         function encodeBlob( $b ) {
00801         // we can't have zero's and such, this is a simple encoding to make sure we don't barf
00802                 return base64_encode( $b );
00803         }
00804 
00805         function decodeBlob( $b ) {
00806         // we can't have zero's and such, this is a simple encoding to make sure we don't barf
00807         return base64_decode( $b );
00808         }
00809 
00814         function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
00815                 $ret = array();
00816                 $retJOIN = array();
00817                 $use_index_safe = is_array( $use_index ) ? $use_index : array();
00818                 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
00819                 foreach ( $tables as $table ) {
00820                         // Is there a JOIN and INDEX clause for this table?
00821                         if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
00822                                 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
00823                                 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
00824                                 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
00825                                 $retJOIN[] = $tableClause;
00826                         // Is there an INDEX clause?
00827                         } elseif ( isset( $use_index_safe[$table] ) ) {
00828                                 $tableClause = $this->tableName( $table );
00829                                 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
00830                                 $ret[] = $tableClause;
00831                         // Is there a JOIN clause?
00832                         } elseif ( isset( $join_conds_safe[$table] ) ) {
00833                                 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
00834                                 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
00835                                 $retJOIN[] = $tableClause;
00836                         } else {
00837                                 $tableClause = $this->tableName( $table );
00838                                 $ret[] = $tableClause;
00839                         }
00840                 }
00841                 // We can't separate explicit JOIN clauses with ',', use ' ' for those
00842                 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
00843                 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
00844                 // Compile our final table clause
00845                 return implode( ' ', array( $straightJoins, $otherJoins ) );
00846         }
00847 
00848         function strencode( $s ) { # Should not be called by us
00849                 return str_replace( "'", "''", $s );
00850         }
00851 
00852         function addQuotes( $s ) {
00853                 if ( $s instanceof Blob ) {
00854                         return "'" . $s->fetch( $s ) . "'";
00855                 } else {
00856                         return parent::addQuotes( $s );
00857                 }
00858         }
00859 
00860         public function addIdentifierQuotes( $s ) {
00861                 // http://msdn.microsoft.com/en-us/library/aa223962.aspx
00862                 return '[' . $s . ']';
00863         }
00864 
00865         public function isQuotedIdentifier( $name ) {
00866                 return $name[0] == '[' && substr( $name, -1, 1 ) == ']';
00867         }
00868 
00869         function selectDB( $db ) {
00870                 return ( $this->query( "SET DATABASE $db" ) !== false );
00871         }
00872 
00880         function makeSelectOptions( $options ) {
00881                 $tailOpts = '';
00882                 $startOpts = '';
00883 
00884                 $noKeyOptions = array();
00885                 foreach ( $options as $key => $option ) {
00886                         if ( is_numeric( $key ) ) {
00887                                 $noKeyOptions[$option] = true;
00888                         }
00889                 }
00890 
00891                 if ( isset( $options['GROUP BY'] ) ) {
00892                         $tailOpts .= " GROUP BY {$options['GROUP BY']}";
00893                 }
00894                 if ( isset( $options['HAVING'] ) ) {
00895                         $tailOpts .= " HAVING {$options['GROUP BY']}";
00896                 }
00897                 if ( isset( $options['ORDER BY'] ) ) {
00898                         $tailOpts .= " ORDER BY {$options['ORDER BY']}";
00899                 }
00900 
00901                 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) {
00902                         $startOpts .= 'DISTINCT';
00903                 }
00904 
00905                 // we want this to be compatible with the output of parent::makeSelectOptions()
00906                 return array( $startOpts, '' , $tailOpts, '' );
00907         }
00908 
00913         function getType(){
00914                 return 'mssql';
00915         }
00916 
00917         function buildConcat( $stringList ) {
00918                 return implode( ' + ', $stringList );
00919         }
00920 
00921         public function getSearchEngine() {
00922                 return "SearchMssql";
00923         }
00924 
00930         public function getInfinity() {
00931                 return '3000-01-31 00:00:00.000';
00932         }
00933 
00934 } // end DatabaseMssql class
00935 
00941 class MssqlField implements Field {
00942         private $name, $tablename, $default, $max_length, $nullable, $type;
00943         function __construct ( $info ) {
00944                 $this->name = $info['COLUMN_NAME'];
00945                 $this->tablename = $info['TABLE_NAME'];
00946                 $this->default = $info['COLUMN_DEFAULT'];
00947                 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
00948                 $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' );
00949                 $this->type = $info['DATA_TYPE'];
00950         }
00951 
00952         function name() {
00953                 return $this->name;
00954         }
00955 
00956         function tableName() {
00957                 return $this->tableName;
00958         }
00959 
00960         function defaultValue() {
00961                 return $this->default;
00962         }
00963 
00964         function maxLength() {
00965                 return $this->max_length;
00966         }
00967 
00968         function isNullable() {
00969                 return $this->nullable;
00970         }
00971 
00972         function type() {
00973                 return $this->type;
00974         }
00975 }
00976 
00983 class MssqlResult {
00984 
00985         public function __construct( $queryresult = false ) {
00986                 $this->mCursor = 0;
00987                 $this->mRows = array();
00988                 $this->mNumFields = sqlsrv_num_fields( $queryresult );
00989                 $this->mFieldMeta = sqlsrv_field_metadata( $queryresult );
00990 
00991                 $rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC );
00992 
00993                 foreach( $rows as $row ) {
00994                         if ( $row !== null ) {
00995                                 foreach ( $row as $k => $v ) {
00996                                         if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object
00997                                                 $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" );
00998                                         }
00999                                 }
01000                                 $this->mRows[] = $row;// read results into memory, cursors are not supported
01001                         }
01002                 }
01003                 $this->mRowCount = count( $this->mRows );
01004                 sqlsrv_free_stmt( $queryresult );
01005         }
01006 
01007         private function array_to_obj( $array, &$obj ) {
01008                 foreach ( $array as $key => $value ) {
01009                         if ( is_array( $value ) ) {
01010                                 $obj->$key = new stdClass();
01011                                 $this->array_to_obj( $value, $obj->$key );
01012                         } else {
01013                                 if ( !empty( $key ) ) {
01014                                         $obj->$key = $value;
01015                                 }
01016                         }
01017                 }
01018                 return $obj;
01019         }
01020 
01021         public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) {
01022                 if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) {
01023                         return false;
01024                 }
01025                 $arrNum = array();
01026                 if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
01027                         foreach ( $this->mRows[$this->mCursor] as $value ) {
01028                                 $arrNum[] = $value;
01029                         }
01030                 }
01031                 switch( $mode ) {
01032                         case SQLSRV_FETCH_ASSOC:
01033                                 $ret = $this->mRows[$this->mCursor];
01034                                 break;
01035                         case SQLSRV_FETCH_NUMERIC:
01036                                 $ret = $arrNum;
01037                                 break;
01038                         case 'OBJECT':
01039                                 $o = new $object_class;
01040                                 $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o );
01041                                 break;
01042                         case SQLSRV_FETCH_BOTH:
01043                         default:
01044                                 $ret = $this->mRows[$this->mCursor] + $arrNum;
01045                                 break;
01046                 }
01047 
01048                 $this->mCursor++;
01049                 return $ret;
01050         }
01051 
01052         public function get( $pos, $fld ) {
01053                 return $this->mRows[$pos][$fld];
01054         }
01055 
01056         public function numrows() {
01057                 return $this->mRowCount;
01058         }
01059 
01060         public function seek( $iRow ) {
01061                 $this->mCursor = min( $iRow, $this->mRowCount );
01062         }
01063 
01064         public function numfields() {
01065                 return $this->mNumFields;
01066         }
01067 
01068         public function fieldname( $nr ) {
01069                 $arrKeys = array_keys( $this->mRows[0] );
01070                 return $arrKeys[$nr];
01071         }
01072 
01073         public function fieldtype( $nr ) {
01074                 $i = 0;
01075                 $intType = -1;
01076                 foreach ( $this->mFieldMeta as $meta ) {
01077                         if ( $nr == $i ) {
01078                                 $intType = $meta['Type'];
01079                                 break;
01080                         }
01081                         $i++;
01082                 }
01083                 // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
01084                 switch( $intType ) {
01085                         case SQLSRV_SQLTYPE_BIGINT:             $strType = 'bigint'; break;
01086                         case SQLSRV_SQLTYPE_BINARY:             $strType = 'binary'; break;
01087                         case SQLSRV_SQLTYPE_BIT:                        $strType = 'bit'; break;
01088                         case SQLSRV_SQLTYPE_CHAR:                       $strType = 'char'; break;
01089                         case SQLSRV_SQLTYPE_DATETIME:           $strType = 'datetime'; break;
01090                         case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break;
01091                         case SQLSRV_SQLTYPE_FLOAT:                      $strType = 'float'; break;
01092                         case SQLSRV_SQLTYPE_IMAGE:                      $strType = 'image'; break;
01093                         case SQLSRV_SQLTYPE_INT:                        $strType = 'int'; break;
01094                         case SQLSRV_SQLTYPE_MONEY:                      $strType = 'money'; break;
01095                         case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break;
01096                         case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break;
01097                         case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break;
01098                         // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break;
01099                         case SQLSRV_SQLTYPE_NTEXT:                      $strType = 'ntext'; break;
01100                         case SQLSRV_SQLTYPE_REAL:                       $strType = 'real'; break;
01101                         case SQLSRV_SQLTYPE_SMALLDATETIME:      $strType = 'smalldatetime'; break;
01102                         case SQLSRV_SQLTYPE_SMALLINT:           $strType = 'smallint'; break;
01103                         case SQLSRV_SQLTYPE_SMALLMONEY:         $strType = 'smallmoney'; break;
01104                         case SQLSRV_SQLTYPE_TEXT:                       $strType = 'text'; break;
01105                         case SQLSRV_SQLTYPE_TIMESTAMP:          $strType = 'timestamp'; break;
01106                         case SQLSRV_SQLTYPE_TINYINT:            $strType = 'tinyint'; break;
01107                         case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break;
01108                         case SQLSRV_SQLTYPE_UDT:                        $strType = 'UDT'; break;
01109                         case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break;
01110                         // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break;
01111                         case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break;
01112                         // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break;
01113                         case SQLSRV_SQLTYPE_XML:                        $strType = 'xml'; break;
01114                         default: $strType = $intType;
01115                 }
01116                 return $strType;
01117         }
01118 
01119         public function free() {
01120                 unset( $this->mRows );
01121                 return;
01122         }
01123 }