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