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