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