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