Remove ancient and deprecated newFromParams() constructor wrappers from various Datab...
[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 /**
53 * Usually aborts on failure
54 */
55 function open( $server, $user, $password, $dbName ) {
56 # Test for driver support, to avoid suppressed fatal error
57 if ( !function_exists( 'sqlsrv_connect' ) ) {
58 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" );
59 }
60
61 global $wgDBport;
62
63 if ( !strlen( $user ) ) { # e.g. the class is being loaded
64 return;
65 }
66
67 $this->close();
68 $this->mServer = $server;
69 $this->mPort = $wgDBport;
70 $this->mUser = $user;
71 $this->mPassword = $password;
72 $this->mDBname = $dbName;
73
74 $connectionInfo = array();
75
76 if( $dbName ) {
77 $connectionInfo['Database'] = $dbName;
78 }
79
80 // Start NT Auth Hack
81 // Quick and dirty work around to provide NT Auth designation support.
82 // Current solution requires installer to know to input 'ntauth' for both username and password
83 // to trigger connection via NT Auth. - ugly, ugly, ugly
84 // TO-DO: Make this better and add NT Auth choice to MW installer when SQL Server option is chosen.
85 $ntAuthUserTest = strtolower( $user );
86 $ntAuthPassTest = strtolower( $password );
87
88 // Decide which auth scenerio to use
89 if( ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) ){
90 // Don't add credentials to $connectionInfo
91 } else {
92 $connectionInfo['UID'] = $user;
93 $connectionInfo['PWD'] = $password;
94 }
95 // End NT Auth Hack
96
97 $this->mConn = @sqlsrv_connect( $server, $connectionInfo );
98
99 if ( $this->mConn === false ) {
100 wfDebug( "DB connection error\n" );
101 wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
102 wfDebug( $this->lastError() . "\n" );
103 return false;
104 }
105
106 $this->mOpened = true;
107 return $this->mConn;
108 }
109
110 /**
111 * Closes a database connection, if it is open
112 * Returns success, true if already closed
113 */
114 function close() {
115 $this->mOpened = false;
116 if ( $this->mConn ) {
117 return sqlsrv_close( $this->mConn );
118 } else {
119 return true;
120 }
121 }
122
123 function doQuery( $sql ) {
124 wfDebug( "SQL: [$sql]\n" );
125 $this->offset = 0;
126
127 // several extensions seem to think that all databases support limits via LIMIT N after the WHERE clause
128 // well, MSSQL uses SELECT TOP N, so to catch any of those extensions we'll do a quick check for a LIMIT
129 // clause and pass $sql through $this->LimitToTopN() which parses the limit clause and passes the result to
130 // $this->limitResult();
131 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
132 // massage LIMIT -> TopN
133 $sql = $this->LimitToTopN( $sql ) ;
134 }
135
136 // MSSQL doesn't have EXTRACT(epoch FROM XXX)
137 if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
138 // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
139 $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
140 }
141
142 // perform query
143 $stmt = sqlsrv_query( $this->mConn, $sql );
144 if ( $stmt == false ) {
145 $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" .
146 "Query: " . htmlentities( $sql ) . "\n" .
147 "Function: " . __METHOD__ . "\n";
148 // process each error (our driver will give us an array of errors unlike other providers)
149 foreach ( sqlsrv_errors() as $error ) {
150 $message .= $message . "ERROR[" . $error['code'] . "] " . $error['message'] . "\n";
151 }
152
153 throw new DBUnexpectedError( $this, $message );
154 }
155 // remember number of rows affected
156 $this->mAffectedRows = sqlsrv_rows_affected( $stmt );
157
158 // if it is a SELECT statement, or an insert with a request to output something we want to return a row.
159 if ( ( preg_match( '#\bSELECT\s#i', $sql ) ) ||
160 ( preg_match( '#\bINSERT\s#i', $sql ) && preg_match( '#\bOUTPUT\s+INSERTED\b#i', $sql ) ) ) {
161 // this is essentially a rowset, but Mediawiki calls these 'result'
162 // the rowset owns freeing the statement
163 $res = new MssqlResult( $stmt );
164 } else {
165 // otherwise we simply return it was successful, failure throws an exception
166 $res = true;
167 }
168 return $res;
169 }
170
171 function freeResult( $res ) {
172 if ( $res instanceof ResultWrapper ) {
173 $res = $res->result;
174 }
175 $res->free();
176 }
177
178 function fetchObject( $res ) {
179 if ( $res instanceof ResultWrapper ) {
180 $res = $res->result;
181 }
182 $row = $res->fetch( 'OBJECT' );
183 return $row;
184 }
185
186 function getErrors() {
187 $strRet = '';
188 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
189 if ( $retErrors != null ) {
190 foreach ( $retErrors as $arrError ) {
191 $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n";
192 $strRet .= "Error Code: " . $arrError[ 'code'] . "\n";
193 $strRet .= "Message: " . $arrError[ 'message'] . "\n";
194 }
195 } else {
196 $strRet = "No errors found";
197 }
198 return $strRet;
199 }
200
201 function fetchRow( $res ) {
202 if ( $res instanceof ResultWrapper ) {
203 $res = $res->result;
204 }
205 $row = $res->fetch( SQLSRV_FETCH_BOTH );
206 return $row;
207 }
208
209 function numRows( $res ) {
210 if ( $res instanceof ResultWrapper ) {
211 $res = $res->result;
212 }
213 return ( $res ) ? $res->numrows() : 0;
214 }
215
216 function numFields( $res ) {
217 if ( $res instanceof ResultWrapper ) {
218 $res = $res->result;
219 }
220 return ( $res ) ? $res->numfields() : 0;
221 }
222
223 function fieldName( $res, $n ) {
224 if ( $res instanceof ResultWrapper ) {
225 $res = $res->result;
226 }
227 return ( $res ) ? $res->fieldname( $n ) : 0;
228 }
229
230 /**
231 * This must be called after nextSequenceVal
232 */
233 function insertId() {
234 return $this->mInsertId;
235 }
236
237 function dataSeek( $res, $row ) {
238 if ( $res instanceof ResultWrapper ) {
239 $res = $res->result;
240 }
241 return ( $res ) ? $res->seek( $row ) : false;
242 }
243
244 function lastError() {
245 if ( $this->mConn ) {
246 return $this->getErrors();
247 }
248 else {
249 return "No database connection";
250 }
251 }
252
253 function lastErrno() {
254 $err = sqlsrv_errors( SQLSRV_ERR_ALL );
255 if ( $err[0] ) return $err[0]['code'];
256 else return 0;
257 }
258
259 function affectedRows() {
260 return $this->mAffectedRows;
261 }
262
263 /**
264 * SELECT wrapper
265 *
266 * @param $table Mixed: array or string, table name(s) (prefix auto-added)
267 * @param $vars Mixed: array or string, field name(s) to be retrieved
268 * @param $conds Mixed: array or string, condition(s) for WHERE
269 * @param $fname String: calling function name (use __METHOD__) for logs/profiling
270 * @param $options Array: associative array of options (e.g. array('GROUP BY' => 'page_title')),
271 * see Database::makeSelectOptions code for list of supported stuff
272 * @param $join_conds Array: Associative array of table join conditions (optional)
273 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
274 * @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure
275 */
276 function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() )
277 {
278 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
279 if ( isset( $options['EXPLAIN'] ) ) {
280 sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL ON;" );
281 $ret = $this->query( $sql, $fname );
282 sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" );
283 return $ret;
284 }
285 return $this->query( $sql, $fname );
286 }
287
288 /**
289 * SELECT wrapper
290 *
291 * @param $table Mixed: Array or string, table name(s) (prefix auto-added)
292 * @param $vars Mixed: Array or string, field name(s) to be retrieved
293 * @param $conds Mixed: Array or string, condition(s) for WHERE
294 * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
295 * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')),
296 * see Database::makeSelectOptions code for list of supported stuff
297 * @param $join_conds Array: Associative array of table join conditions (optional)
298 * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
299 * @return string, the SQL text
300 */
301 function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
302 if ( isset( $options['EXPLAIN'] ) ) {
303 unset( $options['EXPLAIN'] );
304 }
305 return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
306 }
307
308 /**
309 * Estimate rows in dataset
310 * Returns estimated count, based on SHOWPLAN_ALL output
311 * This is not necessarily an accurate estimate, so use sparingly
312 * Returns -1 if count cannot be found
313 * Takes same arguments as Database::select()
314 */
315 function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
316 $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
317 $res = $this->select( $table, $vars, $conds, $fname, $options );
318
319 $rows = -1;
320 if ( $res ) {
321 $row = $this->fetchRow( $res );
322 if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
323 }
324 return $rows;
325 }
326
327
328 /**
329 * Returns information about an index
330 * If errors are explicitly ignored, returns NULL on failure
331 */
332 function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
333 # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
334 # returned value except to check for the existance of indexes.
335 $sql = "sp_helpindex '" . $table . "'";
336 $res = $this->query( $sql, $fname );
337 if ( !$res ) {
338 return NULL;
339 }
340
341 $result = array();
342 foreach ( $res as $row ) {
343 if ( $row->index_name == $index ) {
344 $row->Non_unique = !stristr( $row->index_description, "unique" );
345 $cols = explode( ", ", $row->index_keys );
346 foreach ( $cols as $col ) {
347 $row->Column_name = trim( $col );
348 $result[] = clone $row;
349 }
350 } else if ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
351 $row->Non_unique = 0;
352 $cols = explode( ", ", $row->index_keys );
353 foreach ( $cols as $col ) {
354 $row->Column_name = trim( $col );
355 $result[] = clone $row;
356 }
357 }
358 }
359 return empty( $result ) ? false : $result;
360 }
361
362 /**
363 * INSERT wrapper, inserts an array into a table
364 *
365 * $arrToInsert may be a single associative array, or an array of these with numeric keys, for
366 * multi-row insert.
367 *
368 * Usually aborts on failure
369 * If errors are explicitly ignored, returns success
370 */
371 function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
372 # No rows to insert, easy just return now
373 if ( !count( $arrToInsert ) ) {
374 return true;
375 }
376
377 if ( !is_array( $options ) ) {
378 $options = array( $options );
379 }
380
381 $table = $this->tableName( $table );
382
383 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row
384 $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible
385 }
386
387 $allOk = true;
388
389
390 // We know the table we're inserting into, get its identity column
391 $identity = null;
392 $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name
393 $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" );
394 if( $res && $res->numrows() ){
395 // There is an identity for this table.
396 $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) );
397 }
398 unset( $res );
399
400 foreach ( $arrToInsert as $a ) {
401 // start out with empty identity column, this is so we can return it as a result of the insert logic
402 $sqlPre = '';
403 $sqlPost = '';
404 $identityClause = '';
405
406 // if we have an identity column
407 if( $identity ) {
408 // iterate through
409 foreach ($a as $k => $v ) {
410 if ( $k == $identity ) {
411 if( !is_null($v) ){
412 // there is a value being passed to us, we need to turn on and off inserted identity
413 $sqlPre = "SET IDENTITY_INSERT $table ON;" ;
414 $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
415
416 } else {
417 // we can't insert NULL into an identity column, so remove the column from the insert.
418 unset( $a[$k] );
419 }
420 }
421 }
422 $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result
423 }
424
425 $keys = array_keys( $a );
426
427
428 // INSERT IGNORE is not supported by SQL Server
429 // remove IGNORE from options list and set ignore flag to true
430 $ignoreClause = false;
431 foreach ( $options as $k => $v ) {
432 if ( strtoupper( $v ) == "IGNORE" ) {
433 unset( $options[$k] );
434 $ignoreClause = true;
435 }
436 }
437
438 // translate MySQL INSERT IGNORE to something SQL Server can use
439 // example:
440 // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
441 // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
442 if ( $ignoreClause == true ) {
443 $prival = $a[$keys[0]];
444 $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')";
445 }
446
447 // Build the actual query
448 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
449 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
450
451 $first = true;
452 foreach ( $a as $value ) {
453 if ( $first ) {
454 $first = false;
455 } else {
456 $sql .= ',';
457 }
458 if ( is_string( $value ) ) {
459 $sql .= $this->addIdentifierQuotes( $value );
460 } elseif ( is_null( $value ) ) {
461 $sql .= 'null';
462 } elseif ( is_array( $value ) || is_object( $value ) ) {
463 if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) {
464 $sql .= $this->addIdentifierQuotes( $value->fetch() );
465 } else {
466 $sql .= $this->addIdentifierQuotes( serialize( $value ) );
467 }
468 } else {
469 $sql .= $value;
470 }
471 }
472 $sql .= ')' . $sqlPost;
473
474 // Run the query
475 $ret = sqlsrv_query( $this->mConn, $sql );
476
477 if ( $ret === false ) {
478 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
479 } elseif ( $ret != NULL ) {
480 // remember number of rows affected
481 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
482 if ( !is_null($identity) ) {
483 // then we want to get the identity column value we were assigned and save it off
484 $row = sqlsrv_fetch_object( $ret );
485 $this->mInsertId = $row->$identity;
486 }
487 sqlsrv_free_stmt( $ret );
488 continue;
489 }
490 $allOk = false;
491 }
492 return $allOk;
493 }
494
495 /**
496 * INSERT SELECT wrapper
497 * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
498 * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
499 * $conds may be "*" to copy the whole table
500 * srcTable may be an array of tables.
501 */
502 function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
503 $insertOptions = array(), $selectOptions = array() )
504 {
505 $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
506
507 if ( $ret === false ) {
508 throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname );
509 } elseif ( $ret != NULL ) {
510 // remember number of rows affected
511 $this->mAffectedRows = sqlsrv_rows_affected( $ret );
512 return $ret;
513 }
514 return NULL;
515 }
516
517 /**
518 * Format a table name ready for use in constructing an SQL query
519 *
520 * This does two important things: it brackets table names which as necessary,
521 * and it adds a table prefix if there is one.
522 *
523 * All functions of this object which require a table name call this function
524 * themselves. Pass the canonical name to such functions. This is only needed
525 * when calling query() directly.
526 *
527 * @param $name String: database table name
528 */
529 function tableName( $name ) {
530 global $wgSharedDB;
531 # Skip quoted literals
532 if ( $name != '' && $name { 0 } != '[' ) {
533 if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) {
534 $name = "{$this->mTablePrefix}$name";
535 }
536 if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
537 $name = "[$wgSharedDB].[$name]";
538 } else {
539 # Standard quoting
540 if ( $name != '' ) $name = "[$name]";
541 }
542 }
543 return $name;
544 }
545
546 /**
547 * Return the next in a sequence, save the value for retrieval via insertId()
548 */
549 function nextSequenceValue( $seqName ) {
550 if ( !$this->tableExists( 'sequence_' . $seqName ) ) {
551 sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" );
552 }
553 sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" );
554 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
555 $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't
556
557 sqlsrv_free_stmt( $ret );
558 $this->mInsertId = $row['id'];
559 return $row['id'];
560 }
561
562 /**
563 * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
564 */
565 function currentSequenceValue( $seqName ) {
566 $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" );
567 if ( $ret !== false ) {
568 $row = sqlsrv_fetch_array( $ret );
569 sqlsrv_free_stmt( $ret );
570 return $row['id'];
571 } else {
572 return $this->nextSequenceValue( $seqName );
573 }
574 }
575
576
577 # REPLACE query wrapper
578 # MSSQL simulates this with a DELETE followed by INSERT
579 # $row is the row to insert, an associative array
580 # $uniqueIndexes is an array of indexes. Each element may be either a
581 # field name or an array of field names
582 #
583 # It may be more efficient to leave off unique indexes which are unlikely to collide.
584 # However if you do this, you run the risk of encountering errors which wouldn't have
585 # occurred in MySQL
586 function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMssql::replace' ) {
587 $table = $this->tableName( $table );
588
589 if ( count( $rows ) == 0 ) {
590 return;
591 }
592
593 # Single row case
594 if ( !is_array( reset( $rows ) ) ) {
595 $rows = array( $rows );
596 }
597
598 foreach ( $rows as $row ) {
599 # Delete rows which collide
600 if ( $uniqueIndexes ) {
601 $sql = "DELETE FROM $table WHERE ";
602 $first = true;
603 foreach ( $uniqueIndexes as $index ) {
604 if ( $first ) {
605 $first = false;
606 $sql .= "(";
607 } else {
608 $sql .= ') OR (';
609 }
610 if ( is_array( $index ) ) {
611 $first2 = true;
612 foreach ( $index as $col ) {
613 if ( $first2 ) {
614 $first2 = false;
615 } else {
616 $sql .= ' AND ';
617 }
618 $sql .= $col . '=' . $this->addQuotes( $row[$col] );
619 }
620 } else {
621 $sql .= $index . '=' . $this->addQuotes( $row[$index] );
622 }
623 }
624 $sql .= ')';
625 $this->query( $sql, $fname );
626 }
627
628 # Now insert the row
629 $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) . ') VALUES (' .
630 $this->makeList( $row, LIST_COMMA ) . ')';
631 $this->query( $sql, $fname );
632 }
633 }
634
635 # DELETE where the condition is a join
636 function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseMssql::deleteJoin" ) {
637 if ( !$conds ) {
638 throw new DBUnexpectedError( $this, 'DatabaseMssql::deleteJoin() called with empty $conds' );
639 }
640
641 $delTable = $this->tableName( $delTable );
642 $joinTable = $this->tableName( $joinTable );
643 $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
644 if ( $conds != '*' ) {
645 $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
646 }
647 $sql .= ')';
648
649 $this->query( $sql, $fname );
650 }
651
652 # Returns the size of a text field, or -1 for "unlimited"
653 function textFieldSize( $table, $field ) {
654 $table = $this->tableName( $table );
655 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
656 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
657 $res = $this->query( $sql );
658 $row = $this->fetchRow( $res );
659 $size = -1;
660 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) $size = $row['CHARACTER_MAXIMUM_LENGTH'];
661 return $size;
662 }
663
664 /**
665 * Construct a LIMIT query with optional offset
666 * This is used for query pages
667 * $sql string SQL query we will append the limit too
668 * $limit integer the SQL limit
669 * $offset integer the SQL offset (default false)
670 */
671 function limitResult( $sql, $limit, $offset = false ) {
672 if ( $offset === false || $offset == 0 ) {
673 if ( strpos( $sql, "SELECT" ) === false ) {
674 return "TOP {$limit} " . $sql;
675 } else {
676 return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 );
677 }
678 } else {
679 $sql = '
680 SELECT * FROM (
681 SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM (
682 SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1
683 ) as sub2
684 ) AS sub3
685 WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit );
686 return $sql;
687 }
688 }
689
690 // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult()
691 // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser.
692 // This exists becase there are still too many extensions that don't use dynamic sql generation.
693 function LimitToTopN( $sql ) {
694 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
695 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
696 if ( preg_match( $pattern, $sql, $matches ) ) {
697 // row_count = $matches[4]
698 $row_count = $matches[4];
699 // offset = $matches[3] OR $matches[6]
700 $offset = $matches[3] or
701 $offset = $matches[6] or
702 $offset = false;
703
704 // strip the matching LIMIT clause out
705 $sql = str_replace( $matches[0], '', $sql );
706 return $this->limitResult( $sql, $row_count, $offset );
707 }
708 return $sql;
709 }
710
711 // MSSQL does support this, but documentation is too thin to make a generalized
712 // function for this. Apparently UPDATE TOP (N) works, but the sort order
713 // may not be what we're expecting so the top n results may be a random selection.
714 // TODO: Implement properly.
715 function limitResultForUpdate( $sql, $num ) {
716 return $sql;
717 }
718
719
720 function timestamp( $ts = 0 ) {
721 return wfTimestamp( TS_ISO_8601, $ts );
722 }
723
724 /**
725 * @return string wikitext of a link to the server software's web site
726 */
727 public static function getSoftwareLink() {
728 return "[http://www.microsoft.com/sql/ MS SQL Server]";
729 }
730
731 /**
732 * @return string Version information from the database
733 */
734 function getServerVersion() {
735 $server_info = sqlsrv_server_info( $this->mConn );
736 $version = 'Error';
737 if ( isset( $server_info['SQLServerVersion'] ) ) $version = $server_info['SQLServerVersion'];
738 return $version;
739 }
740
741 function tableExists ( $table, $schema = false ) {
742 $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
743 WHERE table_type='BASE TABLE' AND table_name = '$table'" );
744 if ( $res === false ) {
745 print( "Error in tableExists query: " . $this->getErrors() );
746 return false;
747 }
748 if ( sqlsrv_fetch( $res ) )
749 return true;
750 else
751 return false;
752 }
753
754 /**
755 * Query whether a given column exists in the mediawiki schema
756 */
757 function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
758 $table = $this->tableName( $table );
759 $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
760 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
761 if ( $res === false ) {
762 print( "Error in fieldExists query: " . $this->getErrors() );
763 return false;
764 }
765 if ( sqlsrv_fetch( $res ) )
766 return true;
767 else
768 return false;
769 }
770
771 function fieldInfo( $table, $field ) {
772 $table = $this->tableName( $table );
773 $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
774 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
775 if ( $res === false ) {
776 print( "Error in fieldInfo query: " . $this->getErrors() );
777 return false;
778 }
779 $meta = $this->fetchRow( $res );
780 if ( $meta ) {
781 return new MssqlField( $meta );
782 }
783 return false;
784 }
785
786 /**
787 * Begin a transaction, committing any previously open transaction
788 */
789 function begin( $fname = 'DatabaseMssql::begin' ) {
790 sqlsrv_begin_transaction( $this->mConn );
791 $this->mTrxLevel = 1;
792 }
793
794 /**
795 * End a transaction
796 */
797 function commit( $fname = 'DatabaseMssql::commit' ) {
798 sqlsrv_commit( $this->mConn );
799 $this->mTrxLevel = 0;
800 }
801
802 /**
803 * Rollback a transaction.
804 * No-op on non-transactional databases.
805 */
806 function rollback( $fname = 'DatabaseMssql::rollback' ) {
807 sqlsrv_rollback( $this->mConn );
808 $this->mTrxLevel = 0;
809 }
810
811 function setup_database() {
812 global $wgDBuser;
813
814 // Make sure that we can write to the correct schema
815 $ctest = "mediawiki_test_table";
816 if ( $this->tableExists( $ctest ) ) {
817 $this->doQuery( "DROP TABLE $ctest" );
818 }
819 $SQL = "CREATE TABLE $ctest (a int)";
820 $res = $this->doQuery( $SQL );
821 if ( !$res ) {
822 print "<b>FAILED</b>. Make sure that the user " . htmlspecialchars( $wgDBuser ) . " can write to the database</li>\n";
823 dieout( );
824 }
825 $this->doQuery( "DROP TABLE $ctest" );
826
827 $res = $this->sourceFile( "../maintenance/mssql/tables.sql" );
828 if ( $res !== true ) {
829 echo " <b>FAILED</b></li>";
830 dieout( htmlspecialchars( $res ) );
831 }
832
833 # Avoid the non-standard "REPLACE INTO" syntax
834 $f = fopen( "../maintenance/interwiki.sql", 'r' );
835 if ( $f == false ) {
836 dieout( "<li>Could not find the interwiki.sql file" );
837 }
838 # We simply assume it is already empty as we have just created it
839 $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
840 while ( ! feof( $f ) ) {
841 $line = fgets( $f, 1024 );
842 $matches = array();
843 if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) {
844 continue;
845 }
846 $this->query( "$SQL $matches[1],$matches[2])" );
847 }
848 print " (table interwiki successfully populated)...\n";
849
850 $this->commit();
851 }
852
853 /**
854 * Escapes a identifier for use inm SQL.
855 * Throws an exception if it is invalid.
856 * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
857 */
858 private function escapeIdentifier( $identifier ) {
859 if ( strlen( $identifier ) == 0 ) {
860 throw new MWException( "An identifier must not be empty" );
861 }
862 if ( strlen( $identifier ) > 128 ) {
863 throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
864 }
865 if ( ( strpos( $identifier, '[' ) !== false ) || ( strpos( $identifier, ']' ) !== false ) ) {
866 // It may be allowed if you quoted with double quotation marks, but that would break if QUOTED_IDENTIFIER is OFF
867 throw new MWException( "You can't use square brackers in the identifier '$identifier'" );
868 }
869 return "[$identifier]";
870 }
871
872 /**
873 * Initial setup.
874 * Precondition: This object is connected as the superuser.
875 * Creates the database, schema, user and login.
876 */
877 function initial_setup( $dbName, $newUser, $loginPassword ) {
878 $dbName = $this->escapeIdentifier( $dbName );
879
880 // It is not clear what can be used as a login,
881 // From http://msdn.microsoft.com/en-us/library/ms173463.aspx
882 // a sysname may be the same as an identifier.
883 $newUser = $this->escapeIdentifier( $newUser );
884 $loginPassword = $this->addQuotes( $loginPassword );
885
886 $this->doQuery("CREATE DATABASE $dbName;");
887 $this->doQuery("USE $dbName;");
888 $this->doQuery("CREATE SCHEMA $dbName;");
889 $this->doQuery("
890 CREATE
891 LOGIN $newUser
892 WITH
893 PASSWORD=$loginPassword
894 ;
895 ");
896 $this->doQuery("
897 CREATE
898 USER $newUser
899 FOR
900 LOGIN $newUser
901 WITH
902 DEFAULT_SCHEMA=$dbName
903 ;
904 ");
905 $this->doQuery("
906 GRANT
907 BACKUP DATABASE,
908 BACKUP LOG,
909 CREATE DEFAULT,
910 CREATE FUNCTION,
911 CREATE PROCEDURE,
912 CREATE RULE,
913 CREATE TABLE,
914 CREATE VIEW,
915 CREATE FULLTEXT CATALOG
916 ON
917 DATABASE::$dbName
918 TO $newUser
919 ;
920 ");
921 $this->doQuery("
922 GRANT
923 CONTROL
924 ON
925 SCHEMA::$dbName
926 TO $newUser
927 ;
928 ");
929
930
931 }
932
933 function encodeBlob( $b ) {
934 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
935 return base64_encode( $b );
936 }
937
938 function decodeBlob( $b ) {
939 // we can't have zero's and such, this is a simple encoding to make sure we don't barf
940 return base64_decode( $b );
941 }
942
943 /**
944 * @private
945 */
946 function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
947 $ret = array();
948 $retJOIN = array();
949 $use_index_safe = is_array( $use_index ) ? $use_index : array();
950 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
951 foreach ( $tables as $table ) {
952 // Is there a JOIN and INDEX clause for this table?
953 if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
954 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
955 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
956 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
957 $retJOIN[] = $tableClause;
958 // Is there an INDEX clause?
959 } else if ( isset( $use_index_safe[$table] ) ) {
960 $tableClause = $this->tableName( $table );
961 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
962 $ret[] = $tableClause;
963 // Is there a JOIN clause?
964 } else if ( isset( $join_conds_safe[$table] ) ) {
965 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
966 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
967 $retJOIN[] = $tableClause;
968 } else {
969 $tableClause = $this->tableName( $table );
970 $ret[] = $tableClause;
971 }
972 }
973 // We can't separate explicit JOIN clauses with ',', use ' ' for those
974 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
975 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
976 // Compile our final table clause
977 return implode( ' ', array( $straightJoins, $otherJoins ) );
978 }
979
980 function strencode( $s ) { # Should not be called by us
981 return str_replace( "'", "''", $s );
982 }
983
984 function addQuotes( $s ) {
985 if ( $s instanceof Blob ) {
986 return "'" . $s->fetch( $s ) . "'";
987 } else {
988 return parent::addQuotes( $s );
989 }
990 }
991
992 function selectDB( $db ) {
993 return ( $this->query( "SET DATABASE $db" ) !== false );
994 }
995
996 /**
997 * @private
998 *
999 * @param $options Array: an associative array of options to be turned into
1000 * an SQL query, valid keys are listed in the function.
1001 * @return Array
1002 */
1003 function makeSelectOptions( $options ) {
1004 $tailOpts = '';
1005 $startOpts = '';
1006
1007 $noKeyOptions = array();
1008 foreach ( $options as $key => $option ) {
1009 if ( is_numeric( $key ) ) {
1010 $noKeyOptions[$option] = true;
1011 }
1012 }
1013
1014 if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}";
1015 if ( isset( $options['HAVING'] ) ) $tailOpts .= " HAVING {$options['GROUP BY']}";
1016 if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}";
1017
1018 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
1019
1020 // we want this to be compatible with the output of parent::makeSelectOptions()
1021 return array( $startOpts, '' , $tailOpts, '' );
1022 }
1023
1024 /**
1025 * Get the type of the DBMS, as it appears in $wgDBtype.
1026 */
1027 function getType(){
1028 return 'mssql';
1029 }
1030
1031 function buildConcat( $stringList ) {
1032 return implode( ' + ', $stringList );
1033 }
1034
1035 public function getSearchEngine() {
1036 return "SearchMssql";
1037 }
1038
1039 /**
1040 * Since MSSQL doesn't recognize the infinity keyword, set date manually.
1041 * @todo Remove magic date
1042 */
1043 public function getInfinity() {
1044 return '3000-01-31 00:00:00.000';
1045 }
1046
1047 } // end DatabaseMssql class
1048
1049 /**
1050 * Utility class.
1051 *
1052 * @ingroup Database
1053 */
1054 class MssqlField implements Field {
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 isNullable() {
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 $this->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 }