Merge "(bug 40098) Don't parse the section's name in the summary when creating a...
[mediawiki.git] / includes / db / DatabaseMssql.php
blob914ab4089a280b2d9d80e660efaf4d60de4dfd39
1 <?php
2 /**
3 * This is the MS SQL Server Native database abstraction layer.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
21 * @ingroup Database
22 * @author Joel Penner <a-joelpe at microsoft dot com>
23 * @author Chris Pucci <a-cpucci at microsoft dot com>
24 * @author Ryan Biesemeyer <v-ryanbi at microsoft dot com>
27 /**
28 * @ingroup Database
30 class DatabaseMssql extends DatabaseBase {
31 var $mInsertId = NULL;
32 var $mLastResult = NULL;
33 var $mAffectedRows = NULL;
35 var $mPort;
37 function cascadingDeletes() {
38 return true;
40 function cleanupTriggers() {
41 return true;
43 function strictIPs() {
44 return true;
46 function realTimestamps() {
47 return true;
49 function implicitGroupby() {
50 return false;
52 function implicitOrderby() {
53 return false;
55 function functionalIndexes() {
56 return true;
58 function unionSupportsOrderAndLimit() {
59 return false;
62 /**
63 * Usually aborts on failure
64 * @return bool|DatabaseBase|null
66 function open( $server, $user, $password, $dbName ) {
67 # Test for driver support, to avoid suppressed fatal error
68 if ( !function_exists( 'sqlsrv_connect' ) ) {
69 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" );
72 global $wgDBport;
74 if ( !strlen( $user ) ) { # e.g. the class is being loaded
75 return;
78 $this->close();
79 $this->mServer = $server;
80 $this->mPort = $wgDBport;
81 $this->mUser = $user;
82 $this->mPassword = $password;
83 $this->mDBname = $dbName;
85 $connectionInfo = array();
87 if( $dbName ) {
88 $connectionInfo['Database'] = $dbName;
91 // Start NT Auth Hack
92 // Quick and dirty work around to provide NT Auth designation support.
93 // Current solution requires installer to know to input 'ntauth' for both username and password
94 // to trigger connection via NT Auth. - ugly, ugly, ugly
95 // TO-DO: Make this better and add NT Auth choice to MW installer when SQL Server option is chosen.
96 $ntAuthUserTest = strtolower( $user );
97 $ntAuthPassTest = strtolower( $password );
99 // Decide which auth scenerio to use
100 if( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ){
101 // Don't add credentials to $connectionInfo
102 } else {
103 $connectionInfo['UID'] = $user;
104 $connectionInfo['PWD'] = $password;
106 // End NT Auth Hack
108 wfSuppressWarnings();
109 $this->mConn = sqlsrv_connect( $server, $connectionInfo );
110 wfRestoreWarnings();
112 if ( $this->mConn === false ) {
113 wfDebug( "DB connection error\n" );
114 wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
115 wfDebug( $this->lastError() . "\n" );
116 return false;
119 $this->mOpened = true;
120 return $this->mConn;
124 * Closes a database connection, if it is open
125 * Returns success, true if already closed
126 * @return bool
128 protected function closeConnection() {
129 return sqlsrv_close( $this->mConn );
132 protected function doQuery( $sql ) {
133 wfDebug( "SQL: [$sql]\n" );
134 $this->offset = 0;
136 // several extensions seem to think that all databases support limits via LIMIT N after the WHERE clause
137 // well, MSSQL uses SELECT TOP N, so to catch any of those extensions we'll do a quick check for a LIMIT
138 // clause and pass $sql through $this->LimitToTopN() which parses the limit clause and passes the result to
139 // $this->limitResult();
140 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
141 // massage LIMIT -> TopN
142 $sql = $this->LimitToTopN( $sql ) ;
145 // MSSQL doesn't have EXTRACT(epoch FROM XXX)
146 if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
147 // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
148 $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
151 // perform query
152 $stmt = sqlsrv_query( $this->mConn, $sql );
153 if ( $stmt == false ) {
154 $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" .
155 "Query: " . htmlentities( $sql ) . "\n" .
156 "Function: " . __METHOD__ . "\n";
157 // process each error (our driver will give us an array of errors unlike other providers)
158 foreach ( sqlsrv_errors() as $error ) {
159 $message .= $message . "ERROR[" . $error['code'] . "] " . $error['message'] . "\n";
162 throw new DBUnexpectedError( $this, $message );
164 // remember number of rows affected
165 $this->mAffectedRows = sqlsrv_rows_affected( $stmt );
167 // if it is a SELECT statement, or an insert with a request to output something we want to return a row.
168 if ( ( preg_match( '#\bSELECT\s#i', $sql ) ) ||
169 ( preg_match( '#\bINSERT\s#i', $sql ) && preg_match( '#\bOUTPUT\s+INSERTED\b#i', $sql ) ) ) {
170 // this is essentially a rowset, but Mediawiki calls these 'result'
171 // the rowset owns freeing the statement
172 $res = new MssqlResult( $stmt );
173 } else {
174 // otherwise we simply return it was successful, failure throws an exception
175 $res = true;
177 return $res;
180 function freeResult( $res ) {
181 if ( $res instanceof ResultWrapper ) {
182 $res = $res->result;
184 $res->free();
187 function fetchObject( $res ) {
188 if ( $res instanceof ResultWrapper ) {
189 $res = $res->result;
191 $row = $res->fetch( 'OBJECT' );
192 return $row;
195 function getErrors() {
196 $strRet = '';
197 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
198 if ( $retErrors != null ) {
199 foreach ( $retErrors as $arrError ) {
200 $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n";
201 $strRet .= "Error Code: " . $arrError[ 'code'] . "\n";
202 $strRet .= "Message: " . $arrError[ 'message'] . "\n";
204 } else {
205 $strRet = "No errors found";
207 return $strRet;
210 function fetchRow( $res ) {
211 if ( $res instanceof ResultWrapper ) {
212 $res = $res->result;
214 $row = $res->fetch( SQLSRV_FETCH_BOTH );
215 return $row;
218 function numRows( $res ) {
219 if ( $res instanceof ResultWrapper ) {
220 $res = $res->result;
222 return ( $res ) ? $res->numrows() : 0;
225 function numFields( $res ) {
226 if ( $res instanceof ResultWrapper ) {
227 $res = $res->result;
229 return ( $res ) ? $res->numfields() : 0;
232 function fieldName( $res, $n ) {
233 if ( $res instanceof ResultWrapper ) {
234 $res = $res->result;
236 return ( $res ) ? $res->fieldname( $n ) : 0;
240 * This must be called after nextSequenceVal
241 * @return null
243 function insertId() {
244 return $this->mInsertId;
247 function dataSeek( $res, $row ) {
248 if ( $res instanceof ResultWrapper ) {
249 $res = $res->result;
251 return ( $res ) ? $res->seek( $row ) : false;
254 function lastError() {
255 if ( $this->mConn ) {
256 return $this->getErrors();
257 } else {
258 return "No database connection";
262 function lastErrno() {
263 $err = sqlsrv_errors( SQLSRV_ERR_ALL );
264 if ( $err[0] ) {
265 return $err[0]['code'];
266 } else {
267 return 0;
271 function affectedRows() {
272 return $this->mAffectedRows;
276 * SELECT wrapper
278 * @param $table Mixed: array or string, table name(s) (prefix auto-added)
279 * @param $vars Mixed: array or string, field name(s) to be retrieved
280 * @param $conds Mixed: array or string, condition(s) for WHERE
281 * @param $fname String: calling function name (use __METHOD__) for logs/profiling
282 * @param $options Array: associative array of options (e.g. array('GROUP BY' => 'page_title')),
283 * see Database::makeSelectOptions code for list of supported stuff
284 * @param $join_conds Array: Associative array of table join conditions (optional)
285 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
286 * @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure
288 function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() )
290 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
291 if ( isset( $options['EXPLAIN'] ) ) {
292 sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL ON;" );
293 $ret = $this->query( $sql, $fname );
294 sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" );
295 return $ret;
297 return $this->query( $sql, $fname );
301 * SELECT wrapper
303 * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
304 * @param $vars Mixed: Array or string, field name(s) to be retrieved
305 * @param $conds Mixed: Array or string, condition(s) for WHERE
306 * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
307 * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
308 * see Database::makeSelectOptions code for list of supported stuff
309 * @param $join_conds Array: Associative array of table join conditions (optional)
310 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
311 * @return string, the SQL text
313 function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
314 if ( isset( $options['EXPLAIN'] ) ) {
315 unset( $options['EXPLAIN'] );
317 return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
321 * Estimate rows in dataset
322 * Returns estimated count, based on SHOWPLAN_ALL output
323 * This is not necessarily an accurate estimate, so use sparingly
324 * Returns -1 if count cannot be found
325 * Takes same arguments as Database::select()
326 * @return int
328 function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
329 $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
330 $res = $this->select( $table, $vars, $conds, $fname, $options );
332 $rows = -1;
333 if ( $res ) {
334 $row = $this->fetchRow( $res );
335 if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
337 return $rows;
341 * Returns information about an index
342 * If errors are explicitly ignored, returns NULL on failure
343 * @return array|bool|null
345 function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
346 # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
347 # returned value except to check for the existance of indexes.
348 $sql = "sp_helpindex '" . $table . "'";
349 $res = $this->query( $sql, $fname );
350 if ( !$res ) {
351 return NULL;
354 $result = array();
355 foreach ( $res as $row ) {
356 if ( $row->index_name == $index ) {
357 $row->Non_unique = !stristr( $row->index_description, "unique" );
358 $cols = explode( ", ", $row->index_keys );
359 foreach ( $cols as $col ) {
360 $row->Column_name = trim( $col );
361 $result[] = clone $row;
363 } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
364 $row->Non_unique = 0;
365 $cols = explode( ", ", $row->index_keys );
366 foreach ( $cols as $col ) {
367 $row->Column_name = trim( $col );
368 $result[] = clone $row;
372 return empty( $result ) ? false : $result;
376 * INSERT wrapper, inserts an array into a table
378 * $arrToInsert may be a single associative array, or an array of these with numeric keys, for
379 * multi-row insert.
381 * Usually aborts on failure
382 * If errors are explicitly ignored, returns success
383 * @return bool
385 function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
386 # No rows to insert, easy just return now
387 if ( !count( $arrToInsert ) ) {
388 return true;
391 if ( !is_array( $options ) ) {
392 $options = array( $options );
395 $table = $this->tableName( $table );
397 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row
398 $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible
401 $allOk = true;
403 // We know the table we're inserting into, get its identity column
404 $identity = null;
405 $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name
406 $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" );
407 if( $res && $res->numrows() ){
408 // There is an identity for this table.
409 $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) );
411 unset( $res );
413 foreach ( $arrToInsert as $a ) {
414 // start out with empty identity column, this is so we can return it as a result of the insert logic
415 $sqlPre = '';
416 $sqlPost = '';
417 $identityClause = '';
419 // if we have an identity column
420 if( $identity ) {
421 // iterate through
422 foreach ($a as $k => $v ) {
423 if ( $k == $identity ) {
424 if( !is_null($v) ){
425 // there is a value being passed to us, we need to turn on and off inserted identity
426 $sqlPre = "SET IDENTITY_INSERT $table ON;" ;
427 $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
429 } else {
430 // we can't insert NULL into an identity column, so remove the column from the insert.
431 unset( $a[$k] );
435 $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result
438 $keys = array_keys( $a );
440 // INSERT IGNORE is not supported by SQL Server
441 // remove IGNORE from options list and set ignore flag to true
442 $ignoreClause = false;
443 foreach ( $options as $k => $v ) {
444 if ( strtoupper( $v ) == "IGNORE" ) {
445 unset( $options[$k] );
446 $ignoreClause = true;
450 // translate MySQL INSERT IGNORE to something SQL Server can use
451 // example:
452 // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
453 // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
454 if ( $ignoreClause ) {
455 $prival = $a[$keys[0]];
456 $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')";
459 // Build the actual query
460 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
461 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
463 $first = true;
464 foreach ( $a as $value ) {
465 if ( $first ) {
466 $first = false;
467 } else {
468 $sql .= ',';
470 if ( is_string( $value ) ) {
471 $sql .= $this->addQuotes( $value );
472 } elseif ( is_null( $value ) ) {
473 $sql .= 'null';
474 } elseif ( is_array( $value ) || is_object( $value ) ) {
475 if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) {
476 $sql .= $this->addQuotes( $value );
477 } else {
478 $sql .= $this->addQuotes( serialize( $value ) );
480 } else {
481 $sql .= $value;
484 $sql .= ')' . $sqlPost;
486 // Run the query
487 $ret = sqlsrv_query( $this->mConn, $sql );
489 if ( $ret === false ) {
490 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
491 } elseif ( $ret != NULL ) {
492 // remember number of rows affected
493 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
494 if ( !is_null($identity) ) {
495 // then we want to get the identity column value we were assigned and save it off
496 $row = sqlsrv_fetch_object( $ret );
497 $this->mInsertId = $row->$identity;
499 sqlsrv_free_stmt( $ret );
500 continue;
502 $allOk = false;
504 return $allOk;
508 * INSERT SELECT wrapper
509 * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
510 * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
511 * $conds may be "*" to copy the whole table
512 * srcTable may be an array of tables.
513 * @return null|\ResultWrapper
515 function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
516 $insertOptions = array(), $selectOptions = array() ) {
517 $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
519 if ( $ret === false ) {
520 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), /*$sql*/ '', $fname );
521 } elseif ( $ret != NULL ) {
522 // remember number of rows affected
523 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
524 return $ret;
526 return NULL;
530 * Return the next in a sequence, save the value for retrieval via insertId()
531 * @return
533 function nextSequenceValue( $seqName ) {
534 if ( !$this->tableExists( 'sequence_' . $seqName ) ) {
535 sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" );
537 sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" );
538 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
539 $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't
541 sqlsrv_free_stmt( $ret );
542 $this->mInsertId = $row['id'];
543 return $row['id'];
547 * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
548 * @return
550 function currentSequenceValue( $seqName ) {
551 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
552 if ( $ret !== false ) {
553 $row = sqlsrv_fetch_array( $ret );
554 sqlsrv_free_stmt( $ret );
555 return $row['id'];
556 } else {
557 return $this->nextSequenceValue( $seqName );
561 # Returns the size of a text field, or -1 for "unlimited"
562 function textFieldSize( $table, $field ) {
563 $table = $this->tableName( $table );
564 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
565 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
566 $res = $this->query( $sql );
567 $row = $this->fetchRow( $res );
568 $size = -1;
569 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) {
570 $size = $row['CHARACTER_MAXIMUM_LENGTH'];
572 return $size;
576 * Construct a LIMIT query with optional offset
577 * This is used for query pages
578 * $sql string SQL query we will append the limit too
579 * $limit integer the SQL limit
580 * $offset integer the SQL offset (default false)
581 * @return mixed|string
583 function limitResult( $sql, $limit, $offset = false ) {
584 if ( $offset === false || $offset == 0 ) {
585 if ( strpos( $sql, "SELECT" ) === false ) {
586 return "TOP {$limit} " . $sql;
587 } else {
588 return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 );
590 } else {
591 $sql = '
592 SELECT * FROM (
593 SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
594 SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
595 ) as sub2
596 ) AS sub3
597 WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit );
598 return $sql;
602 // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult()
603 // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser.
604 // This exists becase there are still too many extensions that don't use dynamic sql generation.
605 function LimitToTopN( $sql ) {
606 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
607 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
608 if ( preg_match( $pattern, $sql, $matches ) ) {
609 // row_count = $matches[4]
610 $row_count = $matches[4];
611 // offset = $matches[3] OR $matches[6]
612 $offset = $matches[3] or
613 $offset = $matches[6] or
614 $offset = false;
616 // strip the matching LIMIT clause out
617 $sql = str_replace( $matches[0], '', $sql );
618 return $this->limitResult( $sql, $row_count, $offset );
620 return $sql;
623 function timestamp( $ts = 0 ) {
624 return wfTimestamp( TS_ISO_8601, $ts );
628 * @return string wikitext of a link to the server software's web site
630 public static function getSoftwareLink() {
631 return "[http://www.microsoft.com/sql/ MS SQL Server]";
635 * @return string Version information from the database
637 function getServerVersion() {
638 $server_info = sqlsrv_server_info( $this->mConn );
639 $version = 'Error';
640 if ( isset( $server_info['SQLServerVersion'] ) ) {
641 $version = $server_info['SQLServerVersion'];
643 return $version;
646 function tableExists ( $table, $fname = __METHOD__, $schema = false ) {
647 $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
648 WHERE table_type='BASE TABLE' AND table_name = '$table'" );
649 if ( $res === false ) {
650 print( "Error in tableExists query: " . $this->getErrors() );
651 return false;
653 if ( sqlsrv_fetch( $res ) ) {
654 return true;
655 } else {
656 return false;
661 * Query whether a given column exists in the mediawiki schema
662 * @return bool
664 function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
665 $table = $this->tableName( $table );
666 $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
667 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
668 if ( $res === false ) {
669 print( "Error in fieldExists query: " . $this->getErrors() );
670 return false;
672 if ( sqlsrv_fetch( $res ) ) {
673 return true;
674 } else {
675 return false;
679 function fieldInfo( $table, $field ) {
680 $table = $this->tableName( $table );
681 $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
682 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
683 if ( $res === false ) {
684 print( "Error in fieldInfo query: " . $this->getErrors() );
685 return false;
687 $meta = $this->fetchRow( $res );
688 if ( $meta ) {
689 return new MssqlField( $meta );
691 return false;
695 * Begin a transaction, committing any previously open transaction
697 protected function doBegin( $fname = 'DatabaseMssql::begin' ) {
698 sqlsrv_begin_transaction( $this->mConn );
699 $this->mTrxLevel = 1;
703 * End a transaction
705 protected function doCommit( $fname = 'DatabaseMssql::commit' ) {
706 sqlsrv_commit( $this->mConn );
707 $this->mTrxLevel = 0;
711 * Rollback a transaction.
712 * No-op on non-transactional databases.
714 protected function doRollback( $fname = 'DatabaseMssql::rollback' ) {
715 sqlsrv_rollback( $this->mConn );
716 $this->mTrxLevel = 0;
720 * Escapes a identifier for use inm SQL.
721 * Throws an exception if it is invalid.
722 * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
723 * @return string
725 private function escapeIdentifier( $identifier ) {
726 if ( strlen( $identifier ) == 0 ) {
727 throw new MWException( "An identifier must not be empty" );
729 if ( strlen( $identifier ) > 128 ) {
730 throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
732 if ( ( strpos( $identifier, '[' ) !== false ) || ( strpos( $identifier, ']' ) !== false ) ) {
733 // It may be allowed if you quoted with double quotation marks, but that would break if QUOTED_IDENTIFIER is OFF
734 throw new MWException( "You can't use square brackers in the identifier '$identifier'" );
736 return "[$identifier]";
740 * Initial setup.
741 * Precondition: This object is connected as the superuser.
742 * Creates the database, schema, user and login.
744 function initial_setup( $dbName, $newUser, $loginPassword ) {
745 $dbName = $this->escapeIdentifier( $dbName );
747 // It is not clear what can be used as a login,
748 // From http://msdn.microsoft.com/en-us/library/ms173463.aspx
749 // a sysname may be the same as an identifier.
750 $newUser = $this->escapeIdentifier( $newUser );
751 $loginPassword = $this->addQuotes( $loginPassword );
753 $this->doQuery("CREATE DATABASE $dbName;");
754 $this->doQuery("USE $dbName;");
755 $this->doQuery("CREATE SCHEMA $dbName;");
756 $this->doQuery("
757 CREATE
758 LOGIN $newUser
759 WITH
760 PASSWORD=$loginPassword
763 $this->doQuery("
764 CREATE
765 USER $newUser
767 LOGIN $newUser
768 WITH
769 DEFAULT_SCHEMA=$dbName
772 $this->doQuery("
773 GRANT
774 BACKUP DATABASE,
775 BACKUP LOG,
776 CREATE DEFAULT,
777 CREATE FUNCTION,
778 CREATE PROCEDURE,
779 CREATE RULE,
780 CREATE TABLE,
781 CREATE VIEW,
782 CREATE FULLTEXT CATALOG
784 DATABASE::$dbName
785 TO $newUser
788 $this->doQuery("
789 GRANT
790 CONTROL
792 SCHEMA::$dbName
793 TO $newUser
800 function encodeBlob( $b ) {
801 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
802 return base64_encode( $b );
805 function decodeBlob( $b ) {
806 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
807 return base64_decode( $b );
811 * @private
812 * @return string
814 function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
815 $ret = array();
816 $retJOIN = array();
817 $use_index_safe = is_array( $use_index ) ? $use_index : array();
818 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
819 foreach ( $tables as $table ) {
820 // Is there a JOIN and INDEX clause for this table?
821 if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
822 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
823 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
824 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
825 $retJOIN[] = $tableClause;
826 // Is there an INDEX clause?
827 } elseif ( isset( $use_index_safe[$table] ) ) {
828 $tableClause = $this->tableName( $table );
829 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
830 $ret[] = $tableClause;
831 // Is there a JOIN clause?
832 } elseif ( isset( $join_conds_safe[$table] ) ) {
833 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
834 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
835 $retJOIN[] = $tableClause;
836 } else {
837 $tableClause = $this->tableName( $table );
838 $ret[] = $tableClause;
841 // We can't separate explicit JOIN clauses with ',', use ' ' for those
842 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
843 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
844 // Compile our final table clause
845 return implode( ' ', array( $straightJoins, $otherJoins ) );
848 function strencode( $s ) { # Should not be called by us
849 return str_replace( "'", "''", $s );
852 function addQuotes( $s ) {
853 if ( $s instanceof Blob ) {
854 return "'" . $s->fetch( $s ) . "'";
855 } else {
856 return parent::addQuotes( $s );
860 public function addIdentifierQuotes( $s ) {
861 // http://msdn.microsoft.com/en-us/library/aa223962.aspx
862 return '[' . $s . ']';
865 public function isQuotedIdentifier( $name ) {
866 return $name[0] == '[' && substr( $name, -1, 1 ) == ']';
869 function selectDB( $db ) {
870 return ( $this->query( "SET DATABASE $db" ) !== false );
874 * @private
876 * @param $options Array: an associative array of options to be turned into
877 * an SQL query, valid keys are listed in the function.
878 * @return Array
880 function makeSelectOptions( $options ) {
881 $tailOpts = '';
882 $startOpts = '';
884 $noKeyOptions = array();
885 foreach ( $options as $key => $option ) {
886 if ( is_numeric( $key ) ) {
887 $noKeyOptions[$option] = true;
891 if ( isset( $options['GROUP BY'] ) ) {
892 $tailOpts .= " GROUP BY {$options['GROUP BY']}";
894 if ( isset( $options['HAVING'] ) ) {
895 $tailOpts .= " HAVING {$options['GROUP BY']}";
897 if ( isset( $options['ORDER BY'] ) ) {
898 $tailOpts .= " ORDER BY {$options['ORDER BY']}";
901 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) {
902 $startOpts .= 'DISTINCT';
905 // we want this to be compatible with the output of parent::makeSelectOptions()
906 return array( $startOpts, '' , $tailOpts, '' );
910 * Get the type of the DBMS, as it appears in $wgDBtype.
911 * @return string
913 function getType(){
914 return 'mssql';
917 function buildConcat( $stringList ) {
918 return implode( ' + ', $stringList );
921 public function getSearchEngine() {
922 return "SearchMssql";
926 * Since MSSQL doesn't recognize the infinity keyword, set date manually.
927 * @todo Remove magic date
928 * @return string
930 public function getInfinity() {
931 return '3000-01-31 00:00:00.000';
934 } // end DatabaseMssql class
937 * Utility class.
939 * @ingroup Database
941 class MssqlField implements Field {
942 private $name, $tablename, $default, $max_length, $nullable, $type;
943 function __construct ( $info ) {
944 $this->name = $info['COLUMN_NAME'];
945 $this->tablename = $info['TABLE_NAME'];
946 $this->default = $info['COLUMN_DEFAULT'];
947 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
948 $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' );
949 $this->type = $info['DATA_TYPE'];
952 function name() {
953 return $this->name;
956 function tableName() {
957 return $this->tableName;
960 function defaultValue() {
961 return $this->default;
964 function maxLength() {
965 return $this->max_length;
968 function isNullable() {
969 return $this->nullable;
972 function type() {
973 return $this->type;
978 * The MSSQL PHP driver doesn't support sqlsrv_num_rows, so we recall all rows into an array and maintain our
979 * own cursor index into that array...This is similar to the way the Oracle driver handles this same issue
981 * @ingroup Database
983 class MssqlResult {
985 public function __construct( $queryresult = false ) {
986 $this->mCursor = 0;
987 $this->mRows = array();
988 $this->mNumFields = sqlsrv_num_fields( $queryresult );
989 $this->mFieldMeta = sqlsrv_field_metadata( $queryresult );
991 $rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC );
993 foreach( $rows as $row ) {
994 if ( $row !== null ) {
995 foreach ( $row as $k => $v ) {
996 if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object
997 $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" );
1000 $this->mRows[] = $row;// read results into memory, cursors are not supported
1003 $this->mRowCount = count( $this->mRows );
1004 sqlsrv_free_stmt( $queryresult );
1007 private function array_to_obj( $array, &$obj ) {
1008 foreach ( $array as $key => $value ) {
1009 if ( is_array( $value ) ) {
1010 $obj->$key = new stdClass();
1011 $this->array_to_obj( $value, $obj->$key );
1012 } else {
1013 if ( !empty( $key ) ) {
1014 $obj->$key = $value;
1018 return $obj;
1021 public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) {
1022 if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) {
1023 return false;
1025 $arrNum = array();
1026 if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
1027 foreach ( $this->mRows[$this->mCursor] as $value ) {
1028 $arrNum[] = $value;
1031 switch( $mode ) {
1032 case SQLSRV_FETCH_ASSOC:
1033 $ret = $this->mRows[$this->mCursor];
1034 break;
1035 case SQLSRV_FETCH_NUMERIC:
1036 $ret = $arrNum;
1037 break;
1038 case 'OBJECT':
1039 $o = new $object_class;
1040 $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o );
1041 break;
1042 case SQLSRV_FETCH_BOTH:
1043 default:
1044 $ret = $this->mRows[$this->mCursor] + $arrNum;
1045 break;
1048 $this->mCursor++;
1049 return $ret;
1052 public function get( $pos, $fld ) {
1053 return $this->mRows[$pos][$fld];
1056 public function numrows() {
1057 return $this->mRowCount;
1060 public function seek( $iRow ) {
1061 $this->mCursor = min( $iRow, $this->mRowCount );
1064 public function numfields() {
1065 return $this->mNumFields;
1068 public function fieldname( $nr ) {
1069 $arrKeys = array_keys( $this->mRows[0] );
1070 return $arrKeys[$nr];
1073 public function fieldtype( $nr ) {
1074 $i = 0;
1075 $intType = -1;
1076 foreach ( $this->mFieldMeta as $meta ) {
1077 if ( $nr == $i ) {
1078 $intType = $meta['Type'];
1079 break;
1081 $i++;
1083 // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
1084 switch( $intType ) {
1085 case SQLSRV_SQLTYPE_BIGINT: $strType = 'bigint'; break;
1086 case SQLSRV_SQLTYPE_BINARY: $strType = 'binary'; break;
1087 case SQLSRV_SQLTYPE_BIT: $strType = 'bit'; break;
1088 case SQLSRV_SQLTYPE_CHAR: $strType = 'char'; break;
1089 case SQLSRV_SQLTYPE_DATETIME: $strType = 'datetime'; break;
1090 case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break;
1091 case SQLSRV_SQLTYPE_FLOAT: $strType = 'float'; break;
1092 case SQLSRV_SQLTYPE_IMAGE: $strType = 'image'; break;
1093 case SQLSRV_SQLTYPE_INT: $strType = 'int'; break;
1094 case SQLSRV_SQLTYPE_MONEY: $strType = 'money'; break;
1095 case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break;
1096 case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break;
1097 case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break;
1098 // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break;
1099 case SQLSRV_SQLTYPE_NTEXT: $strType = 'ntext'; break;
1100 case SQLSRV_SQLTYPE_REAL: $strType = 'real'; break;
1101 case SQLSRV_SQLTYPE_SMALLDATETIME: $strType = 'smalldatetime'; break;
1102 case SQLSRV_SQLTYPE_SMALLINT: $strType = 'smallint'; break;
1103 case SQLSRV_SQLTYPE_SMALLMONEY: $strType = 'smallmoney'; break;
1104 case SQLSRV_SQLTYPE_TEXT: $strType = 'text'; break;
1105 case SQLSRV_SQLTYPE_TIMESTAMP: $strType = 'timestamp'; break;
1106 case SQLSRV_SQLTYPE_TINYINT: $strType = 'tinyint'; break;
1107 case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break;
1108 case SQLSRV_SQLTYPE_UDT: $strType = 'UDT'; break;
1109 case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break;
1110 // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break;
1111 case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break;
1112 // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break;
1113 case SQLSRV_SQLTYPE_XML: $strType = 'xml'; break;
1114 default: $strType = $intType;
1116 return $strType;
1119 public function free() {
1120 unset( $this->mRows );
1121 return;