More reversion of r77297, 1 of 2 commits to keep it readable in CR (hopefully)
[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 $flags = 0 )
22 {
23 $this->mFlags = $flags;
24 $this->open( $server, $user, $password, $dbName );
25 }
26
27 function cascadingDeletes() {
28 return true;
29 }
30 function cleanupTriggers() {
31 return true;
32 }
33 function strictIPs() {
34 return true;
35 }
36 function realTimestamps() {
37 return true;
38 }
39 function implicitGroupby() {
40 return false;
41 }
42 function implicitOrderby() {
43 return false;
44 }
45 function functionalIndexes() {
46 return true;
47 }
48 function unionSupportsOrderAndLimit() {
49 return false;
50 }
51
52 static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) {
53 return new DatabaseMssql( $server, $user, $password, $dbName, $flags );
54 }
55
56 /**
57 * Usually aborts on failure
58 */
59 function open( $server, $user, $password, $dbName ) {
60 # Test for driver support, to avoid suppressed fatal error
61 if ( !function_exists( 'sqlsrv_connect' ) ) {
62 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" );
63 }
64
65 global $wgDBport;
66
67 if ( !strlen( $user ) ) { # e.g. the class is being loaded
68 return;
69 }
70
71 $this->close();
72 $this->mServer = $server;
73 $this->mPort = $wgDBport;
74 $this->mUser = $user;
75 $this->mPassword = $password;
76 $this->mDBname = $dbName;
77
78 $connectionInfo = array();
79
80 if( $dbName ) {
81 $connectionInfo['Database'] = $dbName;
82 }
83
84 // Start NT Auth Hack
85 // Quick and dirty work around to provide NT Auth designation support.
86 // Current solution requires installer to know to input 'ntauth' for both username and password
87 // to trigger connection via NT Auth. - ugly, ugly, ugly
88 // TO-DO: Make this better and add NT Auth choice to MW installer when SQL Server option is chosen.
89 $ntAuthUserTest = strtolower( $user );
90 $ntAuthPassTest = strtolower( $password );
91
92 // Decide which auth scenerio to use
93 if( ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) ){
94 // Don't add credentials to $connectionInfo
95 } else {
96 $connectionInfo['UID'] = $user;
97 $connectionInfo['PWD'] = $password;
98 }
99 // End NT Auth Hack
100
101 $this->mConn = @sqlsrv_connect( $server, $connectionInfo );
102
103 if ( $this->mConn === false ) {
104 wfDebug( "DB connection error\n" );
105 wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
106 wfDebug( $this->lastError() . "\n" );
107 return false;
108 }
109
110 $this->mOpened = true;
111 return $this->mConn;
112 }
113
114 /**
115 * Closes a database connection, if it is open
116 * Returns success, true if already closed
117 */
118 function close() {
119 $this->mOpened = false;
120 if ( $this->mConn ) {
121 return sqlsrv_close( $this->mConn );
122 } else {
123 return true;
124 }
125 }
126
127 function doQuery( $sql ) {
128 wfDebug( "SQL: [$sql]\n" );
129 $this->offset = 0;
130
131 // several extensions seem to think that all databases support limits via LIMIT N after the WHERE clause
132 // well, MSSQL uses SELECT TOP N, so to catch any of those extensions we'll do a quick check for a LIMIT
133 // clause and pass $sql through $this->LimitToTopN() which parses the limit clause and passes the result to
134 // $this->limitResult();
135 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
136 // massage LIMIT -> TopN
137 $sql = $this->LimitToTopN( $sql ) ;
138 }
139
140 // MSSQL doesn't have EXTRACT(epoch FROM XXX)
141 if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
142 // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
143 $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
144 }
145
146 // perform query
147 $stmt = sqlsrv_query( $this->mConn, $sql );
148 if ( $stmt == false ) {
149 $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" .
150 "Query: " . htmlentities( $sql ) . "\n" .
151 "Function: " . __METHOD__ . "\n";
152 // process each error (our driver will give us an array of errors unlike other providers)
153 foreach ( sqlsrv_errors() as $error ) {
154 $message .= $message . "ERROR[" . $error['code'] . "] " . $error['message'] . "\n";
155 }
156
157 throw new DBUnexpectedError( $this, $message );
158 }
159 // remember number of rows affected
160 $this->mAffectedRows = sqlsrv_rows_affected( $stmt );
161
162 // if it is a SELECT statement, or an insert with a request to output something we want to return a row.
163 if ( ( preg_match( '#\bSELECT\s#i', $sql ) ) ||
164 ( preg_match( '#\bINSERT\s#i', $sql ) && preg_match( '#\bOUTPUT\s+INSERTED\b#i', $sql ) ) ) {
165 // this is essentially a rowset, but Mediawiki calls these 'result'
166 // the rowset owns freeing the statement
167 $res = new MssqlResult( $stmt );
168 } else {
169 // otherwise we simply return it was successful, failure throws an exception
170 $res = true;
171 }
172 return $res;
173 }
174
175 function freeResult( $res ) {
176 if ( $res instanceof ResultWrapper ) {
177 $res = $res->result;
178 }
179 $res->free();
180 }
181
182 function fetchObject( $res ) {
183 if ( $res instanceof ResultWrapper ) {
184 $res = $res->result;
185 }
186 $row = $res->fetch( 'OBJECT' );
187 return $row;
188 }
189
190 function getErrors() {
191 $strRet = '';
192 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
193 if ( $retErrors != null ) {
194 foreach ( $retErrors as $arrError ) {
195 $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n";
196 $strRet .= "Error Code: " . $arrError[ 'code'] . "\n";
197 $strRet .= "Message: " . $arrError[ 'message'] . "\n";
198 }
199 } else {
200 $strRet = "No errors found";
201 }
202 return $strRet;
203 }
204
205 function fetchRow( $res ) {
206 if ( $res instanceof ResultWrapper ) {
207 $res = $res->result;
208 }
209 $row = $res->fetch( SQLSRV_FETCH_BOTH );
210 return $row;
211 }
212
213 function numRows( $res ) {
214 if ( $res instanceof ResultWrapper ) {
215 $res = $res->result;
216 }
217 return ( $res ) ? $res->numrows() : 0;
218 }
219
220 function numFields( $res ) {
221 if ( $res instanceof ResultWrapper ) {
222 $res = $res->result;
223 }
224 return ( $res ) ? $res->numfields() : 0;
225 }
226
227 function fieldName( $res, $n ) {
228 if ( $res instanceof ResultWrapper ) {
229 $res = $res->result;
230 }
231 return ( $res ) ? $res->fieldname( $n ) : 0;
232 }
233
234 /**
235 * This must be called after nextSequenceVal
236 */
237 function insertId() {
238 return $this->mInsertId;
239 }
240
241 function dataSeek( $res, $row ) {
242 if ( $res instanceof ResultWrapper ) {
243 $res = $res->result;
244 }
245 return ( $res ) ? $res->seek( $row ) : false;
246 }
247
248 function lastError() {
249 if ( $this->mConn ) {
250 return $this->getErrors();
251 }
252 else {
253 return "No database connection";
254 }
255 }
256
257 function lastErrno() {
258 $err = sqlsrv_errors( SQLSRV_ERR_ALL );
259 if ( $err[0] ) return $err[0]['code'];
260 else return 0;
261 }
262
263 function affectedRows() {
264 return $this->mAffectedRows;
265 }
266
267 /**
268 * SELECT wrapper
269 *
270 * @param $table Mixed: array or string, table name(s) (prefix auto-added)
271 * @param $vars Mixed: array or string, field name(s) to be retrieved
272 * @param $conds Mixed: array or string, condition(s) for WHERE
273 * @param $fname String: calling function name (use __METHOD__) for logs/profiling
274 * @param $options Array: associative array of options (e.g. array('GROUP BY' => 'page_title')),
275 * see Database::makeSelectOptions code for list of supported stuff
276 * @param $join_conds Array: Associative array of table join conditions (optional)
277 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
278 * @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure
279 */
280 function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() )
281 {
282 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
283 if ( isset( $options['EXPLAIN'] ) ) {
284 sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL ON;" );
285 $ret = $this->query( $sql, $fname );
286 sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" );
287 return $ret;
288 }
289 return $this->query( $sql, $fname );
290 }
291
292 /**
293 * SELECT wrapper
294 *
295 * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
296 * @param $vars Mixed: Array or string, field name(s) to be retrieved
297 * @param $conds Mixed: Array or string, condition(s) for WHERE
298 * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
299 * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
300 * see Database::makeSelectOptions code for list of supported stuff
301 * @param $join_conds Array: Associative array of table join conditions (optional)
302 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
303 * @return string, the SQL text
304 */
305 function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
306 if ( isset( $options['EXPLAIN'] ) ) {
307 unset( $options['EXPLAIN'] );
308 }
309 return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
310 }
311
312 /**
313 * Estimate rows in dataset
314 * Returns estimated count, based on SHOWPLAN_ALL output
315 * This is not necessarily an accurate estimate, so use sparingly
316 * Returns -1 if count cannot be found
317 * Takes same arguments as Database::select()
318 */
319 function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
320 $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
321 $res = $this->select( $table, $vars, $conds, $fname, $options );
322
323 $rows = -1;
324 if ( $res ) {
325 $row = $this->fetchRow( $res );
326 if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
327 }
328 return $rows;
329 }
330
331
332 /**
333 * Returns information about an index
334 * If errors are explicitly ignored, returns NULL on failure
335 */
336 function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
337 # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
338 # returned value except to check for the existance of indexes.
339 $sql = "sp_helpindex '" . $table . "'";
340 $res = $this->query( $sql, $fname );
341 if ( !$res ) {
342 return NULL;
343 }
344
345 $result = array();
346 foreach ( $res as $row ) {
347 if ( $row->index_name == $index ) {
348 $row->Non_unique = !stristr( $row->index_description, "unique" );
349 $cols = explode( ", ", $row->index_keys );
350 foreach ( $cols as $col ) {
351 $row->Column_name = trim( $col );
352 $result[] = clone $row;
353 }
354 } else if ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
355 $row->Non_unique = 0;
356 $cols = explode( ", ", $row->index_keys );
357 foreach ( $cols as $col ) {
358 $row->Column_name = trim( $col );
359 $result[] = clone $row;
360 }
361 }
362 }
363 return empty( $result ) ? false : $result;
364 }
365
366 /**
367 * INSERT wrapper, inserts an array into a table
368 *
369 * $arrToInsert may be a single associative array, or an array of these with numeric keys, for
370 * multi-row insert.
371 *
372 * Usually aborts on failure
373 * If errors are explicitly ignored, returns success
374 */
375 function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
376 # No rows to insert, easy just return now
377 if ( !count( $arrToInsert ) ) {
378 return true;
379 }
380
381 if ( !is_array( $options ) ) {
382 $options = array( $options );
383 }
384
385 $table = $this->tableName( $table );
386
387 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row
388 $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible
389 }
390
391 $allOk = true;
392
393
394 // We know the table we're inserting into, get its identity column
395 $identity = null;
396 $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name
397 $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" );
398 if( $res && $res->numrows() ){
399 // There is an identity for this table.
400 $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) );
401 }
402 unset( $res );
403
404 foreach ( $arrToInsert as $a ) {
405 // start out with empty identity column, this is so we can return it as a result of the insert logic
406 $sqlPre = '';
407 $sqlPost = '';
408 $identityClause = '';
409
410 // if we have an identity column
411 if( $identity ) {
412 // iterate through
413 foreach ($a as $k => $v ) {
414 if ( $k == $identity ) {
415 if( !is_null($v) ){
416 // there is a value being passed to us, we need to turn on and off inserted identity
417 $sqlPre = "SET IDENTITY_INSERT $table ON;" ;
418 $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
419
420 } else {
421 // we can't insert NULL into an identity column, so remove the column from the insert.
422 unset( $a[$k] );
423 }
424 }
425 }
426 $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result
427 }
428
429 $keys = array_keys( $a );
430
431
432 // INSERT IGNORE is not supported by SQL Server
433 // remove IGNORE from options list and set ignore flag to true
434 $ignoreClause = false;
435 foreach ( $options as $k => $v ) {
436 if ( strtoupper( $v ) == "IGNORE" ) {
437 unset( $options[$k] );
438 $ignoreClause = true;
439 }
440 }
441
442 // translate MySQL INSERT IGNORE to something SQL Server can use
443 // example:
444 // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
445 // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
446 if ( $ignoreClause == true ) {
447 $prival = $a[$keys[0]];
448 $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')";
449 }
450
451 // Build the actual query
452 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
453 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
454
455 $first = true;
456 foreach ( $a as $value ) {
457 if ( $first ) {
458 $first = false;
459 } else {
460 $sql .= ',';
461 }
462 if ( is_string( $value ) ) {
463 $sql .= $this->quote_ident( $value );
464 } elseif ( is_null( $value ) ) {
465 $sql .= 'null';
466 } elseif ( is_array( $value ) || is_object( $value ) ) {
467 if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) {
468 $sql .= $this->quote_ident( $value->fetch() );
469 } else {
470 $sql .= $this->quote_ident( serialize( $value ) );
471 }
472 } else {
473 $sql .= $value;
474 }
475 }
476 $sql .= ')' . $sqlPost;
477
478 // Run the query
479 $ret = sqlsrv_query( $this->mConn, $sql );
480
481 if ( $ret === false ) {
482 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
483 } elseif ( $ret != NULL ) {
484 // remember number of rows affected
485 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
486 if ( !is_null($identity) ) {
487 // then we want to get the identity column value we were assigned and save it off
488 $row = sqlsrv_fetch_object( $ret );
489 $this->mInsertId = $row->$identity;
490 }
491 sqlsrv_free_stmt( $ret );
492 continue;
493 }
494 $allOk = false;
495 }
496 return $allOk;
497 }
498
499 /**
500 * INSERT SELECT wrapper
501 * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
502 * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
503 * $conds may be "*" to copy the whole table
504 * srcTable may be an array of tables.
505 */
506 function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
507 $insertOptions = array(), $selectOptions = array() )
508 {
509 $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
510
511 if ( $ret === false ) {
512 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
513 } elseif ( $ret != NULL ) {
514 // remember number of rows affected
515 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
516 return $ret;
517 }
518 return NULL;
519 }
520
521 /**
522 * Format a table name ready for use in constructing an SQL query
523 *
524 * This does two important things: it brackets table names which as necessary,
525 * and it adds a table prefix if there is one.
526 *
527 * All functions of this object which require a table name call this function
528 * themselves. Pass the canonical name to such functions. This is only needed
529 * when calling query() directly.
530 *
531 * @param $name String: database table name
532 */
533 function tableName( $name ) {
534 global $wgSharedDB;
535 # Skip quoted literals
536 if ( $name != '' && $name { 0 } != '[' ) {
537 if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) {
538 $name = "{$this->mTablePrefix}$name";
539 }
540 if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
541 $name = "[$wgSharedDB].[$name]";
542 } else {
543 # Standard quoting
544 if ( $name != '' ) $name = "[$name]";
545 }
546 }
547 return $name;
548 }
549
550 /**
551 * Return the next in a sequence, save the value for retrieval via insertId()
552 */
553 function nextSequenceValue( $seqName ) {
554 if ( !$this->tableExists( 'sequence_' . $seqName ) ) {
555 sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" );
556 }
557 sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" );
558 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
559 $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't
560
561 sqlsrv_free_stmt( $ret );
562 $this->mInsertId = $row['id'];
563 return $row['id'];
564 }
565
566 /**
567 * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
568 */
569 function currentSequenceValue( $seqName ) {
570 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
571 if ( $ret !== false ) {
572 $row = sqlsrv_fetch_array( $ret );
573 sqlsrv_free_stmt( $ret );
574 return $row['id'];
575 } else {
576 return $this->nextSequenceValue( $seqName );
577 }
578 }
579
580
581 # REPLACE query wrapper
582 # MSSQL simulates this with a DELETE followed by INSERT
583 # $row is the row to insert, an associative array
584 # $uniqueIndexes is an array of indexes. Each element may be either a
585 # field name or an array of field names
586 #
587 # It may be more efficient to leave off unique indexes which are unlikely to collide.
588 # However if you do this, you run the risk of encountering errors which wouldn't have
589 # occurred in MySQL
590 function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMssql::replace' ) {
591 $table = $this->tableName( $table );
592
593 if ( count( $rows ) == 0 ) {
594 return;
595 }
596
597 # Single row case
598 if ( !is_array( reset( $rows ) ) ) {
599 $rows = array( $rows );
600 }
601
602 foreach ( $rows as $row ) {
603 # Delete rows which collide
604 if ( $uniqueIndexes ) {
605 $sql = "DELETE FROM $table WHERE ";
606 $first = true;
607 foreach ( $uniqueIndexes as $index ) {
608 if ( $first ) {
609 $first = false;
610 $sql .= "(";
611 } else {
612 $sql .= ') OR (';
613 }
614 if ( is_array( $index ) ) {
615 $first2 = true;
616 foreach ( $index as $col ) {
617 if ( $first2 ) {
618 $first2 = false;
619 } else {
620 $sql .= ' AND ';
621 }
622 $sql .= $col . '=' . $this->addQuotes( $row[$col] );
623 }
624 } else {
625 $sql .= $index . '=' . $this->addQuotes( $row[$index] );
626 }
627 }
628 $sql .= ')';
629 $this->query( $sql, $fname );
630 }
631
632 # Now insert the row
633 $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) . ') VALUES (' .
634 $this->makeList( $row, LIST_COMMA ) . ')';
635 $this->query( $sql, $fname );
636 }
637 }
638
639 # DELETE where the condition is a join
640 function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseMssql::deleteJoin" ) {
641 if ( !$conds ) {
642 throw new DBUnexpectedError( $this, 'DatabaseMssql::deleteJoin() called with empty $conds' );
643 }
644
645 $delTable = $this->tableName( $delTable );
646 $joinTable = $this->tableName( $joinTable );
647 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
648 if ( $conds != '*' ) {
649 $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
650 }
651 $sql .= ')';
652
653 $this->query( $sql, $fname );
654 }
655
656 # Returns the size of a text field, or -1 for "unlimited"
657 function textFieldSize( $table, $field ) {
658 $table = $this->tableName( $table );
659 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
660 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
661 $res = $this->query( $sql );
662 $row = $this->fetchRow( $res );
663 $size = -1;
664 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) $size = $row['CHARACTER_MAXIMUM_LENGTH'];
665 return $size;
666 }
667
668 /**
669 * Construct a LIMIT query with optional offset
670 * This is used for query pages
671 * $sql string SQL query we will append the limit too
672 * $limit integer the SQL limit
673 * $offset integer the SQL offset (default false)
674 */
675 function limitResult( $sql, $limit, $offset = false ) {
676 if ( $offset === false || $offset == 0 ) {
677 if ( strpos( $sql, "SELECT" ) === false ) {
678 return "TOP {$limit} " . $sql;
679 } else {
680 return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 );
681 }
682 } else {
683 $sql = '
684 SELECT * FROM (
685 SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
686 SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
687 ) as sub2
688 ) AS sub3
689 WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit );
690 return $sql;
691 }
692 }
693
694 // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult()
695 // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser.
696 // This exists becase there are still too many extensions that don't use dynamic sql generation.
697 function LimitToTopN( $sql ) {
698 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
699 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
700 if ( preg_match( $pattern, $sql, $matches ) ) {
701 // row_count = $matches[4]
702 $row_count = $matches[4];
703 // offset = $matches[3] OR $matches[6]
704 $offset = $matches[3] or
705 $offset = $matches[6] or
706 $offset = false;
707
708 // strip the matching LIMIT clause out
709 $sql = str_replace( $matches[0], '', $sql );
710 return $this->limitResult( $sql, $row_count, $offset );
711 }
712 return $sql;
713 }
714
715 // MSSQL does support this, but documentation is too thin to make a generalized
716 // function for this. Apparently UPDATE TOP (N) works, but the sort order
717 // may not be what we're expecting so the top n results may be a random selection.
718 // TODO: Implement properly.
719 function limitResultForUpdate( $sql, $num ) {
720 return $sql;
721 }
722
723
724 function timestamp( $ts = 0 ) {
725 return wfTimestamp( TS_ISO_8601, $ts );
726 }
727
728 /**
729 * @return string wikitext of a link to the server software's web site
730 */
731 public static function getSoftwareLink() {
732 return "[http://www.microsoft.com/sql/ MS SQL Server]";
733 }
734
735 /**
736 * @return string Version information from the database
737 */
738 function getServerVersion() {
739 $server_info = sqlsrv_server_info( $this->mConn );
740 $version = 'Error';
741 if ( isset( $server_info['SQLServerVersion'] ) ) $version = $server_info['SQLServerVersion'];
742 return $version;
743 }
744
745 function tableExists ( $table, $schema = false ) {
746 $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
747 WHERE table_type='BASE TABLE' AND table_name = '$table'" );
748 if ( $res === false ) {
749 print( "Error in tableExists query: " . $this->getErrors() );
750 return false;
751 }
752 if ( sqlsrv_fetch( $res ) )
753 return true;
754 else
755 return false;
756 }
757
758 /**
759 * Query whether a given column exists in the mediawiki schema
760 */
761 function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
762 $table = $this->tableName( $table );
763 $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
764 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
765 if ( $res === false ) {
766 print( "Error in fieldExists query: " . $this->getErrors() );
767 return false;
768 }
769 if ( sqlsrv_fetch( $res ) )
770 return true;
771 else
772 return false;
773 }
774
775 function fieldInfo( $table, $field ) {
776 $table = $this->tableName( $table );
777 $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
778 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
779 if ( $res === false ) {
780 print( "Error in fieldInfo query: " . $this->getErrors() );
781 return false;
782 }
783 $meta = $this->fetchRow( $res );
784 if ( $meta ) {
785 return new MssqlField( $meta );
786 }
787 return false;
788 }
789
790 public function unixTimestamp( $field ) {
791 return "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),$field)";
792 }
793
794 /**
795 * Begin a transaction, committing any previously open transaction
796 */
797 function begin( $fname = 'DatabaseMssql::begin' ) {
798 sqlsrv_begin_transaction( $this->mConn );
799 $this->mTrxLevel = 1;
800 }
801
802 /**
803 * End a transaction
804 */
805 function commit( $fname = 'DatabaseMssql::commit' ) {
806 sqlsrv_commit( $this->mConn );
807 $this->mTrxLevel = 0;
808 }
809
810 /**
811 * Rollback a transaction.
812 * No-op on non-transactional databases.
813 */
814 function rollback( $fname = 'DatabaseMssql::rollback' ) {
815 sqlsrv_rollback( $this->mConn );
816 $this->mTrxLevel = 0;
817 }
818
819 function setup_database() {
820 global $wgDBuser;
821
822 // Make sure that we can write to the correct schema
823 $ctest = "mediawiki_test_table";
824 if ( $this->tableExists( $ctest ) ) {
825 $this->doQuery( "DROP TABLE $ctest" );
826 }
827 $SQL = "CREATE TABLE $ctest (a int)";
828 $res = $this->doQuery( $SQL );
829 if ( !$res ) {
830 print "<b>FAILED</b>. Make sure that the user " . htmlspecialchars( $wgDBuser ) . " can write to the database</li>\n";
831 dieout( );
832 }
833 $this->doQuery( "DROP TABLE $ctest" );
834
835 $res = $this->sourceFile( "../maintenance/mssql/tables.sql" );
836 if ( $res !== true ) {
837 echo " <b>FAILED</b></li>";
838 dieout( htmlspecialchars( $res ) );
839 }
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 $options Array: 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 implements Field {
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 isNullable() {
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 ) {
1136 return false;
1137 }
1138 $arrNum = array();
1139 if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
1140 foreach ( $this->mRows[$this->mCursor] as $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 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 }