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