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