Follow up r74135 removing the globals, too.
[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. Did you forget to run maintenance/update.php after upgrading? See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
154 "Query: " . htmlentities( $sql ) . "\n" .
155 "Function: " . __METHOD__ . "\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 $table Mixed: array or string, table name(s) (prefix auto-added)
275 * @param $vars Mixed: array or string, field name(s) to be retrieved
276 * @param $conds Mixed: array or string, condition(s) for WHERE
277 * @param $fname String: calling function name (use __METHOD__) for logs/profiling
278 * @param $options Array: 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 = 'DatabaseMssql::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 $ret = $this->query( $sql, $fname );
290 sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" );
291 return $ret;
292 }
293 return $this->query( $sql, $fname );
294 }
295
296 /**
297 * SELECT wrapper
298 *
299 * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
300 * @param $vars Mixed: Array or string, field name(s) to be retrieved
301 * @param $conds Mixed: Array or string, condition(s) for WHERE
302 * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
303 * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
304 * see Database::makeSelectOptions code for list of supported stuff
305 * @param $join_conds Array: Associative array of table join conditions (optional)
306 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
307 * @return string, the SQL text
308 */
309 function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
310 if ( isset( $options['EXPLAIN'] ) ) {
311 unset( $options['EXPLAIN'] );
312 }
313 return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
314 }
315
316 /**
317 * Estimate rows in dataset
318 * Returns estimated count, based on SHOWPLAN_ALL output
319 * This is not necessarily an accurate estimate, so use sparingly
320 * Returns -1 if count cannot be found
321 * Takes same arguments as Database::select()
322 */
323 function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
324 $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
325 $res = $this->select( $table, $vars, $conds, $fname, $options );
326
327 $rows = -1;
328 if ( $res ) {
329 $row = $this->fetchRow( $res );
330 if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
331 }
332 return $rows;
333 }
334
335
336 /**
337 * Returns information about an index
338 * If errors are explicitly ignored, returns NULL on failure
339 */
340 function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
341 # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
342 # returned value except to check for the existance of indexes.
343 $sql = "sp_helpindex '" . $table . "'";
344 $res = $this->query( $sql, $fname );
345 if ( !$res ) {
346 return NULL;
347 }
348
349 $result = array();
350 while ( $row = $res->FetchNextObj() ) {
351 if ( $row->index_name == $index ) {
352 $row->Non_unique = !stristr( $row->index_description, "unique" );
353 $cols = explode( ", ", $row->index_keys );
354 foreach ( $cols as $col ) {
355 $row->Column_name = trim( $col );
356 $result[] = clone $row;
357 }
358 } else if ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
359 $row->Non_unique = 0;
360 $cols = explode( ", ", $row->index_keys );
361 foreach ( $cols as $col ) {
362 $row->Column_name = trim( $col );
363 $result[] = clone $row;
364 }
365 }
366 }
367 return empty( $result ) ? false : $result;
368 }
369
370 /**
371 * INSERT wrapper, inserts an array into a table
372 *
373 * $arrToInsert may be a single associative array, or an array of these with numeric keys, for
374 * multi-row insert.
375 *
376 * Usually aborts on failure
377 * If errors are explicitly ignored, returns success
378 */
379 function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
380 # No rows to insert, easy just return now
381 if ( !count( $arrToInsert ) ) {
382 return true;
383 }
384
385 if ( !is_array( $options ) ) {
386 $options = array( $options );
387 }
388
389 $table = $this->tableName( $table );
390
391 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row
392 $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible
393 }
394
395 $allOk = true;
396
397
398 // We know the table we're inserting into, get its identity column
399 $identity = null;
400 $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name
401 $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" );
402 if( $res && $res->numrows() ){
403 // There is an identity for this table.
404 $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) );
405 }
406 unset( $res );
407
408 foreach ( $arrToInsert as $blah => $a ) {
409 // start out with empty identity column, this is so we can return it as a result of the insert logic
410 $sqlPre = '';
411 $sqlPost = '';
412 $identityClause = '';
413
414 // if we have an identity column
415 if( $identity ) {
416 // iterate through
417 foreach ($a as $k => $v ) {
418 if ( $k == $identity ) {
419 if( !is_null($v) ){
420 // there is a value being passed to us, we need to turn on and off inserted identity
421 $sqlPre = "SET IDENTITY_INSERT $table ON;" ;
422 $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
423
424 } else {
425 // we can't insert NULL into an identity column, so remove the column from the insert.
426 unset( $a[$k] );
427 }
428 }
429 }
430 $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result
431 }
432
433 $keys = array_keys( $a );
434
435
436 // INSERT IGNORE is not supported by SQL Server
437 // remove IGNORE from options list and set ignore flag to true
438 $ignoreClause = false;
439 foreach ( $options as $k => $v ) {
440 if ( strtoupper( $v ) == "IGNORE" ) {
441 unset( $options[$k] );
442 $ignoreClause = true;
443 }
444 }
445
446 // translate MySQL INSERT IGNORE to something SQL Server can use
447 // example:
448 // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
449 // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
450 if ( $ignoreClause == true ) {
451 $prival = $a[$keys[0]];
452 $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')";
453 }
454
455 // Build the actual query
456 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
457 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
458
459 $first = true;
460 foreach ( $a as $key => $value ) {
461 if ( $first ) {
462 $first = false;
463 } else {
464 $sql .= ',';
465 }
466 if ( is_string( $value ) ) {
467 $sql .= $this->quote_ident( $value );
468 } elseif ( is_null( $value ) ) {
469 $sql .= 'null';
470 } elseif ( is_array( $value ) || is_object( $value ) ) {
471 if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) {
472 $sql .= $this->quote_ident( $value->fetch() );
473 } else {
474 $sql .= $this->quote_ident( serialize( $value ) );
475 }
476 } else {
477 $sql .= $value;
478 }
479 }
480 $sql .= ')' . $sqlPost;
481
482 // Run the query
483 $ret = sqlsrv_query( $this->mConn, $sql );
484
485 if ( $ret === false ) {
486 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
487 } elseif ( $ret != NULL ) {
488 // remember number of rows affected
489 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
490 if ( !is_null($identity) ) {
491 // then we want to get the identity column value we were assigned and save it off
492 $row = sqlsrv_fetch_object( $ret );
493 $this->mInsertId = $row->$identity;
494 }
495 sqlsrv_free_stmt( $ret );
496 continue;
497 }
498 $allOk = false;
499 }
500 return $allOk;
501 }
502
503 /**
504 * INSERT SELECT wrapper
505 * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
506 * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
507 * $conds may be "*" to copy the whole table
508 * srcTable may be an array of tables.
509 */
510 function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
511 $insertOptions = array(), $selectOptions = array() )
512 {
513 $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
514
515 if ( $ret === false ) {
516 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
517 } elseif ( $ret != NULL ) {
518 // remember number of rows affected
519 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
520 return $ret;
521 }
522 return NULL;
523 }
524
525 /**
526 * Format a table name ready for use in constructing an SQL query
527 *
528 * This does two important things: it brackets table names which as necessary,
529 * and it adds a table prefix if there is one.
530 *
531 * All functions of this object which require a table name call this function
532 * themselves. Pass the canonical name to such functions. This is only needed
533 * when calling query() directly.
534 *
535 * @param $name String: database table name
536 */
537 function tableName( $name ) {
538 global $wgSharedDB;
539 # Skip quoted literals
540 if ( $name != '' && $name { 0 } != '[' ) {
541 if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) {
542 $name = "{$this->mTablePrefix}$name";
543 }
544 if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
545 $name = "[$wgSharedDB].[$name]";
546 } else {
547 # Standard quoting
548 if ( $name != '' ) $name = "[$name]";
549 }
550 }
551 return $name;
552 }
553
554 /**
555 * Return the next in a sequence, save the value for retrieval via insertId()
556 */
557 function nextSequenceValue( $seqName ) {
558 if ( !$this->tableExists( 'sequence_' . $seqName ) ) {
559 $ret = sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" );
560 }
561 $ret = sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" );
562 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
563 $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't
564
565 sqlsrv_free_stmt( $ret );
566 $this->mInsertId = $row['id'];
567 return $row['id'];
568 }
569
570 /**
571 * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
572 */
573 function currentSequenceValue( $seqName ) {
574 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
575 if ( $ret !== false ) {
576 $row = sqlsrv_fetch_array( $ret );
577 sqlsrv_free_stmt( $ret );
578 return $row['id'];
579 } else {
580 return $this->nextSequenceValue( $seqName );
581 }
582 }
583
584
585 # REPLACE query wrapper
586 # MSSQL simulates this with a DELETE followed by INSERT
587 # $row is the row to insert, an associative array
588 # $uniqueIndexes is an array of indexes. Each element may be either a
589 # field name or an array of field names
590 #
591 # It may be more efficient to leave off unique indexes which are unlikely to collide.
592 # However if you do this, you run the risk of encountering errors which wouldn't have
593 # occurred in MySQL
594 function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMssql::replace' ) {
595 $table = $this->tableName( $table );
596
597 if ( count( $rows ) == 0 ) {
598 return;
599 }
600
601 # Single row case
602 if ( !is_array( reset( $rows ) ) ) {
603 $rows = array( $rows );
604 }
605
606 foreach ( $rows as $row ) {
607 # Delete rows which collide
608 if ( $uniqueIndexes ) {
609 $sql = "DELETE FROM $table WHERE ";
610 $first = true;
611 foreach ( $uniqueIndexes as $index ) {
612 if ( $first ) {
613 $first = false;
614 $sql .= "(";
615 } else {
616 $sql .= ') OR (';
617 }
618 if ( is_array( $index ) ) {
619 $first2 = true;
620 foreach ( $index as $col ) {
621 if ( $first2 ) {
622 $first2 = false;
623 } else {
624 $sql .= ' AND ';
625 }
626 $sql .= $col . '=' . $this->addQuotes( $row[$col] );
627 }
628 } else {
629 $sql .= $index . '=' . $this->addQuotes( $row[$index] );
630 }
631 }
632 $sql .= ')';
633 $this->query( $sql, $fname );
634 }
635
636 # Now insert the row
637 $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) . ') VALUES (' .
638 $this->makeList( $row, LIST_COMMA ) . ')';
639 $this->query( $sql, $fname );
640 }
641 }
642
643 # DELETE where the condition is a join
644 function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseMssql::deleteJoin" ) {
645 if ( !$conds ) {
646 throw new DBUnexpectedError( $this, 'DatabaseMssql::deleteJoin() called with empty $conds' );
647 }
648
649 $delTable = $this->tableName( $delTable );
650 $joinTable = $this->tableName( $joinTable );
651 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
652 if ( $conds != '*' ) {
653 $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
654 }
655 $sql .= ')';
656
657 $this->query( $sql, $fname );
658 }
659
660 # Returns the size of a text field, or -1 for "unlimited"
661 function textFieldSize( $table, $field ) {
662 $table = $this->tableName( $table );
663 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
664 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
665 $res = $this->query( $sql );
666 $row = $this->fetchRow( $res );
667 $size = -1;
668 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) $size = $row['CHARACTER_MAXIMUM_LENGTH'];
669 return $size;
670 }
671
672 /**
673 * Construct a LIMIT query with optional offset
674 * This is used for query pages
675 * $sql string SQL query we will append the limit too
676 * $limit integer the SQL limit
677 * $offset integer the SQL offset (default false)
678 */
679 function limitResult( $sql, $limit, $offset = false ) {
680 if ( $offset === false || $offset == 0 ) {
681 if ( strpos( $sql, "SELECT" ) === false ) {
682 return "TOP {$limit} " . $sql;
683 } else {
684 return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 );
685 }
686 } else {
687 $sql = '
688 SELECT * FROM (
689 SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
690 SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
691 ) as sub2
692 ) AS sub3
693 WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit );
694 return $sql;
695 }
696 }
697
698 // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult()
699 // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser.
700 // This exists becase there are still too many extensions that don't use dynamic sql generation.
701 function LimitToTopN( $sql ) {
702 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
703 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
704 if ( preg_match( $pattern, $sql, $matches ) ) {
705 // row_count = $matches[4]
706 $row_count = $matches[4];
707 // offset = $matches[3] OR $matches[6]
708 $offset = $matches[3] or
709 $offset = $matches[6] or
710 $offset = false;
711
712 // strip the matching LIMIT clause out
713 $sql = str_replace( $matches[0], '', $sql );
714 return $this->limitResult( $sql, $row_count, $offset );
715 }
716 return $sql;
717 }
718
719 // MSSQL does support this, but documentation is too thin to make a generalized
720 // function for this. Apparently UPDATE TOP (N) works, but the sort order
721 // may not be what we're expecting so the top n results may be a random selection.
722 // TODO: Implement properly.
723 function limitResultForUpdate( $sql, $num ) {
724 return $sql;
725 }
726
727
728 function timestamp( $ts = 0 ) {
729 return wfTimestamp( TS_ISO_8601, $ts );
730 }
731
732 /**
733 * @return string wikitext of a link to the server software's web site
734 */
735 public static function getSoftwareLink() {
736 return "[http://www.microsoft.com/sql/ MS SQL Server]";
737 }
738
739 /**
740 * @return string Version information from the database
741 */
742 function getServerVersion() {
743 $server_info = sqlsrv_server_info( $this->mConn );
744 $version = 'Error';
745 if ( isset( $server_info['SQLServerVersion'] ) ) $version = $server_info['SQLServerVersion'];
746 return $version;
747 }
748
749 function tableExists ( $table, $schema = false ) {
750 $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
751 WHERE table_type='BASE TABLE' AND table_name = '$table'" );
752 if ( $res === false ) {
753 print( "Error in tableExists query: " . $this->getErrors() );
754 return false;
755 }
756 if ( sqlsrv_fetch( $res ) )
757 return true;
758 else
759 return false;
760 }
761
762 /**
763 * Query whether a given column exists in the mediawiki schema
764 */
765 function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
766 $table = $this->tableName( $table );
767 $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
768 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
769 if ( $res === false ) {
770 print( "Error in fieldExists query: " . $this->getErrors() );
771 return false;
772 }
773 if ( sqlsrv_fetch( $res ) )
774 return true;
775 else
776 return false;
777 }
778
779 function fieldInfo( $table, $field ) {
780 $table = $this->tableName( $table );
781 $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
782 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
783 if ( $res === false ) {
784 print( "Error in fieldInfo query: " . $this->getErrors() );
785 return false;
786 }
787 if ( $meta = $this->fetchRow( $res ) )
788 return new MssqlField( $meta );
789 return false;
790 }
791
792 /**
793 * Begin a transaction, committing any previously open transaction
794 */
795 function begin( $fname = 'DatabaseMssql::begin' ) {
796 sqlsrv_begin_transaction( $this->mConn );
797 $this->mTrxLevel = 1;
798 }
799
800 /**
801 * End a transaction
802 */
803 function commit( $fname = 'DatabaseMssql::commit' ) {
804 sqlsrv_commit( $this->mConn );
805 $this->mTrxLevel = 0;
806 }
807
808 /**
809 * Rollback a transaction.
810 * No-op on non-transactional databases.
811 */
812 function rollback( $fname = 'DatabaseMssql::rollback' ) {
813 sqlsrv_rollback( $this->mConn );
814 $this->mTrxLevel = 0;
815 }
816
817 function setup_database() {
818 global $wgDBuser;
819
820 // Make sure that we can write to the correct schema
821 $ctest = "mediawiki_test_table";
822 if ( $this->tableExists( $ctest ) ) {
823 $this->doQuery( "DROP TABLE $ctest" );
824 }
825 $SQL = "CREATE TABLE $ctest (a int)";
826 $res = $this->doQuery( $SQL );
827 if ( !$res ) {
828 print "<b>FAILED</b>. Make sure that the user " . htmlspecialchars( $wgDBuser ) . " can write to the database</li>\n";
829 dieout( "</ul>" );
830 }
831 $this->doQuery( "DROP TABLE $ctest" );
832
833 $res = $this->sourceFile( "../maintenance/mssql/tables.sql" );
834 if ( $res !== true ) {
835 echo " <b>FAILED</b></li>";
836 dieout( htmlspecialchars( $res ) );
837 }
838
839 # Avoid the non-standard "REPLACE INTO" syntax
840 $f = fopen( "../maintenance/interwiki.sql", 'r' );
841 if ( $f == false ) {
842 dieout( "<li>Could not find the interwiki.sql file" );
843 }
844 # We simply assume it is already empty as we have just created it
845 $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
846 while ( ! feof( $f ) ) {
847 $line = fgets( $f, 1024 );
848 $matches = array();
849 if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) {
850 continue;
851 }
852 $this->query( "$SQL $matches[1],$matches[2])" );
853 }
854 print " (table interwiki successfully populated)...\n";
855
856 $this->commit();
857 }
858
859 /**
860 * Escapes a identifier for use inm SQL.
861 * Throws an exception if it is invalid.
862 * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
863 */
864 private function escapeIdentifier( $identifier ) {
865 if ( strlen( $identifier ) == 0 ) {
866 throw new MWException( "An identifier must not be empty" );
867 }
868 if ( strlen( $identifier ) > 128 ) {
869 throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
870 }
871 if ( ( strpos( $identifier, '[' ) !== false ) || ( strpos( $identifier, ']' ) !== false ) ) {
872 // It may be allowed if you quoted with double quotation marks, but that would break if QUOTED_IDENTIFIER is OFF
873 throw new MWException( "You can't use square brackers in the identifier '$identifier'" );
874 }
875 return "[$identifier]";
876 }
877
878 /**
879 * Initial setup.
880 * Precondition: This object is connected as the superuser.
881 * Creates the database, schema, user and login.
882 */
883 function initial_setup( $dbName, $newUser, $loginPassword ) {
884 $dbName = $this->escapeIdentifier( $dbName );
885
886 // It is not clear what can be used as a login,
887 // From http://msdn.microsoft.com/en-us/library/ms173463.aspx
888 // a sysname may be the same as an identifier.
889 $newUser = $this->escapeIdentifier( $newUser );
890 $loginPassword = $this->addQuotes( $loginPassword );
891
892 $this->doQuery("CREATE DATABASE $dbName;");
893 $this->doQuery("USE $dbName;");
894 $this->doQuery("CREATE SCHEMA $dbName;");
895 $this->doQuery("
896 CREATE
897 LOGIN $newUser
898 WITH
899 PASSWORD=$loginPassword
900 ;
901 ");
902 $this->doQuery("
903 CREATE
904 USER $newUser
905 FOR
906 LOGIN $newUser
907 WITH
908 DEFAULT_SCHEMA=$dbName
909 ;
910 ");
911 $this->doQuery("
912 GRANT
913 BACKUP DATABASE,
914 BACKUP LOG,
915 CREATE DEFAULT,
916 CREATE FUNCTION,
917 CREATE PROCEDURE,
918 CREATE RULE,
919 CREATE TABLE,
920 CREATE VIEW,
921 CREATE FULLTEXT CATALOG
922 ON
923 DATABASE::$dbName
924 TO $newUser
925 ;
926 ");
927 $this->doQuery("
928 GRANT
929 CONTROL
930 ON
931 SCHEMA::$dbName
932 TO $newUser
933 ;
934 ");
935
936
937 }
938
939 function encodeBlob( $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_encode( $b );
942 }
943
944 function decodeBlob( $b ) {
945 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
946 return base64_decode( $b );
947 }
948
949 /**
950 * @private
951 */
952 function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
953 $ret = array();
954 $retJOIN = array();
955 $use_index_safe = is_array( $use_index ) ? $use_index : array();
956 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
957 foreach ( $tables as $table ) {
958 // Is there a JOIN and INDEX clause for this table?
959 if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
960 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
961 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
962 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
963 $retJOIN[] = $tableClause;
964 // Is there an INDEX clause?
965 } else if ( isset( $use_index_safe[$table] ) ) {
966 $tableClause = $this->tableName( $table );
967 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
968 $ret[] = $tableClause;
969 // Is there a JOIN clause?
970 } else if ( isset( $join_conds_safe[$table] ) ) {
971 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
972 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
973 $retJOIN[] = $tableClause;
974 } else {
975 $tableClause = $this->tableName( $table );
976 $ret[] = $tableClause;
977 }
978 }
979 // We can't separate explicit JOIN clauses with ',', use ' ' for those
980 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
981 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
982 // Compile our final table clause
983 return implode( ' ', array( $straightJoins, $otherJoins ) );
984 }
985
986 function strencode( $s ) { # Should not be called by us
987 return str_replace( "'", "''", $s );
988 }
989
990 function addQuotes( $s ) {
991 if ( $s instanceof Blob ) {
992 return "'" . $s->fetch( $s ) . "'";
993 } else {
994 return parent::addQuotes( $s );
995 }
996 }
997
998 function quote_ident( $s ) {
999 return "'" . str_replace( "'", "''", $s ) . "'";
1000 }
1001
1002 function selectDB( $db ) {
1003 return ( $this->query( "SET DATABASE $db" ) !== false );
1004 }
1005
1006 /**
1007 * @private
1008 *
1009 * @param $options Array: an associative array of options to be turned into
1010 * an SQL query, valid keys are listed in the function.
1011 * @return Array
1012 */
1013 function makeSelectOptions( $options ) {
1014 $tailOpts = '';
1015 $startOpts = '';
1016
1017 $noKeyOptions = array();
1018 foreach ( $options as $key => $option ) {
1019 if ( is_numeric( $key ) ) {
1020 $noKeyOptions[$option] = true;
1021 }
1022 }
1023
1024 if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}";
1025 if ( isset( $options['HAVING'] ) ) $tailOpts .= " HAVING {$options['GROUP BY']}";
1026 if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}";
1027
1028 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
1029
1030 // we want this to be compatible with the output of parent::makeSelectOptions()
1031 return array( $startOpts, '' , $tailOpts, '' );
1032 }
1033
1034 /**
1035 * Get the type of the DBMS, as it appears in $wgDBtype.
1036 */
1037 function getType(){
1038 return 'mssql';
1039 }
1040
1041 function buildConcat( $stringList ) {
1042 return implode( ' + ', $stringList );
1043 }
1044
1045 public function getSearchEngine() {
1046 return "SearchMssql";
1047 }
1048
1049 } // end DatabaseMssql class
1050
1051 /**
1052 * Utility class.
1053 *
1054 * @ingroup Database
1055 */
1056 class MssqlField {
1057 private $name, $tablename, $default, $max_length, $nullable, $type;
1058 function __construct ( $info ) {
1059 $this->name = $info['COLUMN_NAME'];
1060 $this->tablename = $info['TABLE_NAME'];
1061 $this->default = $info['COLUMN_DEFAULT'];
1062 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
1063 $this->nullable = ( strtolower( $info['IS_NULLABLE'] ) == 'no' ) ? false:true;
1064 $this->type = $info['DATA_TYPE'];
1065 }
1066 function name() {
1067 return $this->name;
1068 }
1069
1070 function tableName() {
1071 return $this->tableName;
1072 }
1073
1074 function defaultValue() {
1075 return $this->default;
1076 }
1077
1078 function maxLength() {
1079 return $this->max_length;
1080 }
1081
1082 function nullable() {
1083 return $this->nullable;
1084 }
1085
1086 function type() {
1087 return $this->type;
1088 }
1089 }
1090
1091 /**
1092 * The MSSQL PHP driver doesn't support sqlsrv_num_rows, so we recall all rows into an array and maintain our
1093 * own cursor index into that array...This is similar to the way the Oracle driver handles this same issue
1094 *
1095 * @ingroup Database
1096 */
1097 class MssqlResult {
1098
1099 public function __construct( $queryresult = false ) {
1100 $this->mCursor = 0;
1101 $this->mRows = array();
1102 $this->mNumFields = sqlsrv_num_fields( $queryresult );
1103 $this->mFieldMeta = sqlsrv_field_metadata( $queryresult );
1104 while ( $row = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC ) ) {
1105 if ( $row !== null ) {
1106 foreach ( $row as $k => $v ) {
1107 if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object
1108 $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" );
1109 }
1110 }
1111 $this->mRows[] = $row;// read results into memory, cursors are not supported
1112 }
1113 }
1114 $this->mRowCount = count( $this->mRows );
1115 sqlsrv_free_stmt( $queryresult );
1116 }
1117
1118 private function array_to_obj( $array, &$obj ) {
1119 foreach ( $array as $key => $value ) {
1120 if ( is_array( $value ) ) {
1121 $obj->$key = new stdClass();
1122 array_to_obj( $value, $obj->$key );
1123 } else {
1124 if ( !empty( $key ) ) {
1125 $obj->$key = $value;
1126 }
1127 }
1128 }
1129 return $obj;
1130 }
1131
1132 public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) {
1133 if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) return false;
1134 $ret = false;
1135 $arrNum = array();
1136 if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
1137 foreach ( $this->mRows[$this->mCursor] as $key => $value ) {
1138 $arrNum[] = $value;
1139 }
1140 }
1141 switch( $mode ) {
1142 case SQLSRV_FETCH_ASSOC:
1143 $ret = $this->mRows[$this->mCursor];
1144 break;
1145 case SQLSRV_FETCH_NUMERIC:
1146 $ret = $arrNum;
1147 break;
1148 case 'OBJECT':
1149 $o = new $object_class;
1150 $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o );
1151 break;
1152 case SQLSRV_FETCH_BOTH:
1153 default:
1154 $ret = $this->mRows[$this->mCursor] + $arrNum;
1155 break;
1156 }
1157
1158 $this->mCursor++;
1159 return $ret;
1160 }
1161
1162 public function get( $pos, $fld ) {
1163 return $this->mRows[$pos][$fld];
1164 }
1165
1166 public function numrows() {
1167 return $this->mRowCount;
1168 }
1169
1170 public function seek( $iRow ) {
1171 $this->mCursor = min( $iRow, $this->mRowCount );
1172 }
1173
1174 public function numfields() {
1175 return $this->mNumFields;
1176 }
1177
1178 public function fieldname( $nr ) {
1179 $arrKeys = array_keys( $this->mRows[0] );
1180 return $arrKeys[$nr];
1181 }
1182
1183 public function fieldtype( $nr ) {
1184 $i = 0;
1185 $intType = -1;
1186 $strType = '';
1187 foreach ( $this->mFieldMeta as $meta ) {
1188 if ( $nr == $i ) {
1189 $intType = $meta['Type'];
1190 break;
1191 }
1192 $i++;
1193 }
1194 // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
1195 switch( $intType ) {
1196 case SQLSRV_SQLTYPE_BIGINT: $strType = 'bigint'; break;
1197 case SQLSRV_SQLTYPE_BINARY: $strType = 'binary'; break;
1198 case SQLSRV_SQLTYPE_BIT: $strType = 'bit'; break;
1199 case SQLSRV_SQLTYPE_CHAR: $strType = 'char'; break;
1200 case SQLSRV_SQLTYPE_DATETIME: $strType = 'datetime'; break;
1201 case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break;
1202 case SQLSRV_SQLTYPE_FLOAT: $strType = 'float'; break;
1203 case SQLSRV_SQLTYPE_IMAGE: $strType = 'image'; break;
1204 case SQLSRV_SQLTYPE_INT: $strType = 'int'; break;
1205 case SQLSRV_SQLTYPE_MONEY: $strType = 'money'; break;
1206 case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break;
1207 case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break;
1208 case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break;
1209 // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break;
1210 case SQLSRV_SQLTYPE_NTEXT: $strType = 'ntext'; break;
1211 case SQLSRV_SQLTYPE_REAL: $strType = 'real'; break;
1212 case SQLSRV_SQLTYPE_SMALLDATETIME: $strType = 'smalldatetime'; break;
1213 case SQLSRV_SQLTYPE_SMALLINT: $strType = 'smallint'; break;
1214 case SQLSRV_SQLTYPE_SMALLMONEY: $strType = 'smallmoney'; break;
1215 case SQLSRV_SQLTYPE_TEXT: $strType = 'text'; break;
1216 case SQLSRV_SQLTYPE_TIMESTAMP: $strType = 'timestamp'; break;
1217 case SQLSRV_SQLTYPE_TINYINT: $strType = 'tinyint'; break;
1218 case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break;
1219 case SQLSRV_SQLTYPE_UDT: $strType = 'UDT'; break;
1220 case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break;
1221 // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break;
1222 case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break;
1223 // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break;
1224 case SQLSRV_SQLTYPE_XML: $strType = 'xml'; break;
1225 default: $strType = $intType;
1226 }
1227 return $strType;
1228 }
1229
1230 public function free() {
1231 unset( $this->mRows );
1232 return;
1233 }
1234
1235 }