DatabaseMssql class and related changes
[lhc/web/wiklou.git] / includes / db / DatabaseMssql.php
1 <?php
2
3 /**
4 * This is the MS SQL Server Native database abstraction layer.
5 *
6 * @addtogroup 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 * @addtogroup 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 = $port = $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 $this->freeResult( $res );
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 = 'Database::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 = 'Database::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 = 'Database::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 string $name 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 = 'Database::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 = "Database::deleteJoin" ) {
645 if ( !$conds ) {
646 throw new DBUnexpectedError( $this, 'Database::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 $this->freeResult( $res );
670 return $size;
671 }
672
673 /**
674 * Construct a LIMIT query with optional offset
675 * This is used for query pages
676 * $sql string SQL query we will append the limit too
677 * $limit integer the SQL limit
678 * $offset integer the SQL offset (default false)
679 */
680 function limitResult( $sql, $limit, $offset = false ) {
681 if ( $offset === false || $offset == 0 ) {
682 if ( strpos( $sql, "SELECT" ) === false ) {
683 return "TOP {$limit} " . $sql;
684 } else {
685 return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 );
686 }
687 } else {
688 $sql = '
689 SELECT * FROM (
690 SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
691 SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
692 ) as sub2
693 ) AS sub3
694 WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit );
695 return $sql;
696 }
697 }
698
699 // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult()
700 // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser.
701 // This exists becase there are still too many extensions that don't use dynamic sql generation.
702 function LimitToTopN( $sql ) {
703 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
704 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
705 if ( preg_match( $pattern, $sql, $matches ) ) {
706 // row_count = $matches[4]
707 $row_count = $matches[4];
708 // offset = $matches[3] OR $matches[6]
709 $offset = $matches[3] or
710 $offset = $matches[6] or
711 $offset = false;
712
713 // strip the matching LIMIT clause out
714 $sql = str_replace( $matches[0], '', $sql );
715 return $this->limitResult( $sql, $row_count, $offset );
716 }
717 return $sql;
718 }
719
720 // MSSQL does support this, but documentation is too thin to make a generalized
721 // function for this. Apparently UPDATE TOP (N) works, but the sort order
722 // may not be what we're expecting so the top n results may be a random selection.
723 // TODO: Implement properly.
724 function limitResultForUpdate( $sql, $num ) {
725 return $sql;
726 }
727
728
729 function timestamp( $ts = 0 ) {
730 return wfTimestamp( TS_ISO_8601, $ts );
731 }
732
733 /**
734 * @return string wikitext of a link to the server software's web site
735 */
736 function getSoftwareLink() {
737 return "[http://www.microsoft.com/sql/ MS SQL Server]";
738 }
739
740 /**
741 * @return string Version information from the database
742 */
743 function getServerVersion() {
744 $server_info = sqlsrv_server_info( $this->mConn );
745 $version = 'Error';
746 if ( isset( $server_info['SQLServerVersion'] ) ) $version = $server_info['SQLServerVersion'];
747 return $version;
748 }
749
750 function tableExists ( $table, $schema = false ) {
751 $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
752 WHERE table_type='BASE TABLE' AND table_name = '$table'" );
753 if ( $res === false ) {
754 print( "Error in tableExists query: " . $this->getErrors() );
755 return false;
756 }
757 if ( sqlsrv_fetch( $res ) )
758 return true;
759 else
760 return false;
761 }
762
763 /**
764 * Query whether a given column exists in the mediawiki schema
765 */
766 function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
767 $table = $this->tableName( $table );
768 $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
769 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
770 if ( $res === false ) {
771 print( "Error in fieldExists query: " . $this->getErrors() );
772 return false;
773 }
774 if ( sqlsrv_fetch( $res ) )
775 return true;
776 else
777 return false;
778 }
779
780 function fieldInfo( $table, $field ) {
781 $table = $this->tableName( $table );
782 $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
783 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
784 if ( $res === false ) {
785 print( "Error in fieldInfo query: " . $this->getErrors() );
786 return false;
787 }
788 if ( $meta = $this->fetchRow( $res ) )
789 return new MssqlField( $meta );
790 return false;
791 }
792
793 /**
794 * Begin a transaction, committing any previously open transaction
795 */
796 function begin( $fname = 'Database::begin' ) {
797 sqlsrv_begin_transaction( $this->mConn );
798 $this->mTrxLevel = 1;
799 }
800
801 /**
802 * End a transaction
803 */
804 function commit( $fname = 'Database::commit' ) {
805 sqlsrv_commit( $this->mConn );
806 $this->mTrxLevel = 0;
807 }
808
809 /**
810 * Rollback a transaction.
811 * No-op on non-transactional databases.
812 */
813 function rollback( $fname = 'Database::rollback' ) {
814 sqlsrv_rollback( $this->mConn );
815 $this->mTrxLevel = 0;
816 }
817
818 function setup_database() {
819 global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
820
821 // Make sure that we can write to the correct schema
822 $ctest = "mediawiki_test_table";
823 if ( $this->tableExists( $ctest ) ) {
824 $this->doQuery( "DROP TABLE $ctest" );
825 }
826 $SQL = "CREATE TABLE $ctest (a int)";
827 $res = $this->doQuery( $SQL );
828 if ( !$res ) {
829 print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" can write to the database</li>\n";
830 dieout( "</ul>" );
831 }
832 $this->doQuery( "DROP TABLE $ctest" );
833
834 $res = dbsource( "../maintenance/mssql/tables.sql", $this );
835
836 # # Update version information
837 $mwv = $this->addQuotes( $wgVersion );
838 $pgv = $this->addQuotes( $this->getServerVersion() );
839 $pgu = $this->addQuotes( $this->mUser );
840 $pgp = $this->addQuotes( $wgDBport );
841 $dbn = $this->addQuotes( $this->mDBname );
842
843 # # Avoid the non-standard "REPLACE INTO" syntax
844 $f = fopen( "../maintenance/interwiki.sql", 'r' );
845 if ( $f == false ) {
846 dieout( "<li>Could not find the interwiki.sql file" );
847 }
848 # # We simply assume it is already empty as we have just created it
849 $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
850 while ( ! feof( $f ) ) {
851 $line = fgets( $f, 1024 );
852 $matches = array();
853 if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) {
854 continue;
855 }
856 $this->query( "$SQL $matches[1],$matches[2])" );
857 }
858 print " (table interwiki successfully populated)...\n";
859
860 $this->commit();
861 }
862
863 /**
864 * Initial setup as superuser.
865 * Create the database, schema, login, and user.
866 */
867 function initial_setup() {
868 global $conf;
869
870 // FIXME: fields need to be properly escaped.
871
872 $this->doQuery("CREATE DATABASE {$conf->DBname};");
873 $this->doQuery("USE {$conf->DBname};");
874 $this->doQuery("CREATE SCHEMA {$conf->DBname};");
875 $this->doQuery("
876 CREATE
877 LOGIN {$conf->DBuser}
878 WITH
879 PASSWORD='{$conf->DBpassword}'
880 ;
881 ");
882 $this->doQuery("
883 CREATE
884 USER {$conf->DBuser}
885 FOR
886 LOGIN {$conf->DBuser}
887 WITH
888 DEFAULT_SCHEMA={$conf->DBname}
889 ;
890 ");
891 $this->doQuery("
892 GRANT
893 BACKUP DATABASE,
894 BACKUP LOG,
895 CREATE DEFAULT,
896 CREATE FUNCTION,
897 CREATE PROCEDURE,
898 CREATE RULE,
899 CREATE TABLE,
900 CREATE VIEW,
901 CREATE FULLTEXT CATALOG
902 ON
903 DATABASE::{$conf->DBname}
904 TO {$conf->DBuser}
905 ;
906 ");
907 $this->doQuery("
908 GRANT
909 CONTROL
910 ON
911 SCHEMA::{$conf->DBname}
912 TO {$conf->DBuser}
913 ;
914 ");
915
916
917 }
918
919 function encodeBlob( $b ) {
920 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
921 return base64_encode( $b );
922 }
923
924 function decodeBlob( $b ) {
925 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
926 return base64_decode( $b );
927 }
928
929 /**
930 * @private
931 */
932 function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
933 $ret = array();
934 $retJOIN = array();
935 $use_index_safe = is_array( $use_index ) ? $use_index : array();
936 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
937 foreach ( $tables as $table ) {
938 // Is there a JOIN and INDEX clause for this table?
939 if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
940 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
941 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
942 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
943 $retJOIN[] = $tableClause;
944 // Is there an INDEX clause?
945 } else if ( isset( $use_index_safe[$table] ) ) {
946 $tableClause = $this->tableName( $table );
947 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
948 $ret[] = $tableClause;
949 // Is there a JOIN clause?
950 } else if ( isset( $join_conds_safe[$table] ) ) {
951 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
952 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
953 $retJOIN[] = $tableClause;
954 } else {
955 $tableClause = $this->tableName( $table );
956 $ret[] = $tableClause;
957 }
958 }
959 // We can't separate explicit JOIN clauses with ',', use ' ' for those
960 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
961 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
962 // Compile our final table clause
963 return implode( ' ', array( $straightJoins, $otherJoins ) );
964 }
965
966 function strencode( $s ) { # # Should not be called by us
967 return str_replace( "'", "''", $s );
968 }
969
970 function addQuotes( $s ) {
971 if ( $s instanceof Blob ) {
972 return "'" . $s->fetch( $s ) . "'";
973 } else {
974 return parent::addQuotes( $s );
975 }
976 }
977
978 function quote_ident( $s ) {
979 return "'" . str_replace( "'", "''", $s ) . "'";
980 }
981
982 function selectDB( $db ) {
983 return ( $this->query( "SET DATABASE $db" ) !== false );
984 }
985
986 /**
987 * @private
988 *
989 * @param array $options an associative array of options to be turned into
990 * an SQL query, valid keys are listed in the function.
991 * @return array
992 */
993 function makeSelectOptions( $options ) {
994 $tailOpts = '';
995 $startOpts = '';
996
997 $noKeyOptions = array();
998 foreach ( $options as $key => $option ) {
999 if ( is_numeric( $key ) ) {
1000 $noKeyOptions[$option] = true;
1001 }
1002 }
1003
1004 if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}";
1005 if ( isset( $options['HAVING'] ) ) $tailOpts .= " HAVING {$options['GROUP BY']}";
1006 if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}";
1007
1008 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
1009
1010 // we want this to be compatible with the output of parent::makeSelectOptions()
1011 return array( $startOpts, '' , $tailOpts, '' );
1012 }
1013
1014 public function setTimeout( $timeout ) {
1015 // couldn't find any timeout functions
1016 }
1017
1018 function ping() {
1019 wfDebug( "Function ping() not written for MSSQL Native yet" );
1020 return true;
1021 }
1022
1023 /**
1024 * How lagged is this slave?
1025 *
1026 */
1027 public function getLag() {
1028 # Not implemented for MSSQL
1029 return false;
1030 }
1031
1032 /**
1033 * FIXME: Add lag support
1034 */
1035 function setFakeSlaveLag( $lag ) { }
1036 function setFakeMaster( $enabled = false ) { }
1037
1038 /**
1039 * Get the type of the DBMS, as it appears in $wgDBtype.
1040 */
1041 function getType(){
1042 return 'mssql';
1043 }
1044
1045 function buildConcat( $stringList ) {
1046 return implode( ' + ', $stringList );
1047 }
1048
1049 public function getSearchEngine() {
1050 return "SearchMssql";
1051 }
1052
1053 } // end DatabaseMssql class
1054
1055 /**
1056 * Utility class.
1057 * @addtogroup 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 * @addtogroup 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 }