]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/db/DatabaseMssql.php
MediaWiki 1.17.0
[autoinstalls/mediawiki.git] / includes / db / DatabaseMssql.php
1 <?php
2 /**
3  * This is the MS SQL Server Native database abstraction layer.
4  *
5  * @file
6  * @ingroup Database
7  * @author Joel Penner <a-joelpe at microsoft dot com>
8  * @author Chris Pucci <a-cpucci at microsoft dot com>
9  * @author Ryan Biesemeyer <v-ryanbi at microsoft dot com>
10  */
11
12 /**
13  * @ingroup Database
14  */
15 class DatabaseMssql extends DatabaseBase {
16         var $mInsertId = NULL;
17         var $mLastResult = NULL;
18         var $mAffectedRows = NULL;
19
20         function cascadingDeletes() {
21                 return true;
22         }
23         function cleanupTriggers() {
24                 return true;
25         }
26         function strictIPs() {
27                 return true;
28         }
29         function realTimestamps() {
30                 return true;
31         }
32         function implicitGroupby() {
33                 return false;
34         }
35         function implicitOrderby() {
36                 return false;
37         }
38         function functionalIndexes() {
39                 return true;
40         }
41         function unionSupportsOrderAndLimit() {
42                 return false;
43         }
44
45         static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) {
46                 return new DatabaseMssql( $server, $user, $password, $dbName, $flags );
47         }
48
49         /**
50          * Usually aborts on failure
51          */
52         function open( $server, $user, $password, $dbName ) {
53                 # Test for driver support, to avoid suppressed fatal error
54                 if ( !function_exists( 'sqlsrv_connect' ) ) {
55                         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" );
56                 }
57
58                 global $wgDBport;
59
60                 if ( !strlen( $user ) ) { # e.g. the class is being loaded
61                         return;
62                 }
63
64                 $this->close();
65                 $this->mServer = $server;
66                 $this->mPort = $wgDBport;
67                 $this->mUser = $user;
68                 $this->mPassword = $password;
69                 $this->mDBname = $dbName;
70
71                 $connectionInfo = array();
72
73                 if( $dbName ) {
74                         $connectionInfo['Database'] = $dbName;
75                 }
76
77                 // Start NT Auth Hack
78                 // Quick and dirty work around to provide NT Auth designation support.
79                 // Current solution requires installer to know to input 'ntauth' for both username and password
80                 // to trigger connection via NT Auth. - ugly, ugly, ugly
81                 // TO-DO: Make this better and add NT Auth choice to MW installer when SQL Server option is chosen.
82                 $ntAuthUserTest = strtolower( $user );
83                 $ntAuthPassTest = strtolower( $password );
84
85                 // Decide which auth scenerio to use
86                 if( ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) ){
87                         // Don't add credentials to $connectionInfo
88                 } else {
89                         $connectionInfo['UID'] = $user;
90                         $connectionInfo['PWD'] = $password;
91                 }
92                 // End NT Auth Hack
93
94                 $this->mConn = @sqlsrv_connect( $server, $connectionInfo );
95
96                 if ( $this->mConn === false ) {
97                         wfDebug( "DB connection error\n" );
98                         wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
99                         wfDebug( $this->lastError() . "\n" );
100                         return false;
101                 }
102
103                 $this->mOpened = true;
104                 return $this->mConn;
105         }
106
107         /**
108          * Closes a database connection, if it is open
109          * Returns success, true if already closed
110          */
111         function close() {
112                 $this->mOpened = false;
113                 if ( $this->mConn ) {
114                         return sqlsrv_close( $this->mConn );
115                 } else {
116                         return true;
117                 }
118         }
119
120         function doQuery( $sql ) {
121                 wfDebug( "SQL: [$sql]\n" );
122                 $this->offset = 0;
123
124                 // several extensions seem to think that all databases support limits via LIMIT N after the WHERE clause
125                 // well, MSSQL uses SELECT TOP N, so to catch any of those extensions we'll do a quick check for a LIMIT
126                 // clause and pass $sql through $this->LimitToTopN() which parses the limit clause and passes the result to
127                 // $this->limitResult();
128                 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
129                         // massage LIMIT -> TopN
130                         $sql = $this->LimitToTopN( $sql ) ;
131                 }
132
133                 // MSSQL doesn't have EXTRACT(epoch FROM XXX)
134                 if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
135                         // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
136                         $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
137                 }
138
139                 // perform query
140                 $stmt = sqlsrv_query( $this->mConn, $sql );
141                 if ( $stmt == false ) {
142                         $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" .
143                                 "Query: " . htmlentities( $sql ) . "\n" .
144                                 "Function: " . __METHOD__ . "\n";
145                         // process each error (our driver will give us an array of errors unlike other providers)
146                         foreach ( sqlsrv_errors() as $error ) {
147                                 $message .= $message . "ERROR[" . $error['code'] . "] " . $error['message'] . "\n";
148                         }
149
150                         throw new DBUnexpectedError( $this, $message );
151                 }
152                 // remember number of rows affected
153                 $this->mAffectedRows = sqlsrv_rows_affected( $stmt );
154
155                 // if it is a SELECT statement, or an insert with a request to output something we want to return a row.
156                 if ( ( preg_match( '#\bSELECT\s#i', $sql ) ) ||
157                         ( preg_match( '#\bINSERT\s#i', $sql ) && preg_match( '#\bOUTPUT\s+INSERTED\b#i', $sql ) ) ) {
158                         // this is essentially a rowset, but Mediawiki calls these 'result'
159                         // the rowset owns freeing the statement
160                         $res = new MssqlResult( $stmt );
161                 } else {
162                         // otherwise we simply return it was successful, failure throws an exception
163                         $res = true;
164                 }
165                 return $res;
166         }
167
168         function freeResult( $res ) {
169                 if ( $res instanceof ResultWrapper ) {
170                         $res = $res->result;
171                 }
172                 $res->free();
173         }
174
175         function fetchObject( $res ) {
176                 if ( $res instanceof ResultWrapper ) {
177                         $res = $res->result;
178                 }
179                 $row = $res->fetch( 'OBJECT' );
180                 return $row;
181         }
182
183         function getErrors() {
184                 $strRet = '';
185                 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
186                 if ( $retErrors != null ) {
187                         foreach ( $retErrors as $arrError ) {
188                                 $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n";
189                                 $strRet .= "Error Code: " . $arrError[ 'code'] . "\n";
190                                 $strRet .= "Message: " . $arrError[ 'message'] . "\n";
191                         }
192                 } else {
193                         $strRet = "No errors found";
194                 }
195                 return $strRet;
196         }
197
198         function fetchRow( $res ) {
199                 if ( $res instanceof ResultWrapper ) {
200                         $res = $res->result;
201                 }
202                 $row = $res->fetch( SQLSRV_FETCH_BOTH );
203                 return $row;
204         }
205
206         function numRows( $res ) {
207                 if ( $res instanceof ResultWrapper ) {
208                         $res = $res->result;
209                 }
210                 return ( $res ) ? $res->numrows() : 0;
211         }
212
213         function numFields( $res ) {
214                 if ( $res instanceof ResultWrapper ) {
215                         $res = $res->result;
216                 }
217                 return ( $res ) ? $res->numfields() : 0;
218         }
219
220         function fieldName( $res, $n ) {
221                 if ( $res instanceof ResultWrapper ) {
222                         $res = $res->result;
223                 }
224                 return ( $res ) ? $res->fieldname( $n ) : 0;
225         }
226
227         /**
228          * This must be called after nextSequenceVal
229          */
230         function insertId() {
231                 return $this->mInsertId;
232         }
233
234         function dataSeek( $res, $row ) {
235                 if ( $res instanceof ResultWrapper ) {
236                         $res = $res->result;
237                 }
238                 return ( $res ) ? $res->seek( $row ) : false;
239         }
240
241         function lastError() {
242                 if ( $this->mConn ) {
243                         return $this->getErrors();
244                 }
245                 else {
246                         return "No database connection";
247                 }
248         }
249
250         function lastErrno() {
251                 $err = sqlsrv_errors( SQLSRV_ERR_ALL );
252                 if ( $err[0] ) return $err[0]['code'];
253                 else return 0;
254         }
255
256         function affectedRows() {
257                 return $this->mAffectedRows;
258         }
259
260         /**
261          * SELECT wrapper
262          *
263          * @param $table   Mixed: array or string, table name(s) (prefix auto-added)
264          * @param $vars    Mixed: array or string, field name(s) to be retrieved
265          * @param $conds   Mixed: array or string, condition(s) for WHERE
266          * @param $fname   String: calling function name (use __METHOD__) for logs/profiling
267          * @param $options Array: associative array of options (e.g. array('GROUP BY' => 'page_title')),
268          *                 see Database::makeSelectOptions code for list of supported stuff
269          * @param $join_conds Array: Associative array of table join conditions (optional)
270          *                                                 (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
271          * @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure
272          */
273         function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() )
274         {
275                 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
276                 if ( isset( $options['EXPLAIN'] ) ) {
277                         sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL ON;" );
278                         $ret = $this->query( $sql, $fname );
279                         sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" );
280                         return $ret;
281                 }
282                 return $this->query( $sql, $fname );
283         }
284
285         /**
286          * SELECT wrapper
287          *
288          * @param $table   Mixed:  Array or string, table name(s) (prefix auto-added)
289          * @param $vars    Mixed:  Array or string, field name(s) to be retrieved
290          * @param $conds   Mixed:  Array or string, condition(s) for WHERE
291          * @param $fname   String: Calling function name (use __METHOD__) for logs/profiling
292          * @param $options Array:  Associative array of options (e.g. array('GROUP BY' => 'page_title')),
293          *                 see Database::makeSelectOptions code for list of supported stuff
294          * @param $join_conds Array: Associative array of table join conditions (optional)
295          *                    (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
296          * @return string, the SQL text
297          */
298         function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
299                 if ( isset( $options['EXPLAIN'] ) ) {
300                         unset( $options['EXPLAIN'] );
301                 }
302                 return parent::selectSQLText(  $table, $vars, $conds, $fname, $options, $join_conds );
303         }
304
305         /**
306          * Estimate rows in dataset
307          * Returns estimated count, based on SHOWPLAN_ALL output
308          * This is not necessarily an accurate estimate, so use sparingly
309          * Returns -1 if count cannot be found
310          * Takes same arguments as Database::select()
311          */
312         function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
313                 $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
314                 $res = $this->select( $table, $vars, $conds, $fname, $options );
315
316                 $rows = -1;
317                 if ( $res ) {
318                         $row = $this->fetchRow( $res );
319                         if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
320                 }
321                 return $rows;
322         }
323
324
325         /**
326          * Returns information about an index
327          * If errors are explicitly ignored, returns NULL on failure
328          */
329         function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
330                 # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
331                 # returned value except to check for the existance of indexes.
332                 $sql = "sp_helpindex '" . $table . "'";
333                 $res = $this->query( $sql, $fname );
334                 if ( !$res ) {
335                         return NULL;
336                 }
337
338                 $result = array();
339                 foreach ( $res as $row ) {
340                         if ( $row->index_name == $index ) {
341                                 $row->Non_unique = !stristr( $row->index_description, "unique" );
342                                 $cols = explode( ", ", $row->index_keys );
343                                 foreach ( $cols as $col ) {
344                                         $row->Column_name = trim( $col );
345                                         $result[] = clone $row;
346                                 }
347                         } else if ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
348                                 $row->Non_unique = 0;
349                                 $cols = explode( ", ", $row->index_keys );
350                                 foreach ( $cols as $col ) {
351                                         $row->Column_name = trim( $col );
352                                         $result[] = clone $row;
353                                 }
354                         }
355                 }
356                 return empty( $result ) ? false : $result;
357         }
358
359         /**
360          * INSERT wrapper, inserts an array into a table
361          *
362          * $arrToInsert may be a single associative array, or an array of these with numeric keys, for
363          * multi-row insert.
364          *
365          * Usually aborts on failure
366          * If errors are explicitly ignored, returns success
367          */
368         function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
369                 # No rows to insert, easy just return now
370                 if ( !count( $arrToInsert ) ) {
371                         return true;
372                 }
373
374                 if ( !is_array( $options ) ) {
375                         $options = array( $options );
376                 }
377
378                 $table = $this->tableName( $table );
379
380                 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row
381                         $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible
382                 }
383
384                 $allOk = true;
385
386
387                 // We know the table we're inserting into, get its identity column
388                 $identity = null;
389                 $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name
390                 $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" );
391                 if( $res && $res->numrows() ){
392                         // There is an identity for this table.
393                         $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) );
394                 }
395                 unset( $res );
396
397                 foreach ( $arrToInsert as $a ) {
398                         // start out with empty identity column, this is so we can return it as a result of the insert logic
399                         $sqlPre = '';
400                         $sqlPost = '';
401                         $identityClause = '';
402
403                         // if we have an identity column
404                         if( $identity ) {
405                                 // iterate through
406                                 foreach ($a as $k => $v ) {
407                                         if ( $k == $identity ) {
408                                                 if( !is_null($v) ){
409                                                         // there is a value being passed to us, we need to turn on and off inserted identity
410                                                         $sqlPre = "SET IDENTITY_INSERT $table ON;" ;
411                                                         $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
412
413                                                 } else {
414                                                         // we can't insert NULL into an identity column, so remove the column from the insert.
415                                                         unset( $a[$k] );
416                                                 }
417                                         }
418                                 }
419                                 $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result
420                         }
421
422                         $keys = array_keys( $a );
423
424
425                         // INSERT IGNORE is not supported by SQL Server
426                         // remove IGNORE from options list and set ignore flag to true
427                         $ignoreClause = false;
428                         foreach ( $options as $k => $v ) {
429                                 if ( strtoupper( $v ) == "IGNORE" ) {
430                                         unset( $options[$k] );
431                                         $ignoreClause = true;
432                                 }
433                         }
434
435                         // translate MySQL INSERT IGNORE to something SQL Server can use
436                         // example:
437                         // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
438                         // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
439                         if ( $ignoreClause == true ) {
440                                 $prival = $a[$keys[0]];
441                                 $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')";
442                         }
443
444                         // Build the actual query
445                         $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
446                                 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
447
448                         $first = true;
449                         foreach ( $a as $value ) {
450                                 if ( $first ) {
451                                         $first = false;
452                                 } else {
453                                         $sql .= ',';
454                                 }
455                                 if ( is_string( $value ) ) {
456                                         $sql .= $this->addIdentifierQuotes( $value );
457                                 } elseif ( is_null( $value ) ) {
458                                         $sql .= 'null';
459                                 } elseif ( is_array( $value ) || is_object( $value ) ) {
460                                         if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) {
461                                                 $sql .= $this->addIdentifierQuotes( $value->fetch() );
462                                         }  else {
463                                                 $sql .= $this->addIdentifierQuotes( serialize( $value ) );
464                                         }
465                                 } else {
466                                         $sql .= $value;
467                                 }
468                         }
469                         $sql .= ')' . $sqlPost;
470
471                         // Run the query
472                         $ret = sqlsrv_query( $this->mConn, $sql );
473
474                         if ( $ret === false ) {
475                                 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
476                         } elseif ( $ret != NULL ) {
477                                 // remember number of rows affected
478                                 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
479                                 if ( !is_null($identity) ) {
480                                         // then we want to get the identity column value we were assigned and save it off
481                                         $row = sqlsrv_fetch_object( $ret );
482                                         $this->mInsertId = $row->$identity;
483                                 }
484                                 sqlsrv_free_stmt( $ret );
485                                 continue;
486                         }
487                         $allOk = false;
488                 }
489                 return $allOk;
490         }
491
492         /**
493          * INSERT SELECT wrapper
494          * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
495          * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
496          * $conds may be "*" to copy the whole table
497          * srcTable may be an array of tables.
498          */
499         function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
500                 $insertOptions = array(), $selectOptions = array() )
501         {
502                 $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
503
504                 if ( $ret === false ) {
505                         throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
506                 } elseif ( $ret != NULL ) {
507                         // remember number of rows affected
508                         $this->mAffectedRows = sqlsrv_rows_affected( $ret );
509                         return $ret;
510                 }
511                 return NULL;
512         }
513
514         /**
515          * Format a table name ready for use in constructing an SQL query
516          *
517          * This does two important things: it brackets table names which as necessary,
518          * and it adds a table prefix if there is one.
519          *
520          * All functions of this object which require a table name call this function
521          * themselves. Pass the canonical name to such functions. This is only needed
522          * when calling query() directly.
523          *
524          * @param $name String: database table name
525          */
526         function tableName( $name ) {
527                 global $wgSharedDB;
528                 # Skip quoted literals
529                 if ( $name != '' && $name { 0 } != '[' ) {
530                         if ( $this->mTablePrefix !== '' &&  strpos( '.', $name ) === false ) {
531                                 $name = "{$this->mTablePrefix}$name";
532                         }
533                         if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
534                                 $name = "[$wgSharedDB].[$name]";
535                         } else {
536                                 # Standard quoting
537                                 if ( $name != '' ) $name = "[$name]";
538                         }
539                 }
540                 return $name;
541         }
542
543         /**
544          * Return the next in a sequence, save the value for retrieval via insertId()
545          */
546         function nextSequenceValue( $seqName ) {
547                 if ( !$this->tableExists( 'sequence_' . $seqName ) ) {
548                         sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" );
549                 }
550                 sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" );
551                 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
552                 $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't
553
554                 sqlsrv_free_stmt( $ret );
555                 $this->mInsertId = $row['id'];
556                 return $row['id'];
557         }
558
559         /**
560          * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
561          */
562         function currentSequenceValue( $seqName ) {
563                 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
564                 if ( $ret !== false ) {
565                         $row = sqlsrv_fetch_array( $ret );
566                         sqlsrv_free_stmt( $ret );
567                         return $row['id'];
568                 } else {
569                         return $this->nextSequenceValue( $seqName );
570                 }
571         }
572
573
574         # REPLACE query wrapper
575         # MSSQL simulates this with a DELETE followed by INSERT
576         # $row is the row to insert, an associative array
577         # $uniqueIndexes is an array of indexes. Each element may be either a
578         # field name or an array of field names
579         #
580         # It may be more efficient to leave off unique indexes which are unlikely to collide.
581         # However if you do this, you run the risk of encountering errors which wouldn't have
582         # occurred in MySQL
583         function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMssql::replace' ) {
584                 $table = $this->tableName( $table );
585
586                 if ( count( $rows ) == 0 ) {
587                         return;
588                 }
589
590                 # Single row case
591                 if ( !is_array( reset( $rows ) ) ) {
592                         $rows = array( $rows );
593                 }
594
595                 foreach ( $rows as $row ) {
596                         # Delete rows which collide
597                         if ( $uniqueIndexes ) {
598                                 $sql = "DELETE FROM $table WHERE ";
599                                 $first = true;
600                                 foreach ( $uniqueIndexes as $index ) {
601                                         if ( $first ) {
602                                                 $first = false;
603                                                 $sql .= "(";
604                                         } else {
605                                                 $sql .= ') OR (';
606                                         }
607                                         if ( is_array( $index ) ) {
608                                                 $first2 = true;
609                                                 foreach ( $index as $col ) {
610                                                         if ( $first2 ) {
611                                                                 $first2 = false;
612                                                         } else {
613                                                                 $sql .= ' AND ';
614                                                         }
615                                                         $sql .= $col . '=' . $this->addQuotes( $row[$col] );
616                                                 }
617                                         } else {
618                                                 $sql .= $index . '=' . $this->addQuotes( $row[$index] );
619                                         }
620                                 }
621                                 $sql .= ')';
622                                 $this->query( $sql, $fname );
623                         }
624
625                         # Now insert the row
626                         $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) . ') VALUES (' .
627                                 $this->makeList( $row, LIST_COMMA ) . ')';
628                         $this->query( $sql, $fname );
629                 }
630         }
631
632         # DELETE where the condition is a join
633         function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseMssql::deleteJoin" ) {
634                 if ( !$conds ) {
635                         throw new DBUnexpectedError( $this, 'DatabaseMssql::deleteJoin() called with empty $conds' );
636                 }
637
638                 $delTable = $this->tableName( $delTable );
639                 $joinTable = $this->tableName( $joinTable );
640                 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
641                 if ( $conds != '*' ) {
642                         $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
643                 }
644                 $sql .= ')';
645
646                 $this->query( $sql, $fname );
647         }
648
649         # Returns the size of a text field, or -1 for "unlimited"
650         function textFieldSize( $table, $field ) {
651                 $table = $this->tableName( $table );
652                 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
653                         WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
654                 $res = $this->query( $sql );
655                 $row = $this->fetchRow( $res );
656                 $size = -1;
657                 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) $size = $row['CHARACTER_MAXIMUM_LENGTH'];
658                 return $size;
659         }
660
661         /**
662          * Construct a LIMIT query with optional offset
663          * This is used for query pages
664          * $sql string SQL query we will append the limit too
665          * $limit integer the SQL limit
666          * $offset integer the SQL offset (default false)
667          */
668         function limitResult( $sql, $limit, $offset = false ) {
669                 if ( $offset === false || $offset == 0 ) {
670                         if ( strpos( $sql, "SELECT" ) === false ) {
671                                 return "TOP {$limit} " . $sql;
672                         } else {
673                                 return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 );
674                         }
675                 } else {
676                         $sql = '
677                                 SELECT * FROM (
678                                   SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
679                                         SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
680                                   ) as sub2
681                                 ) AS sub3
682                                 WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit );
683                         return $sql;
684                 }
685         }
686
687         // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult()
688         // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser.
689         // This exists becase there are still too many extensions that don't use dynamic sql generation.
690         function LimitToTopN( $sql ) {
691                 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
692                 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
693                 if ( preg_match( $pattern, $sql, $matches ) ) {
694                         // row_count = $matches[4]
695                         $row_count = $matches[4];
696                         // offset = $matches[3] OR $matches[6]
697                         $offset = $matches[3] or
698                                 $offset = $matches[6] or
699                                 $offset = false;
700
701                         // strip the matching LIMIT clause out
702                         $sql = str_replace( $matches[0], '', $sql );
703                         return $this->limitResult( $sql, $row_count, $offset );
704                 }
705                 return $sql;
706         }
707
708         // MSSQL does support this, but documentation is too thin to make a generalized
709         // function for this. Apparently UPDATE TOP (N) works, but the sort order
710         // may not be what we're expecting so the top n results may be a random selection.
711         // TODO: Implement properly.
712         function limitResultForUpdate( $sql, $num ) {
713                 return $sql;
714         }
715
716
717         function timestamp( $ts = 0 ) {
718                 return wfTimestamp( TS_ISO_8601, $ts );
719         }
720
721         /**
722          * @return string wikitext of a link to the server software's web site
723          */
724         public static function getSoftwareLink() {
725                 return "[http://www.microsoft.com/sql/ MS SQL Server]";
726         }
727
728         /**
729          * @return string Version information from the database
730          */
731         function getServerVersion() {
732                 $server_info = sqlsrv_server_info( $this->mConn );
733                 $version = 'Error';
734                 if ( isset( $server_info['SQLServerVersion'] ) ) $version = $server_info['SQLServerVersion'];
735                 return $version;
736         }
737
738         function tableExists ( $table, $schema = false ) {
739                 $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
740                         WHERE table_type='BASE TABLE' AND table_name = '$table'" );
741                 if ( $res === false ) {
742                         print( "Error in tableExists query: " . $this->getErrors() );
743                         return false;
744                 }
745                 if ( sqlsrv_fetch( $res ) )
746                         return true;
747                 else
748                         return false;
749         }
750
751         /**
752          * Query whether a given column exists in the mediawiki schema
753          */
754         function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
755                 $table = $this->tableName( $table );
756                 $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
757                         WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
758                 if ( $res === false ) {
759                         print( "Error in fieldExists query: " . $this->getErrors() );
760                         return false;
761                 }
762                 if ( sqlsrv_fetch( $res ) )
763                         return true;
764                 else
765                         return false;
766         }
767
768         function fieldInfo( $table, $field ) {
769                 $table = $this->tableName( $table );
770                 $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
771                         WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
772                 if ( $res === false ) {
773                         print( "Error in fieldInfo query: " . $this->getErrors() );
774                         return false;
775                 }
776                 $meta = $this->fetchRow( $res );
777                 if ( $meta ) {
778                         return new MssqlField( $meta );
779                 }
780                 return false;
781         }
782
783         public function unixTimestamp( $field ) {
784                 return "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),$field)";
785         }
786
787         /**
788          * Begin a transaction, committing any previously open transaction
789          */
790         function begin( $fname = 'DatabaseMssql::begin' ) {
791                 sqlsrv_begin_transaction( $this->mConn );
792                 $this->mTrxLevel = 1;
793         }
794
795         /**
796          * End a transaction
797          */
798         function commit( $fname = 'DatabaseMssql::commit' ) {
799                 sqlsrv_commit( $this->mConn );
800                 $this->mTrxLevel = 0;
801         }
802
803         /**
804          * Rollback a transaction.
805          * No-op on non-transactional databases.
806          */
807         function rollback( $fname = 'DatabaseMssql::rollback' ) {
808                 sqlsrv_rollback( $this->mConn );
809                 $this->mTrxLevel = 0;
810         }
811
812         function setup_database() {
813                 global $wgDBuser;
814
815                 // Make sure that we can write to the correct schema
816                 $ctest = "mediawiki_test_table";
817                 if ( $this->tableExists( $ctest ) ) {
818                         $this->doQuery( "DROP TABLE $ctest" );
819                 }
820                 $SQL = "CREATE TABLE $ctest (a int)";
821                 $res = $this->doQuery( $SQL );
822                 if ( !$res ) {
823                         print "<b>FAILED</b>. Make sure that the user " . htmlspecialchars( $wgDBuser ) . " can write to the database</li>\n";
824                         dieout( );
825                 }
826                 $this->doQuery( "DROP TABLE $ctest" );
827
828                 $res = $this->sourceFile( "../maintenance/mssql/tables.sql" );
829                 if ( $res !== true ) {
830                         echo " <b>FAILED</b></li>";
831                         dieout( htmlspecialchars( $res ) );
832                 }
833
834                 # Avoid the non-standard "REPLACE INTO" syntax
835                 $f = fopen( "../maintenance/interwiki.sql", 'r' );
836                 if ( $f == false ) {
837                         dieout( "<li>Could not find the interwiki.sql file" );
838                 }
839                 # We simply assume it is already empty as we have just created it
840                 $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
841                 while ( ! feof( $f ) ) {
842                         $line = fgets( $f, 1024 );
843                         $matches = array();
844                         if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) {
845                                 continue;
846                         }
847                         $this->query( "$SQL $matches[1],$matches[2])" );
848                 }
849                 print " (table interwiki successfully populated)...\n";
850
851                 $this->commit();
852         }
853
854         /**
855          * Escapes a identifier for use inm SQL.
856          * Throws an exception if it is invalid.
857          * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
858          */
859         private function escapeIdentifier( $identifier ) {
860                 if ( strlen( $identifier ) == 0 ) {
861                         throw new MWException( "An identifier must not be empty" );
862                 }
863                 if ( strlen( $identifier ) > 128 ) {
864                         throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
865                 }
866                 if ( ( strpos( $identifier, '[' ) !== false ) || ( strpos( $identifier, ']' ) !== false ) ) {
867                         // It may be allowed if you quoted with double quotation marks, but that would break if QUOTED_IDENTIFIER is OFF
868                         throw new MWException( "You can't use square brackers in the identifier '$identifier'" );
869                 }
870                 return "[$identifier]";
871         }
872
873         /**
874          * Initial setup.
875          * Precondition: This object is connected as the superuser.
876          * Creates the database, schema, user and login.
877          */
878         function initial_setup( $dbName, $newUser, $loginPassword ) {
879                 $dbName = $this->escapeIdentifier( $dbName );
880
881                 // It is not clear what can be used as a login,
882                 // From http://msdn.microsoft.com/en-us/library/ms173463.aspx
883                 // a sysname may be the same as an identifier.
884                 $newUser = $this->escapeIdentifier( $newUser );
885                 $loginPassword = $this->addQuotes( $loginPassword );
886
887                 $this->doQuery("CREATE DATABASE $dbName;");
888                 $this->doQuery("USE $dbName;");
889                 $this->doQuery("CREATE SCHEMA $dbName;");
890                 $this->doQuery("
891                                                 CREATE
892                                                         LOGIN $newUser
893                                                 WITH
894                                                         PASSWORD=$loginPassword
895                                                 ;
896                                         ");
897                 $this->doQuery("
898                                                 CREATE
899                                                         USER $newUser
900                                                 FOR
901                                                         LOGIN $newUser
902                                                 WITH
903                                                         DEFAULT_SCHEMA=$dbName
904                                                 ;
905                                         ");
906                 $this->doQuery("
907                                                 GRANT
908                                                         BACKUP DATABASE,
909                                                         BACKUP LOG,
910                                                         CREATE DEFAULT,
911                                                         CREATE FUNCTION,
912                                                         CREATE PROCEDURE,
913                                                         CREATE RULE,
914                                                         CREATE TABLE,
915                                                         CREATE VIEW,
916                                                         CREATE FULLTEXT CATALOG
917                                                 ON
918                                                         DATABASE::$dbName
919                                                 TO $newUser
920                                                 ;
921                                         ");
922                 $this->doQuery("
923                                                 GRANT
924                                                         CONTROL
925                                                 ON
926                                                         SCHEMA::$dbName
927                                                 TO $newUser
928                                                 ;
929                                         ");
930
931
932         }
933
934         function encodeBlob( $b ) {
935         // we can't have zero's and such, this is a simple encoding to make sure we don't barf
936                 return base64_encode( $b );
937         }
938
939         function decodeBlob( $b ) {
940         // we can't have zero's and such, this is a simple encoding to make sure we don't barf
941         return base64_decode( $b );
942         }
943
944         /**
945          * @private
946          */
947         function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
948                 $ret = array();
949                 $retJOIN = array();
950                 $use_index_safe = is_array( $use_index ) ? $use_index : array();
951                 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
952                 foreach ( $tables as $table ) {
953                         // Is there a JOIN and INDEX clause for this table?
954                         if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
955                                 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
956                                 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
957                                 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
958                                 $retJOIN[] = $tableClause;
959                         // Is there an INDEX clause?
960                         } else if ( isset( $use_index_safe[$table] ) ) {
961                                 $tableClause = $this->tableName( $table );
962                                 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
963                                 $ret[] = $tableClause;
964                         // Is there a JOIN clause?
965                         } else if ( isset( $join_conds_safe[$table] ) ) {
966                                 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
967                                 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
968                                 $retJOIN[] = $tableClause;
969                         } else {
970                                 $tableClause = $this->tableName( $table );
971                                 $ret[] = $tableClause;
972                         }
973                 }
974                 // We can't separate explicit JOIN clauses with ',', use ' ' for those
975                 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
976                 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
977                 // Compile our final table clause
978                 return implode( ' ', array( $straightJoins, $otherJoins ) );
979         }
980
981         function strencode( $s ) { # Should not be called by us
982                 return str_replace( "'", "''", $s );
983         }
984
985         function addQuotes( $s ) {
986                 if ( $s instanceof Blob ) {
987                         return "'" . $s->fetch( $s ) . "'";
988                 } else {
989                         return parent::addQuotes( $s );
990                 }
991         }
992
993         function selectDB( $db ) {
994                 return ( $this->query( "SET DATABASE $db" ) !== false );
995         }
996
997         /**
998          * @private
999          *
1000          * @param $options Array: an associative array of options to be turned into
1001          *                 an SQL query, valid keys are listed in the function.
1002          * @return Array
1003          */
1004         function makeSelectOptions( $options ) {
1005                 $tailOpts = '';
1006                 $startOpts = '';
1007
1008                 $noKeyOptions = array();
1009                 foreach ( $options as $key => $option ) {
1010                         if ( is_numeric( $key ) ) {
1011                                 $noKeyOptions[$option] = true;
1012                         }
1013                 }
1014
1015                 if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}";
1016                 if ( isset( $options['HAVING'] ) )   $tailOpts .= " HAVING {$options['GROUP BY']}";
1017                 if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}";
1018
1019                 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
1020
1021                 // we want this to be compatible with the output of parent::makeSelectOptions()
1022                 return array( $startOpts, '' , $tailOpts, '' );
1023         }
1024
1025         /**
1026          * Get the type of the DBMS, as it appears in $wgDBtype.
1027          */
1028         function getType(){
1029                 return 'mssql';
1030         }
1031
1032         function buildConcat( $stringList ) {
1033                 return implode( ' + ', $stringList );
1034         }
1035
1036         public function getSearchEngine() {
1037                 return "SearchMssql";
1038         }
1039
1040 } // end DatabaseMssql class
1041
1042 /**
1043  * Utility class.
1044  *
1045  * @ingroup Database
1046  */
1047 class MssqlField implements Field {
1048         private $name, $tablename, $default, $max_length, $nullable, $type;
1049         function __construct ( $info ) {
1050                 $this->name = $info['COLUMN_NAME'];
1051                 $this->tablename = $info['TABLE_NAME'];
1052                 $this->default = $info['COLUMN_DEFAULT'];
1053                 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
1054                 $this->nullable = ( strtolower( $info['IS_NULLABLE'] ) == 'no' ) ? false:true;
1055                 $this->type = $info['DATA_TYPE'];
1056         }
1057         function name() {
1058                 return $this->name;
1059         }
1060
1061         function tableName() {
1062                 return $this->tableName;
1063         }
1064
1065         function defaultValue() {
1066                 return $this->default;
1067         }
1068
1069         function maxLength() {
1070                 return $this->max_length;
1071         }
1072
1073         function isNullable() {
1074                 return $this->nullable;
1075         }
1076
1077         function type() {
1078                 return $this->type;
1079         }
1080 }
1081
1082 /**
1083  * The MSSQL PHP driver doesn't support sqlsrv_num_rows, so we recall all rows into an array and maintain our
1084  * own cursor index into that array...This is similar to the way the Oracle driver handles this same issue
1085  *
1086  * @ingroup Database
1087  */
1088 class MssqlResult {
1089
1090   public function __construct( $queryresult = false ) {
1091         $this->mCursor = 0;
1092         $this->mRows = array();
1093         $this->mNumFields = sqlsrv_num_fields( $queryresult );
1094         $this->mFieldMeta = sqlsrv_field_metadata( $queryresult );
1095         while ( $row = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC ) ) {
1096                 if ( $row !== null ) {
1097                         foreach ( $row as $k => $v ) {
1098                                 if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object
1099                                         $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" );
1100                                 }
1101                         }
1102                         $this->mRows[] = $row;// read results into memory, cursors are not supported
1103                 }
1104         }
1105         $this->mRowCount = count( $this->mRows );
1106         sqlsrv_free_stmt( $queryresult );
1107   }
1108
1109   private function array_to_obj( $array, &$obj ) {
1110                 foreach ( $array as $key => $value ) {
1111                         if ( is_array( $value ) ) {
1112                                 $obj->$key = new stdClass();
1113                                 $this->array_to_obj( $value, $obj->$key );
1114                         } else {
1115                                 if ( !empty( $key ) ) {
1116                                         $obj->$key = $value;
1117                                 }
1118                         }
1119                 }
1120                 return $obj;
1121   }
1122
1123   public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) {
1124         if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) {
1125                 return false;
1126         }
1127         $arrNum = array();
1128         if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
1129                 foreach ( $this->mRows[$this->mCursor] as $value ) {
1130                         $arrNum[] = $value;
1131                 }
1132         }
1133         switch( $mode ) {
1134                 case SQLSRV_FETCH_ASSOC:
1135                         $ret = $this->mRows[$this->mCursor];
1136                         break;
1137                 case SQLSRV_FETCH_NUMERIC:
1138                         $ret = $arrNum;
1139                         break;
1140                 case 'OBJECT':
1141                         $o = new $object_class;
1142                         $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o );
1143                         break;
1144                 case SQLSRV_FETCH_BOTH:
1145                 default:
1146                         $ret = $this->mRows[$this->mCursor] + $arrNum;
1147                         break;
1148         }
1149
1150         $this->mCursor++;
1151         return $ret;
1152   }
1153
1154   public function get( $pos, $fld ) {
1155         return $this->mRows[$pos][$fld];
1156   }
1157
1158   public function numrows() {
1159         return $this->mRowCount;
1160   }
1161
1162   public function seek( $iRow ) {
1163         $this->mCursor = min( $iRow, $this->mRowCount );
1164   }
1165
1166   public function numfields() {
1167         return $this->mNumFields;
1168   }
1169
1170   public function fieldname( $nr ) {
1171         $arrKeys = array_keys( $this->mRows[0] );
1172         return $arrKeys[$nr];
1173   }
1174
1175   public function fieldtype( $nr ) {
1176         $i = 0;
1177         $intType = -1;
1178         foreach ( $this->mFieldMeta as $meta ) {
1179                 if ( $nr == $i ) {
1180                         $intType = $meta['Type'];
1181                         break;
1182                 }
1183                 $i++;
1184         }
1185         // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
1186         switch( $intType ) {
1187                 case SQLSRV_SQLTYPE_BIGINT:             $strType = 'bigint'; break;
1188                 case SQLSRV_SQLTYPE_BINARY:             $strType = 'binary'; break;
1189                 case SQLSRV_SQLTYPE_BIT:                        $strType = 'bit'; break;
1190                 case SQLSRV_SQLTYPE_CHAR:                       $strType = 'char'; break;
1191                 case SQLSRV_SQLTYPE_DATETIME:           $strType = 'datetime'; break;
1192                 case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break;
1193                 case SQLSRV_SQLTYPE_FLOAT:                      $strType = 'float'; break;
1194                 case SQLSRV_SQLTYPE_IMAGE:                      $strType = 'image'; break;
1195                 case SQLSRV_SQLTYPE_INT:                        $strType = 'int'; break;
1196                 case SQLSRV_SQLTYPE_MONEY:                      $strType = 'money'; break;
1197                 case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break;
1198                 case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break;
1199                 case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break;
1200                 // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break;
1201                 case SQLSRV_SQLTYPE_NTEXT:                      $strType = 'ntext'; break;
1202                 case SQLSRV_SQLTYPE_REAL:                       $strType = 'real'; break;
1203                 case SQLSRV_SQLTYPE_SMALLDATETIME:      $strType = 'smalldatetime'; break;
1204                 case SQLSRV_SQLTYPE_SMALLINT:           $strType = 'smallint'; break;
1205                 case SQLSRV_SQLTYPE_SMALLMONEY:         $strType = 'smallmoney'; break;
1206                 case SQLSRV_SQLTYPE_TEXT:                       $strType = 'text'; break;
1207                 case SQLSRV_SQLTYPE_TIMESTAMP:          $strType = 'timestamp'; break;
1208                 case SQLSRV_SQLTYPE_TINYINT:            $strType = 'tinyint'; break;
1209                 case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break;
1210                 case SQLSRV_SQLTYPE_UDT:                        $strType = 'UDT'; break;
1211                 case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break;
1212                 // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break;
1213                 case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break;
1214                 // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break;
1215                 case SQLSRV_SQLTYPE_XML:                        $strType = 'xml'; break;
1216                 default: $strType = $intType;
1217         }
1218         return $strType;
1219   }
1220
1221   public function free() {
1222         unset( $this->mRows );
1223         return;
1224   }
1225
1226 }