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