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