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