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