MediaWiki  REL1_21
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         function encodeBlob( $b ) {
00819         // we can't have zero's and such, this is a simple encoding to make sure we don't barf
00820                 return base64_encode( $b );
00821         }
00822 
00823         function decodeBlob( $b ) {
00824         // we can't have zero's and such, this is a simple encoding to make sure we don't barf
00825         return base64_decode( $b );
00826         }
00827 
00832         function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
00833                 $ret = array();
00834                 $retJOIN = array();
00835                 $use_index_safe = is_array( $use_index ) ? $use_index : array();
00836                 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
00837                 foreach ( $tables as $table ) {
00838                         // Is there a JOIN and INDEX clause for this table?
00839                         if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
00840                                 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
00841                                 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
00842                                 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
00843                                 $retJOIN[] = $tableClause;
00844                         // Is there an INDEX clause?
00845                         } elseif ( isset( $use_index_safe[$table] ) ) {
00846                                 $tableClause = $this->tableName( $table );
00847                                 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
00848                                 $ret[] = $tableClause;
00849                         // Is there a JOIN clause?
00850                         } elseif ( isset( $join_conds_safe[$table] ) ) {
00851                                 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
00852                                 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
00853                                 $retJOIN[] = $tableClause;
00854                         } else {
00855                                 $tableClause = $this->tableName( $table );
00856                                 $ret[] = $tableClause;
00857                         }
00858                 }
00859                 // We can't separate explicit JOIN clauses with ',', use ' ' for those
00860                 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
00861                 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
00862                 // Compile our final table clause
00863                 return implode( ' ', array( $straightJoins, $otherJoins ) );
00864         }
00865 
00866         function strencode( $s ) { # Should not be called by us
00867                 return str_replace( "'", "''", $s );
00868         }
00869 
00870         function addQuotes( $s ) {
00871                 if ( $s instanceof Blob ) {
00872                         return "'" . $s->fetch( $s ) . "'";
00873                 } else {
00874                         return parent::addQuotes( $s );
00875                 }
00876         }
00877 
00878         public function addIdentifierQuotes( $s ) {
00879                 // http://msdn.microsoft.com/en-us/library/aa223962.aspx
00880                 return '[' . $s . ']';
00881         }
00882 
00883         public function isQuotedIdentifier( $name ) {
00884                 return $name[0] == '[' && substr( $name, -1, 1 ) == ']';
00885         }
00886 
00887         function selectDB( $db ) {
00888                 return ( $this->query( "SET DATABASE $db" ) !== false );
00889         }
00890 
00898         function makeSelectOptions( $options ) {
00899                 $tailOpts = '';
00900                 $startOpts = '';
00901 
00902                 $noKeyOptions = array();
00903                 foreach ( $options as $key => $option ) {
00904                         if ( is_numeric( $key ) ) {
00905                                 $noKeyOptions[$option] = true;
00906                         }
00907                 }
00908 
00909                 $tailOpts .= $this->makeGroupByWithHaving( $options );
00910 
00911                 $tailOpts .= $this->makeOrderBy( $options );
00912 
00913                 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) {
00914                         $startOpts .= 'DISTINCT';
00915                 }
00916 
00917                 // we want this to be compatible with the output of parent::makeSelectOptions()
00918                 return array( $startOpts, '', $tailOpts, '' );
00919         }
00920 
00925         function getType() {
00926                 return 'mssql';
00927         }
00928 
00929         function buildConcat( $stringList ) {
00930                 return implode( ' + ', $stringList );
00931         }
00932 
00933         public function getSearchEngine() {
00934                 return "SearchMssql";
00935         }
00936 
00942         public function getInfinity() {
00943                 return '3000-01-31 00:00:00.000';
00944         }
00945 
00946 } // end DatabaseMssql class
00947 
00953 class MssqlField implements Field {
00954         private $name, $tablename, $default, $max_length, $nullable, $type;
00955         function __construct ( $info ) {
00956                 $this->name = $info['COLUMN_NAME'];
00957                 $this->tablename = $info['TABLE_NAME'];
00958                 $this->default = $info['COLUMN_DEFAULT'];
00959                 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
00960                 $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' );
00961                 $this->type = $info['DATA_TYPE'];
00962         }
00963 
00964         function name() {
00965                 return $this->name;
00966         }
00967 
00968         function tableName() {
00969                 return $this->tableName;
00970         }
00971 
00972         function defaultValue() {
00973                 return $this->default;
00974         }
00975 
00976         function maxLength() {
00977                 return $this->max_length;
00978         }
00979 
00980         function isNullable() {
00981                 return $this->nullable;
00982         }
00983 
00984         function type() {
00985                 return $this->type;
00986         }
00987 }
00988 
00995 class MssqlResult {
00996 
00997         public function __construct( $queryresult = false ) {
00998                 $this->mCursor = 0;
00999                 $this->mRows = array();
01000                 $this->mNumFields = sqlsrv_num_fields( $queryresult );
01001                 $this->mFieldMeta = sqlsrv_field_metadata( $queryresult );
01002 
01003                 $rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC );
01004 
01005                 foreach( $rows as $row ) {
01006                         if ( $row !== null ) {
01007                                 foreach ( $row as $k => $v ) {
01008                                         if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object
01009                                                 $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" );
01010                                         }
01011                                 }
01012                                 $this->mRows[] = $row;// read results into memory, cursors are not supported
01013                         }
01014                 }
01015                 $this->mRowCount = count( $this->mRows );
01016                 sqlsrv_free_stmt( $queryresult );
01017         }
01018 
01019         private function array_to_obj( $array, &$obj ) {
01020                 foreach ( $array as $key => $value ) {
01021                         if ( is_array( $value ) ) {
01022                                 $obj->$key = new stdClass();
01023                                 $this->array_to_obj( $value, $obj->$key );
01024                         } else {
01025                                 if ( !empty( $key ) ) {
01026                                         $obj->$key = $value;
01027                                 }
01028                         }
01029                 }
01030                 return $obj;
01031         }
01032 
01033         public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) {
01034                 if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) {
01035                         return false;
01036                 }
01037                 $arrNum = array();
01038                 if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
01039                         foreach ( $this->mRows[$this->mCursor] as $value ) {
01040                                 $arrNum[] = $value;
01041                         }
01042                 }
01043                 switch( $mode ) {
01044                         case SQLSRV_FETCH_ASSOC:
01045                                 $ret = $this->mRows[$this->mCursor];
01046                                 break;
01047                         case SQLSRV_FETCH_NUMERIC:
01048                                 $ret = $arrNum;
01049                                 break;
01050                         case 'OBJECT':
01051                                 $o = new $object_class;
01052                                 $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o );
01053                                 break;
01054                         case SQLSRV_FETCH_BOTH:
01055                         default:
01056                                 $ret = $this->mRows[$this->mCursor] + $arrNum;
01057                                 break;
01058                 }
01059 
01060                 $this->mCursor++;
01061                 return $ret;
01062         }
01063 
01064         public function get( $pos, $fld ) {
01065                 return $this->mRows[$pos][$fld];
01066         }
01067 
01068         public function numrows() {
01069                 return $this->mRowCount;
01070         }
01071 
01072         public function seek( $iRow ) {
01073                 $this->mCursor = min( $iRow, $this->mRowCount );
01074         }
01075 
01076         public function numfields() {
01077                 return $this->mNumFields;
01078         }
01079 
01080         public function fieldname( $nr ) {
01081                 $arrKeys = array_keys( $this->mRows[0] );
01082                 return $arrKeys[$nr];
01083         }
01084 
01085         public function fieldtype( $nr ) {
01086                 $i = 0;
01087                 $intType = -1;
01088                 foreach ( $this->mFieldMeta as $meta ) {
01089                         if ( $nr == $i ) {
01090                                 $intType = $meta['Type'];
01091                                 break;
01092                         }
01093                         $i++;
01094                 }
01095                 // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
01096                 switch( $intType ) {
01097                         case SQLSRV_SQLTYPE_BIGINT:             $strType = 'bigint'; break;
01098                         case SQLSRV_SQLTYPE_BINARY:             $strType = 'binary'; break;
01099                         case SQLSRV_SQLTYPE_BIT:                        $strType = 'bit'; break;
01100                         case SQLSRV_SQLTYPE_CHAR:                       $strType = 'char'; break;
01101                         case SQLSRV_SQLTYPE_DATETIME:           $strType = 'datetime'; break;
01102                         case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break;
01103                         case SQLSRV_SQLTYPE_FLOAT:                      $strType = 'float'; break;
01104                         case SQLSRV_SQLTYPE_IMAGE:                      $strType = 'image'; break;
01105                         case SQLSRV_SQLTYPE_INT:                        $strType = 'int'; break;
01106                         case SQLSRV_SQLTYPE_MONEY:                      $strType = 'money'; break;
01107                         case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break;
01108                         case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break;
01109                         case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break;
01110                         // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break;
01111                         case SQLSRV_SQLTYPE_NTEXT:                      $strType = 'ntext'; break;
01112                         case SQLSRV_SQLTYPE_REAL:                       $strType = 'real'; break;
01113                         case SQLSRV_SQLTYPE_SMALLDATETIME:      $strType = 'smalldatetime'; break;
01114                         case SQLSRV_SQLTYPE_SMALLINT:           $strType = 'smallint'; break;
01115                         case SQLSRV_SQLTYPE_SMALLMONEY:         $strType = 'smallmoney'; break;
01116                         case SQLSRV_SQLTYPE_TEXT:                       $strType = 'text'; break;
01117                         case SQLSRV_SQLTYPE_TIMESTAMP:          $strType = 'timestamp'; break;
01118                         case SQLSRV_SQLTYPE_TINYINT:            $strType = 'tinyint'; break;
01119                         case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break;
01120                         case SQLSRV_SQLTYPE_UDT:                        $strType = 'UDT'; break;
01121                         case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break;
01122                         // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break;
01123                         case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break;
01124                         // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break;
01125                         case SQLSRV_SQLTYPE_XML:                        $strType = 'xml'; break;
01126                         default: $strType = $intType;
01127                 }
01128                 return $strType;
01129         }
01130 
01131         public function free() {
01132                 unset( $this->mRows );
01133         }
01134 }