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