Merge "Throw exception on invalid RecentChange types"
[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 "[http://www.microsoft.com/sql/ 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(), $join_conds = array() ) {
988 $ret = array();
989 $retJOIN = array();
990 $use_index_safe = is_array( $use_index ) ? $use_index : array();
991 $join_conds_safe = is_array( $join_conds ) ? $join_conds : array();
992 foreach ( $tables as $table ) {
993 // Is there a JOIN and INDEX clause for this table?
994 if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) {
995 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
996 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
997 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
998 $retJOIN[] = $tableClause;
999 // Is there an INDEX clause?
1000 } elseif ( isset( $use_index_safe[$table] ) ) {
1001 $tableClause = $this->tableName( $table );
1002 $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
1003 $ret[] = $tableClause;
1004 // Is there a JOIN clause?
1005 } elseif ( isset( $join_conds_safe[$table] ) ) {
1006 $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
1007 $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')';
1008 $retJOIN[] = $tableClause;
1009 } else {
1010 $tableClause = $this->tableName( $table );
1011 $ret[] = $tableClause;
1012 }
1013 }
1014 // We can't separate explicit JOIN clauses with ',', use ' ' for those
1015 $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
1016 $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
1017
1018 // Compile our final table clause
1019 return implode( ' ', array( $straightJoins, $otherJoins ) );
1020 }
1021
1022 function strencode( $s ) { # Should not be called by us
1023 return str_replace( "'", "''", $s );
1024 }
1025
1026 function addQuotes( $s ) {
1027 if ( $s instanceof Blob ) {
1028 return "'" . $s->fetch( $s ) . "'";
1029 } else {
1030 return parent::addQuotes( $s );
1031 }
1032 }
1033
1034 public function addIdentifierQuotes( $s ) {
1035 // http://msdn.microsoft.com/en-us/library/aa223962.aspx
1036 return '[' . $s . ']';
1037 }
1038
1039 public function isQuotedIdentifier( $name ) {
1040 return $name[0] == '[' && substr( $name, -1, 1 ) == ']';
1041 }
1042
1043 function selectDB( $db ) {
1044 return ( $this->query( "SET DATABASE $db" ) !== false );
1045 }
1046
1047 /**
1048 * @param array $options an associative array of options to be turned into
1049 * an SQL query, valid keys are listed in the function.
1050 * @return array
1051 */
1052 function makeSelectOptions( $options ) {
1053 $tailOpts = '';
1054 $startOpts = '';
1055
1056 $noKeyOptions = array();
1057 foreach ( $options as $key => $option ) {
1058 if ( is_numeric( $key ) ) {
1059 $noKeyOptions[$option] = true;
1060 }
1061 }
1062
1063 $tailOpts .= $this->makeGroupByWithHaving( $options );
1064
1065 $tailOpts .= $this->makeOrderBy( $options );
1066
1067 if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) {
1068 $startOpts .= 'DISTINCT';
1069 }
1070
1071 // we want this to be compatible with the output of parent::makeSelectOptions()
1072 return array( $startOpts, '', $tailOpts, '' );
1073 }
1074
1075 /**
1076 * Get the type of the DBMS, as it appears in $wgDBtype.
1077 * @return string
1078 */
1079 function getType() {
1080 return 'mssql';
1081 }
1082
1083 function buildConcat( $stringList ) {
1084 return implode( ' + ', $stringList );
1085 }
1086
1087 public function getSearchEngine() {
1088 return "SearchMssql";
1089 }
1090
1091 /**
1092 * Since MSSQL doesn't recognize the infinity keyword, set date manually.
1093 * @todo Remove magic date
1094 * @return string
1095 */
1096 public function getInfinity() {
1097 return '3000-01-31 00:00:00.000';
1098 }
1099 } // end DatabaseMssql class
1100
1101 /**
1102 * Utility class.
1103 *
1104 * @ingroup Database
1105 */
1106 class MssqlField implements Field {
1107 private $name, $tableName, $default, $max_length, $nullable, $type;
1108
1109 function __construct( $info ) {
1110 $this->name = $info['COLUMN_NAME'];
1111 $this->tableName = $info['TABLE_NAME'];
1112 $this->default = $info['COLUMN_DEFAULT'];
1113 $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
1114 $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' );
1115 $this->type = $info['DATA_TYPE'];
1116 }
1117
1118 function name() {
1119 return $this->name;
1120 }
1121
1122 function tableName() {
1123 return $this->tableName;
1124 }
1125
1126 function defaultValue() {
1127 return $this->default;
1128 }
1129
1130 function maxLength() {
1131 return $this->max_length;
1132 }
1133
1134 function isNullable() {
1135 return $this->nullable;
1136 }
1137
1138 function type() {
1139 return $this->type;
1140 }
1141 }
1142
1143 /**
1144 * The MSSQL PHP driver doesn't support sqlsrv_num_rows, so we recall all rows
1145 * into an array and maintain our own cursor index into that array... This is
1146 * similar to the way the Oracle driver handles this same issue
1147 *
1148 * @ingroup Database
1149 */
1150 class MssqlResult {
1151 /** @var int */
1152 private $mCursor;
1153
1154 /** @var array */
1155 private $mRows;
1156
1157 /** @var bool|int */
1158 private $mNumFields;
1159
1160 /** @var array|bool */
1161 private $mFieldMeta;
1162
1163 /**
1164 * @param bool|resource $queryresult
1165 */
1166 public function __construct( $queryresult = false ) {
1167 $this->mCursor = 0;
1168 $this->mRows = array();
1169 $this->mNumFields = sqlsrv_num_fields( $queryresult );
1170 $this->mFieldMeta = sqlsrv_field_metadata( $queryresult );
1171
1172 $rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC );
1173
1174 foreach ( $rows as $row ) {
1175 if ( $row !== null ) {
1176 foreach ( $row as $k => $v ) {
1177 if ( is_object( $v ) && method_exists( $v, 'format' ) ) { // DateTime Object
1178 $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" );
1179 }
1180 }
1181 $this->mRows[] = $row; // read results into memory, cursors are not supported
1182 }
1183 }
1184 $this->mRowCount = count( $this->mRows );
1185 sqlsrv_free_stmt( $queryresult );
1186 }
1187
1188 /**
1189 * @param array $array
1190 * @param stdClass $obj
1191 * @return stdClass
1192 */
1193 private function array_to_obj( $array, &$obj ) {
1194 foreach ( $array as $key => $value ) {
1195 if ( is_array( $value ) ) {
1196 $obj->$key = new stdClass();
1197 $this->array_to_obj( $value, $obj->$key );
1198 } else {
1199 if ( !empty( $key ) ) {
1200 $obj->$key = $value;
1201 }
1202 }
1203 }
1204
1205 return $obj;
1206 }
1207
1208 public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) {
1209 if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) {
1210 return false;
1211 }
1212 $arrNum = array();
1213 if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) {
1214 foreach ( $this->mRows[$this->mCursor] as $value ) {
1215 $arrNum[] = $value;
1216 }
1217 }
1218 switch ( $mode ) {
1219 case SQLSRV_FETCH_ASSOC:
1220 $ret = $this->mRows[$this->mCursor];
1221 break;
1222 case SQLSRV_FETCH_NUMERIC:
1223 $ret = $arrNum;
1224 break;
1225 case 'OBJECT':
1226 $o = new $object_class;
1227 $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o );
1228 break;
1229 case SQLSRV_FETCH_BOTH:
1230 default:
1231 $ret = $this->mRows[$this->mCursor] + $arrNum;
1232 break;
1233 }
1234
1235 $this->mCursor++;
1236
1237 return $ret;
1238 }
1239
1240 public function get( $pos, $fld ) {
1241 return $this->mRows[$pos][$fld];
1242 }
1243
1244 public function numrows() {
1245 return $this->mRowCount;
1246 }
1247
1248 public function seek( $iRow ) {
1249 $this->mCursor = min( $iRow, $this->mRowCount );
1250 }
1251
1252 public function numfields() {
1253 return $this->mNumFields;
1254 }
1255
1256 public function fieldname( $nr ) {
1257 $arrKeys = array_keys( $this->mRows[0] );
1258
1259 return $arrKeys[$nr];
1260 }
1261
1262 public function fieldtype( $nr ) {
1263 $i = 0;
1264 $intType = -1;
1265 foreach ( $this->mFieldMeta as $meta ) {
1266 if ( $nr == $i ) {
1267 $intType = $meta['Type'];
1268 break;
1269 }
1270 $i++;
1271 }
1272 // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
1273 switch ( $intType ) {
1274 case SQLSRV_SQLTYPE_BIGINT:
1275 $strType = 'bigint';
1276 break;
1277 case SQLSRV_SQLTYPE_BINARY:
1278 $strType = 'binary';
1279 break;
1280 case SQLSRV_SQLTYPE_BIT:
1281 $strType = 'bit';
1282 break;
1283 case SQLSRV_SQLTYPE_CHAR:
1284 $strType = 'char';
1285 break;
1286 case SQLSRV_SQLTYPE_DATETIME:
1287 $strType = 'datetime';
1288 break;
1289 case SQLSRV_SQLTYPE_DECIMAL: // ($precision, $scale)
1290 $strType = 'decimal';
1291 break;
1292 case SQLSRV_SQLTYPE_FLOAT:
1293 $strType = 'float';
1294 break;
1295 case SQLSRV_SQLTYPE_IMAGE:
1296 $strType = 'image';
1297 break;
1298 case SQLSRV_SQLTYPE_INT:
1299 $strType = 'int';
1300 break;
1301 case SQLSRV_SQLTYPE_MONEY:
1302 $strType = 'money';
1303 break;
1304 case SQLSRV_SQLTYPE_NCHAR: // ($charCount):
1305 $strType = 'nchar';
1306 break;
1307 case SQLSRV_SQLTYPE_NUMERIC: // ($precision, $scale):
1308 $strType = 'numeric';
1309 break;
1310 case SQLSRV_SQLTYPE_NVARCHAR: // ($charCount)
1311 $strType = 'nvarchar';
1312 break;
1313 // case SQLSRV_SQLTYPE_NVARCHAR('max'):
1314 // $strType = 'nvarchar(MAX)';
1315 // break;
1316 case SQLSRV_SQLTYPE_NTEXT:
1317 $strType = 'ntext';
1318 break;
1319 case SQLSRV_SQLTYPE_REAL:
1320 $strType = 'real';
1321 break;
1322 case SQLSRV_SQLTYPE_SMALLDATETIME:
1323 $strType = 'smalldatetime';
1324 break;
1325 case SQLSRV_SQLTYPE_SMALLINT:
1326 $strType = 'smallint';
1327 break;
1328 case SQLSRV_SQLTYPE_SMALLMONEY:
1329 $strType = 'smallmoney';
1330 break;
1331 case SQLSRV_SQLTYPE_TEXT:
1332 $strType = 'text';
1333 break;
1334 case SQLSRV_SQLTYPE_TIMESTAMP:
1335 $strType = 'timestamp';
1336 break;
1337 case SQLSRV_SQLTYPE_TINYINT:
1338 $strType = 'tinyint';
1339 break;
1340 case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER:
1341 $strType = 'uniqueidentifier';
1342 break;
1343 case SQLSRV_SQLTYPE_UDT:
1344 $strType = 'UDT';
1345 break;
1346 case SQLSRV_SQLTYPE_VARBINARY: // ($byteCount)
1347 $strType = 'varbinary';
1348 break;
1349 // case SQLSRV_SQLTYPE_VARBINARY('max'):
1350 // $strType = 'varbinary(MAX)';
1351 // break;
1352 case SQLSRV_SQLTYPE_VARCHAR: // ($charCount)
1353 $strType = 'varchar';
1354 break;
1355 // case SQLSRV_SQLTYPE_VARCHAR('max'):
1356 // $strType = 'varchar(MAX)';
1357 // break;
1358 case SQLSRV_SQLTYPE_XML:
1359 $strType = 'xml';
1360 break;
1361 default:
1362 $strType = $intType;
1363 }
1364
1365 return $strType;
1366 }
1367
1368 public function free() {
1369 unset( $this->mRows );
1370 }
1371 }