trim trailing spaces
[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: " . __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 $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 $wgVersion, $wgDBport, $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 \"$wgDBuser\" can write to the database</li>\n";
829 dieout( "</ul>" );
830 }
831 $this->doQuery( "DROP TABLE $ctest" );
832
833 $res = dbsource( "../maintenance/mssql/tables.sql", $this );
834
835 # # Update version information
836 $mwv = $this->addQuotes( $wgVersion );
837 $pgv = $this->addQuotes( $this->getServerVersion() );
838 $pgu = $this->addQuotes( $this->mUser );
839 $pgp = $this->addQuotes( $wgDBport );
840 $dbn = $this->addQuotes( $this->mDBname );
841
842 # # Avoid the non-standard "REPLACE INTO" syntax
843 $f = fopen( "../maintenance/interwiki.sql", 'r' );
844 if ( $f == false ) {
845 dieout( "<li>Could not find the interwiki.sql file" );
846 }
847 # # We simply assume it is already empty as we have just created it
848 $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
849 while ( ! feof( $f ) ) {
850 $line = fgets( $f, 1024 );
851 $matches = array();
852 if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) {
853 continue;
854 }
855 $this->query( "$SQL $matches[1],$matches[2])" );
856 }
857 print " (table interwiki successfully populated)...\n";
858
859 $this->commit();
860 }
861
862 /**
863 * Escapes a identifier for use inm SQL.
864 * Throws an exception if it is invalid.
865 * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
866 */
867 private function escapeIdentifier( $identifier ) {
868 if ( strlen( $identifier ) == 0 ) {
869 throw new MWException( "An identifier must not be empty" );
870 }
871 if ( strlen( $identifier ) > 128 ) {
872 throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
873 }
874 if ( ( strpos( $identifier, '[' ) !== false ) || ( strpos( $identifier, ']' ) !== false ) ) {
875 // It may be allowed if you quoted with double quotation marks, but that would break if QUOTED_IDENTIFIER is OFF
876 throw new MWException( "You can't use square brackers in the identifier '$identifier'" );
877 }
878 return "[$identifier]";
879 }
880
881 /**
882 * Initial setup.
883 * Precondition: This object is connected as the superuser.
884 * Creates the database, schema, user and login.
885 */
886 function initial_setup( $dbName, $newUser, $loginPassword ) {
887 $dbName = $this->escapeIdentifier( $dbName );
888
889 // It is not clear what can be used as a login,
890 // From http://msdn.microsoft.com/en-us/library/ms173463.aspx
891 // a sysname may be the same as an identifier.
892 $newUser = $this->escapeIdentifier( $newUser );
893 $loginPassword = $this->addQuotes( $loginPassword );
894
895 $this->doQuery("CREATE DATABASE $dbName;");
896 $this->doQuery("USE $dbName;");
897 $this->doQuery("CREATE SCHEMA $dbName;");
898 $this->doQuery("
899 CREATE
900 LOGIN $newUser
901 WITH
902 PASSWORD=$loginPassword
903 ;
904 ");
905 $this->doQuery("
906 CREATE
907 USER $newUser
908 FOR
909 LOGIN $newUser
910 WITH
911 DEFAULT_SCHEMA=$dbName
912 ;
913 ");
914 $this->doQuery("
915 GRANT
916 BACKUP DATABASE,
917 BACKUP LOG,
918 CREATE DEFAULT,
919 CREATE FUNCTION,
920 CREATE PROCEDURE,
921 CREATE RULE,
922 CREATE TABLE,
923 CREATE VIEW,
924 CREATE FULLTEXT CATALOG
925 ON
926 DATABASE::$dbName
927 TO $newUser
928 ;
929 ");
930 $this->doQuery("
931 GRANT
932 CONTROL
933 ON
934 SCHEMA::$dbName
935 TO $newUser
936 ;
937 ");
938
939
940 }
941
942 function encodeBlob( $b ) {
943 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
944 return base64_encode( $b );
945 }
946
947 function decodeBlob( $b ) {
948 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
949 return base64_decode( $b );
950 }
951
952 /**
953 * @private
954 */
955 function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
956 $ret = array();
957 $retJOIN = array();
958 $use_index_safe = is_array( $use_index ) ? $use_index : array();
959 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
960 foreach ( $tables as $table ) {
961 // Is there a JOIN and INDEX clause for this table?
962 if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
963 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
964 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
965 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
966 $retJOIN[] = $tableClause;
967 // Is there an INDEX clause?
968 } else if ( isset( $use_index_safe[$table] ) ) {
969 $tableClause = $this->tableName( $table );
970 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
971 $ret[] = $tableClause;
972 // Is there a JOIN clause?
973 } else if ( isset( $join_conds_safe[$table] ) ) {
974 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
975 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
976 $retJOIN[] = $tableClause;
977 } else {
978 $tableClause = $this->tableName( $table );
979 $ret[] = $tableClause;
980 }
981 }
982 // We can't separate explicit JOIN clauses with ',', use ' ' for those
983 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
984 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
985 // Compile our final table clause
986 return implode( ' ', array( $straightJoins, $otherJoins ) );
987 }
988
989 function strencode( $s ) { # # Should not be called by us
990 return str_replace( "'", "''", $s );
991 }
992
993 function addQuotes( $s ) {
994 if ( $s instanceof Blob ) {
995 return "'" . $s->fetch( $s ) . "'";
996 } else {
997 return parent::addQuotes( $s );
998 }
999 }
1000
1001 function quote_ident( $s ) {
1002 return "'" . str_replace( "'", "''", $s ) . "'";
1003 }
1004
1005 function selectDB( $db ) {
1006 return ( $this->query( "SET DATABASE $db" ) !== false );
1007 }
1008
1009 /**
1010 * @private
1011 *
1012 * @param $options Array: an associative array of options to be turned into
1013 * an SQL query, valid keys are listed in the function.
1014 * @return Array
1015 */
1016 function makeSelectOptions( $options ) {
1017 $tailOpts = '';
1018 $startOpts = '';
1019
1020 $noKeyOptions = array();
1021 foreach ( $options as $key => $option ) {
1022 if ( is_numeric( $key ) ) {
1023 $noKeyOptions[$option] = true;
1024 }
1025 }
1026
1027 if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}";
1028 if ( isset( $options['HAVING'] ) ) $tailOpts .= " HAVING {$options['GROUP BY']}";
1029 if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}";
1030
1031 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
1032
1033 // we want this to be compatible with the output of parent::makeSelectOptions()
1034 return array( $startOpts, '' , $tailOpts, '' );
1035 }
1036
1037 /**
1038 * Get the type of the DBMS, as it appears in $wgDBtype.
1039 */
1040 function getType(){
1041 return 'mssql';
1042 }
1043
1044 function buildConcat( $stringList ) {
1045 return implode( ' + ', $stringList );
1046 }
1047
1048 public function getSearchEngine() {
1049 return "SearchMssql";
1050 }
1051
1052 } // end DatabaseMssql class
1053
1054 /**
1055 * Utility class.
1056 *
1057 * @ingroup Database
1058 */
1059 class MssqlField {
1060 private $name, $tablename, $default, $max_length, $nullable, $type;
1061 function __construct ( $info ) {
1062 $this->name = $info['COLUMN_NAME'];
1063 $this->tablename = $info['TABLE_NAME'];
1064 $this->default = $info['COLUMN_DEFAULT'];
1065 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
1066 $this->nullable = ( strtolower( $info['IS_NULLABLE'] ) == 'no' ) ? false:true;
1067 $this->type = $info['DATA_TYPE'];
1068 }
1069 function name() {
1070 return $this->name;
1071 }
1072
1073 function tableName() {
1074 return $this->tableName;
1075 }
1076
1077 function defaultValue() {
1078 return $this->default;
1079 }
1080
1081 function maxLength() {
1082 return $this->max_length;
1083 }
1084
1085 function nullable() {
1086 return $this->nullable;
1087 }
1088
1089 function type() {
1090 return $this->type;
1091 }
1092 }
1093
1094 /**
1095 * The MSSQL PHP driver doesn't support sqlsrv_num_rows, so we recall all rows into an array and maintain our
1096 * own cursor index into that array...This is similar to the way the Oracle driver handles this same issue
1097 *
1098 * @ingroup Database
1099 */
1100 class MssqlResult {
1101
1102 public function __construct( $queryresult = false ) {
1103 $this->mCursor = 0;
1104 $this->mRows = array();
1105 $this->mNumFields = sqlsrv_num_fields( $queryresult );
1106 $this->mFieldMeta = sqlsrv_field_metadata( $queryresult );
1107 while ( $row = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC ) ) {
1108 if ( $row !== null ) {
1109 foreach ( $row as $k => $v ) {
1110 if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object
1111 $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" );
1112 }
1113 }
1114 $this->mRows[] = $row;// read results into memory, cursors are not supported
1115 }
1116 }
1117 $this->mRowCount = count( $this->mRows );
1118 sqlsrv_free_stmt( $queryresult );
1119 }
1120
1121 private function array_to_obj( $array, &$obj ) {
1122 foreach ( $array as $key => $value ) {
1123 if ( is_array( $value ) ) {
1124 $obj->$key = new stdClass();
1125 array_to_obj( $value, $obj->$key );
1126 } else {
1127 if ( !empty( $key ) ) {
1128 $obj->$key = $value;
1129 }
1130 }
1131 }
1132 return $obj;
1133 }
1134
1135 public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) {
1136 if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) return false;
1137 $ret = false;
1138 $arrNum = array();
1139 if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
1140 foreach ( $this->mRows[$this->mCursor] as $key => $value ) {
1141 $arrNum[] = $value;
1142 }
1143 }
1144 switch( $mode ) {
1145 case SQLSRV_FETCH_ASSOC:
1146 $ret = $this->mRows[$this->mCursor];
1147 break;
1148 case SQLSRV_FETCH_NUMERIC:
1149 $ret = $arrNum;
1150 break;
1151 case 'OBJECT':
1152 $o = new $object_class;
1153 $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o );
1154 break;
1155 case SQLSRV_FETCH_BOTH:
1156 default:
1157 $ret = $this->mRows[$this->mCursor] + $arrNum;
1158 break;
1159 }
1160
1161 $this->mCursor++;
1162 return $ret;
1163 }
1164
1165 public function get( $pos, $fld ) {
1166 return $this->mRows[$pos][$fld];
1167 }
1168
1169 public function numrows() {
1170 return $this->mRowCount;
1171 }
1172
1173 public function seek( $iRow ) {
1174 $this->mCursor = min( $iRow, $this->mRowCount );
1175 }
1176
1177 public function numfields() {
1178 return $this->mNumFields;
1179 }
1180
1181 public function fieldname( $nr ) {
1182 $arrKeys = array_keys( $this->mRows[0] );
1183 return $arrKeys[$nr];
1184 }
1185
1186 public function fieldtype( $nr ) {
1187 $i = 0;
1188 $intType = -1;
1189 $strType = '';
1190 foreach ( $this->mFieldMeta as $meta ) {
1191 if ( $nr == $i ) {
1192 $intType = $meta['Type'];
1193 break;
1194 }
1195 $i++;
1196 }
1197 // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
1198 switch( $intType ) {
1199 case SQLSRV_SQLTYPE_BIGINT: $strType = 'bigint'; break;
1200 case SQLSRV_SQLTYPE_BINARY: $strType = 'binary'; break;
1201 case SQLSRV_SQLTYPE_BIT: $strType = 'bit'; break;
1202 case SQLSRV_SQLTYPE_CHAR: $strType = 'char'; break;
1203 case SQLSRV_SQLTYPE_DATETIME: $strType = 'datetime'; break;
1204 case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break;
1205 case SQLSRV_SQLTYPE_FLOAT: $strType = 'float'; break;
1206 case SQLSRV_SQLTYPE_IMAGE: $strType = 'image'; break;
1207 case SQLSRV_SQLTYPE_INT: $strType = 'int'; break;
1208 case SQLSRV_SQLTYPE_MONEY: $strType = 'money'; break;
1209 case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break;
1210 case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break;
1211 case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break;
1212 // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break;
1213 case SQLSRV_SQLTYPE_NTEXT: $strType = 'ntext'; break;
1214 case SQLSRV_SQLTYPE_REAL: $strType = 'real'; break;
1215 case SQLSRV_SQLTYPE_SMALLDATETIME: $strType = 'smalldatetime'; break;
1216 case SQLSRV_SQLTYPE_SMALLINT: $strType = 'smallint'; break;
1217 case SQLSRV_SQLTYPE_SMALLMONEY: $strType = 'smallmoney'; break;
1218 case SQLSRV_SQLTYPE_TEXT: $strType = 'text'; break;
1219 case SQLSRV_SQLTYPE_TIMESTAMP: $strType = 'timestamp'; break;
1220 case SQLSRV_SQLTYPE_TINYINT: $strType = 'tinyint'; break;
1221 case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break;
1222 case SQLSRV_SQLTYPE_UDT: $strType = 'UDT'; break;
1223 case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break;
1224 // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break;
1225 case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break;
1226 // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break;
1227 case SQLSRV_SQLTYPE_XML: $strType = 'xml'; break;
1228 default: $strType = $intType;
1229 }
1230 return $strType;
1231 }
1232
1233 public function free() {
1234 unset( $this->mRows );
1235 return;
1236 }
1237
1238 }