Followup r73348, r70608: move 'infinity' stuff to DB classes
[mediawiki.git] / includes / db / DatabaseMssql.php
blobc03ffbb7ea9653f2f9fab21adb9f636d37398d47
1 <?php
2 /**
3 * This is the MS SQL Server Native database abstraction layer.
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>
12 /**
13 * @ingroup Database
15 class DatabaseMssql extends DatabaseBase {
16 var $mInsertId = NULL;
17 var $mLastResult = NULL;
18 var $mAffectedRows = NULL;
20 function __construct( $server = false, $user = false, $password = false, $dbName = false,
21 $flags = 0 )
23 $this->mFlags = $flags;
24 $this->open( $server, $user, $password, $dbName );
27 function cascadingDeletes() {
28 return true;
30 function cleanupTriggers() {
31 return true;
33 function strictIPs() {
34 return true;
36 function realTimestamps() {
37 return true;
39 function implicitGroupby() {
40 return false;
42 function implicitOrderby() {
43 return false;
45 function functionalIndexes() {
46 return true;
48 function unionSupportsOrderAndLimit() {
49 return false;
52 static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) {
53 return new DatabaseMssql( $server, $user, $password, $dbName, $flags );
56 /**
57 * Usually aborts on failure
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" );
65 global $wgDBport;
67 if ( !strlen( $user ) ) { # e.g. the class is being loaded
68 return;
71 $this->close();
72 $this->mServer = $server;
73 $this->mPort = $wgDBport;
74 $this->mUser = $user;
75 $this->mPassword = $password;
76 $this->mDBname = $dbName;
78 $connectionInfo = array();
80 if( $dbName ) {
81 $connectionInfo['Database'] = $dbName;
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 );
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;
99 // End NT Auth Hack
101 $this->mConn = @sqlsrv_connect( $server, $connectionInfo );
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;
110 $this->mOpened = true;
111 return $this->mConn;
115 * Closes a database connection, if it is open
116 * Returns success, true if already closed
118 function close() {
119 $this->mOpened = false;
120 if ( $this->mConn ) {
121 return sqlsrv_close( $this->mConn );
122 } else {
123 return true;
127 function doQuery( $sql ) {
128 wfDebug( "SQL: [$sql]\n" );
129 $this->offset = 0;
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 ) ;
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 );
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";
157 throw new DBUnexpectedError( $this, $message );
159 // remember number of rows affected
160 $this->mAffectedRows = sqlsrv_rows_affected( $stmt );
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;
172 return $res;
175 function freeResult( $res ) {
176 if ( $res instanceof ResultWrapper ) {
177 $res = $res->result;
179 $res->free();
182 function fetchObject( $res ) {
183 if ( $res instanceof ResultWrapper ) {
184 $res = $res->result;
186 $row = $res->fetch( 'OBJECT' );
187 return $row;
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";
199 } else {
200 $strRet = "No errors found";
202 return $strRet;
205 function fetchRow( $res ) {
206 if ( $res instanceof ResultWrapper ) {
207 $res = $res->result;
209 $row = $res->fetch( SQLSRV_FETCH_BOTH );
210 return $row;
213 function numRows( $res ) {
214 if ( $res instanceof ResultWrapper ) {
215 $res = $res->result;
217 return ( $res ) ? $res->numrows() : 0;
220 function numFields( $res ) {
221 if ( $res instanceof ResultWrapper ) {
222 $res = $res->result;
224 return ( $res ) ? $res->numfields() : 0;
227 function fieldName( $res, $n ) {
228 if ( $res instanceof ResultWrapper ) {
229 $res = $res->result;
231 return ( $res ) ? $res->fieldname( $n ) : 0;
235 * This must be called after nextSequenceVal
237 function insertId() {
238 return $this->mInsertId;
241 function dataSeek( $res, $row ) {
242 if ( $res instanceof ResultWrapper ) {
243 $res = $res->result;
245 return ( $res ) ? $res->seek( $row ) : false;
248 function lastError() {
249 if ( $this->mConn ) {
250 return $this->getErrors();
252 else {
253 return "No database connection";
257 function lastErrno() {
258 $err = sqlsrv_errors( SQLSRV_ERR_ALL );
259 if ( $err[0] ) return $err[0]['code'];
260 else return 0;
263 function affectedRows() {
264 return $this->mAffectedRows;
268 * SELECT wrapper
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
280 function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() )
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;
289 return $this->query( $sql, $fname );
293 * SELECT wrapper
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
305 function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
306 if ( isset( $options['EXPLAIN'] ) ) {
307 unset( $options['EXPLAIN'] );
309 return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
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()
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 );
323 $rows = -1;
324 if ( $res ) {
325 $row = $this->fetchRow( $res );
326 if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
328 return $rows;
333 * Returns information about an index
334 * If errors are explicitly ignored, returns NULL on failure
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;
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;
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;
363 return empty( $result ) ? false : $result;
367 * INSERT wrapper, inserts an array into a table
369 * $arrToInsert may be a single associative array, or an array of these with numeric keys, for
370 * multi-row insert.
372 * Usually aborts on failure
373 * If errors are explicitly ignored, returns success
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;
381 if ( !is_array( $options ) ) {
382 $options = array( $options );
385 $table = $this->tableName( $table );
387 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row
388 $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible
391 $allOk = true;
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 ) );
402 unset( $res );
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 = '';
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;";
420 } else {
421 // we can't insert NULL into an identity column, so remove the column from the insert.
422 unset( $a[$k] );
426 $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result
429 $keys = array_keys( $a );
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;
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')";
451 // Build the actual query
452 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
453 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
455 $first = true;
456 foreach ( $a as $value ) {
457 if ( $first ) {
458 $first = false;
459 } else {
460 $sql .= ',';
462 if ( is_string( $value ) ) {
463 $sql .= $this->addIdentifierQuotes( $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->addIdentifierQuotes( $value->fetch() );
469 } else {
470 $sql .= $this->addIdentifierQuotes( serialize( $value ) );
472 } else {
473 $sql .= $value;
476 $sql .= ')' . $sqlPost;
478 // Run the query
479 $ret = sqlsrv_query( $this->mConn, $sql );
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;
491 sqlsrv_free_stmt( $ret );
492 continue;
494 $allOk = false;
496 return $allOk;
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.
506 function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
507 $insertOptions = array(), $selectOptions = array() )
509 $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
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;
518 return NULL;
522 * Format a table name ready for use in constructing an SQL query
524 * This does two important things: it brackets table names which as necessary,
525 * and it adds a table prefix if there is one.
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.
531 * @param $name String: database table name
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";
540 if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
541 $name = "[$wgSharedDB].[$name]";
542 } else {
543 # Standard quoting
544 if ( $name != '' ) $name = "[$name]";
547 return $name;
551 * Return the next in a sequence, save the value for retrieval via insertId()
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)" );
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
561 sqlsrv_free_stmt( $ret );
562 $this->mInsertId = $row['id'];
563 return $row['id'];
567 * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
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 );
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
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 );
593 if ( count( $rows ) == 0 ) {
594 return;
597 # Single row case
598 if ( !is_array( reset( $rows ) ) ) {
599 $rows = array( $rows );
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 (';
614 if ( is_array( $index ) ) {
615 $first2 = true;
616 foreach ( $index as $col ) {
617 if ( $first2 ) {
618 $first2 = false;
619 } else {
620 $sql .= ' AND ';
622 $sql .= $col . '=' . $this->addQuotes( $row[$col] );
624 } else {
625 $sql .= $index . '=' . $this->addQuotes( $row[$index] );
628 $sql .= ')';
629 $this->query( $sql, $fname );
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 );
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' );
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 );
651 $sql .= ')';
653 $this->query( $sql, $fname );
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;
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)
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 );
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;
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;
708 // strip the matching LIMIT clause out
709 $sql = str_replace( $matches[0], '', $sql );
710 return $this->limitResult( $sql, $row_count, $offset );
712 return $sql;
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;
724 function timestamp( $ts = 0 ) {
725 return wfTimestamp( TS_ISO_8601, $ts );
729 * @return string wikitext of a link to the server software's web site
731 public static function getSoftwareLink() {
732 return "[http://www.microsoft.com/sql/ MS SQL Server]";
736 * @return string Version information from the database
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;
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;
752 if ( sqlsrv_fetch( $res ) )
753 return true;
754 else
755 return false;
759 * Query whether a given column exists in the mediawiki schema
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;
769 if ( sqlsrv_fetch( $res ) )
770 return true;
771 else
772 return false;
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;
783 $meta = $this->fetchRow( $res );
784 if ( $meta ) {
785 return new MssqlField( $meta );
787 return false;
791 * Begin a transaction, committing any previously open transaction
793 function begin( $fname = 'DatabaseMssql::begin' ) {
794 sqlsrv_begin_transaction( $this->mConn );
795 $this->mTrxLevel = 1;
799 * End a transaction
801 function commit( $fname = 'DatabaseMssql::commit' ) {
802 sqlsrv_commit( $this->mConn );
803 $this->mTrxLevel = 0;
807 * Rollback a transaction.
808 * No-op on non-transactional databases.
810 function rollback( $fname = 'DatabaseMssql::rollback' ) {
811 sqlsrv_rollback( $this->mConn );
812 $this->mTrxLevel = 0;
815 function setup_database() {
816 global $wgDBuser;
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" );
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( );
829 $this->doQuery( "DROP TABLE $ctest" );
831 $res = $this->sourceFile( "../maintenance/mssql/tables.sql" );
832 if ( $res !== true ) {
833 echo " <b>FAILED</b></li>";
834 dieout( htmlspecialchars( $res ) );
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" );
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;
850 $this->query( "$SQL $matches[1],$matches[2])" );
852 print " (table interwiki successfully populated)...\n";
854 $this->commit();
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
862 private function escapeIdentifier( $identifier ) {
863 if ( strlen( $identifier ) == 0 ) {
864 throw new MWException( "An identifier must not be empty" );
866 if ( strlen( $identifier ) > 128 ) {
867 throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
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'" );
873 return "[$identifier]";
877 * Initial setup.
878 * Precondition: This object is connected as the superuser.
879 * Creates the database, schema, user and login.
881 function initial_setup( $dbName, $newUser, $loginPassword ) {
882 $dbName = $this->escapeIdentifier( $dbName );
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 );
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
900 $this->doQuery("
901 CREATE
902 USER $newUser
904 LOGIN $newUser
905 WITH
906 DEFAULT_SCHEMA=$dbName
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
921 DATABASE::$dbName
922 TO $newUser
925 $this->doQuery("
926 GRANT
927 CONTROL
929 SCHEMA::$dbName
930 TO $newUser
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 );
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 );
948 * @private
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;
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 ) );
984 function strencode( $s ) { # Should not be called by us
985 return str_replace( "'", "''", $s );
988 function addQuotes( $s ) {
989 if ( $s instanceof Blob ) {
990 return "'" . $s->fetch( $s ) . "'";
991 } else {
992 return parent::addQuotes( $s );
996 function selectDB( $db ) {
997 return ( $this->query( "SET DATABASE $db" ) !== false );
1001 * @private
1003 * @param $options Array: an associative array of options to be turned into
1004 * an SQL query, valid keys are listed in the function.
1005 * @return Array
1007 function makeSelectOptions( $options ) {
1008 $tailOpts = '';
1009 $startOpts = '';
1011 $noKeyOptions = array();
1012 foreach ( $options as $key => $option ) {
1013 if ( is_numeric( $key ) ) {
1014 $noKeyOptions[$option] = true;
1018 if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}";
1019 if ( isset( $options['HAVING'] ) ) $tailOpts .= " HAVING {$options['GROUP BY']}";
1020 if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}";
1022 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
1024 // we want this to be compatible with the output of parent::makeSelectOptions()
1025 return array( $startOpts, '' , $tailOpts, '' );
1029 * Get the type of the DBMS, as it appears in $wgDBtype.
1031 function getType(){
1032 return 'mssql';
1035 function buildConcat( $stringList ) {
1036 return implode( ' + ', $stringList );
1039 public function getSearchEngine() {
1040 return "SearchMssql";
1044 * Since MSSQL doesn't recognize the infinity keyword, set date manually.
1045 * @todo Remove magic date
1047 public function getInfinity() {
1048 return '3000-01-31 00:00:00.000';
1051 } // end DatabaseMssql class
1054 * Utility class.
1056 * @ingroup Database
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'];
1068 function name() {
1069 return $this->name;
1072 function tableName() {
1073 return $this->tableName;
1076 function defaultValue() {
1077 return $this->default;
1080 function maxLength() {
1081 return $this->max_length;
1084 function isNullable() {
1085 return $this->nullable;
1088 function type() {
1089 return $this->type;
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
1097 * @ingroup Database
1099 class MssqlResult {
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" );
1113 $this->mRows[] = $row;// read results into memory, cursors are not supported
1116 $this->mRowCount = count( $this->mRows );
1117 sqlsrv_free_stmt( $queryresult );
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 $this->array_to_obj( $value, $obj->$key );
1125 } else {
1126 if ( !empty( $key ) ) {
1127 $obj->$key = $value;
1131 return $obj;
1134 public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) {
1135 if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) {
1136 return false;
1138 $arrNum = array();
1139 if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
1140 foreach ( $this->mRows[$this->mCursor] as $value ) {
1141 $arrNum[] = $value;
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;
1161 $this->mCursor++;
1162 return $ret;
1165 public function get( $pos, $fld ) {
1166 return $this->mRows[$pos][$fld];
1169 public function numrows() {
1170 return $this->mRowCount;
1173 public function seek( $iRow ) {
1174 $this->mCursor = min( $iRow, $this->mRowCount );
1177 public function numfields() {
1178 return $this->mNumFields;
1181 public function fieldname( $nr ) {
1182 $arrKeys = array_keys( $this->mRows[0] );
1183 return $arrKeys[$nr];
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;
1194 $i++;
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;
1229 return $strType;
1232 public function free() {
1233 unset( $this->mRows );
1234 return;