MediaWiki REL1_31
DatabaseMssql.php
Go to the documentation of this file.
1<?php
28namespace Wikimedia\Rdbms;
29
30use Wikimedia;
31use Exception;
32use stdClass;
33
37class DatabaseMssql extends Database {
39 protected $serverPort;
41 protected $useWindowsAuth = false;
43 protected $lastInsertId = null;
45 protected $lastAffectedRowCount = null;
47 protected $subqueryId = 0;
49 protected $scrollableCursor = true;
51 protected $prepareStatements = true;
53 protected $binaryColumnCache = null;
55 protected $bitColumnCache = null;
57 protected $ignoreDupKeyErrors = false;
59 protected $ignoreErrors = [];
60
61 public function implicitGroupby() {
62 return false;
63 }
64
65 public function implicitOrderby() {
66 return false;
67 }
68
69 public function unionSupportsOrderAndLimit() {
70 return false;
71 }
72
73 public function __construct( array $params ) {
74 $this->serverPort = $params['port'];
75 $this->useWindowsAuth = $params['UseWindowsAuth'];
76
77 parent::__construct( $params );
78 }
79
89 public function open( $server, $user, $password, $dbName ) {
90 # Test for driver support, to avoid suppressed fatal error
91 if ( !function_exists( 'sqlsrv_connect' ) ) {
92 throw new DBConnectionError(
93 $this,
94 "Microsoft SQL Server Native (sqlsrv) functions missing.
95 You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n"
96 );
97 }
98
99 # e.g. the class is being loaded
100 if ( !strlen( $user ) ) {
101 return null;
102 }
103
104 $this->close();
105 $this->server = $server;
106 $this->user = $user;
107 $this->password = $password;
108 $this->dbName = $dbName;
109
110 $connectionInfo = [];
111
112 if ( $dbName ) {
113 $connectionInfo['Database'] = $dbName;
114 }
115
116 // Decide which auth scenerio to use
117 // if we are using Windows auth, then don't add credentials to $connectionInfo
118 if ( !$this->useWindowsAuth ) {
119 $connectionInfo['UID'] = $user;
120 $connectionInfo['PWD'] = $password;
121 }
122
123 Wikimedia\suppressWarnings();
124 $this->conn = sqlsrv_connect( $server, $connectionInfo );
125 Wikimedia\restoreWarnings();
126
127 if ( $this->conn === false ) {
128 throw new DBConnectionError( $this, $this->lastError() );
129 }
130
131 $this->opened = true;
132
133 return $this->conn;
134 }
135
141 protected function closeConnection() {
142 return sqlsrv_close( $this->conn );
143 }
144
149 protected function resultObject( $result ) {
150 if ( !$result ) {
151 return false;
152 } elseif ( $result instanceof MssqlResultWrapper ) {
153 return $result;
154 } elseif ( $result === true ) {
155 // Successful write query
156 return $result;
157 } else {
158 return new MssqlResultWrapper( $this, $result );
159 }
160 }
161
167 protected function doQuery( $sql ) {
168 // several extensions seem to think that all databases support limits
169 // via LIMIT N after the WHERE clause, but MSSQL uses SELECT TOP N,
170 // so to catch any of those extensions we'll do a quick check for a
171 // LIMIT clause and pass $sql through $this->LimitToTopN() which parses
172 // the LIMIT clause and passes the result to $this->limitResult();
173 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
174 // massage LIMIT -> TopN
175 $sql = $this->LimitToTopN( $sql );
176 }
177
178 // MSSQL doesn't have EXTRACT(epoch FROM XXX)
179 if ( preg_match( '#\bEXTRACT\s*?\‍(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
180 // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
181 $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
182 }
183
184 // perform query
185
186 // SQLSRV_CURSOR_STATIC is slower than SQLSRV_CURSOR_CLIENT_BUFFERED (one of the two is
187 // needed if we want to be able to seek around the result set), however CLIENT_BUFFERED
188 // has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty
189 // strings make php throw a fatal error "Severe error translating Unicode"
190 if ( $this->scrollableCursor ) {
191 $scrollArr = [ 'Scrollable' => SQLSRV_CURSOR_STATIC ];
192 } else {
193 $scrollArr = [];
194 }
195
196 if ( $this->prepareStatements ) {
197 // we do prepare + execute so we can get its field metadata for later usage if desired
198 $stmt = sqlsrv_prepare( $this->conn, $sql, [], $scrollArr );
199 $success = sqlsrv_execute( $stmt );
200 } else {
201 $stmt = sqlsrv_query( $this->conn, $sql, [], $scrollArr );
202 $success = (bool)$stmt;
203 }
204
205 // Make a copy to ensure what we add below does not get reflected in future queries
207
208 if ( $this->ignoreDupKeyErrors ) {
209 // ignore duplicate key errors
210 // this emulates INSERT IGNORE in MySQL
211 $ignoreErrors[] = '2601'; // duplicate key error caused by unique index
212 $ignoreErrors[] = '2627'; // duplicate key error caused by primary key
213 $ignoreErrors[] = '3621'; // generic "the statement has been terminated" error
214 }
215
216 if ( $success === false ) {
217 $errors = sqlsrv_errors();
218 $success = true;
219
220 foreach ( $errors as $err ) {
221 if ( !in_array( $err['code'], $ignoreErrors ) ) {
222 $success = false;
223 break;
224 }
225 }
226
227 if ( $success === false ) {
228 return false;
229 }
230 }
231 // remember number of rows affected
232 $this->lastAffectedRowCount = sqlsrv_rows_affected( $stmt );
233
234 return $stmt;
235 }
236
237 public function freeResult( $res ) {
238 if ( $res instanceof ResultWrapper ) {
239 $res = $res->result;
240 }
241
242 sqlsrv_free_stmt( $res );
243 }
244
249 public function fetchObject( $res ) {
250 // $res is expected to be an instance of MssqlResultWrapper here
251 return $res->fetchObject();
252 }
253
258 public function fetchRow( $res ) {
259 return $res->fetchRow();
260 }
261
266 public function numRows( $res ) {
267 if ( $res instanceof ResultWrapper ) {
268 $res = $res->result;
269 }
270
271 $ret = sqlsrv_num_rows( $res );
272
273 if ( $ret === false ) {
274 // we cannot get an amount of rows from this cursor type
275 // has_rows returns bool true/false if the result has rows
276 $ret = (int)sqlsrv_has_rows( $res );
277 }
278
279 return $ret;
280 }
281
286 public function numFields( $res ) {
287 if ( $res instanceof ResultWrapper ) {
288 $res = $res->result;
289 }
290
291 return sqlsrv_num_fields( $res );
292 }
293
299 public function fieldName( $res, $n ) {
300 if ( $res instanceof ResultWrapper ) {
301 $res = $res->result;
302 }
303
304 return sqlsrv_field_metadata( $res )[$n]['Name'];
305 }
306
311 public function insertId() {
312 return $this->lastInsertId;
313 }
314
320 public function dataSeek( $res, $row ) {
321 return $res->seek( $row );
322 }
323
327 public function lastError() {
328 $strRet = '';
329 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
330 if ( $retErrors != null ) {
331 foreach ( $retErrors as $arrError ) {
332 $strRet .= $this->formatError( $arrError ) . "\n";
333 }
334 } else {
335 $strRet = "No errors found";
336 }
337
338 return $strRet;
339 }
340
345 private function formatError( $err ) {
346 return '[SQLSTATE ' .
347 $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message'];
348 }
349
353 public function lastErrno() {
354 $err = sqlsrv_errors( SQLSRV_ERR_ALL );
355 if ( $err !== null && isset( $err[0] ) ) {
356 return $err[0]['code'];
357 } else {
358 return 0;
359 }
360 }
361
362 protected function wasKnownStatementRollbackError() {
363 $errors = sqlsrv_errors( SQLSRV_ERR_ALL );
364 if ( !$errors ) {
365 return false;
366 }
367 // The transaction vs statement rollback behavior depends on XACT_ABORT, so make sure
368 // that the "statement has been terminated" error (3621) is specifically present.
369 // https://docs.microsoft.com/en-us/sql/t-sql/statements/set-xact-abort-transact-sql
370 $statementOnly = false;
371 $codeWhitelist = [ '2601', '2627', '547' ];
372 foreach ( $errors as $error ) {
373 if ( $error['code'] == '3621' ) {
374 $statementOnly = true;
375 } elseif ( !in_array( $error['code'], $codeWhitelist ) ) {
376 $statementOnly = false;
377 break;
378 }
379 }
380
381 return $statementOnly;
382 }
383
387 protected function fetchAffectedRowCount() {
389 }
390
409 public function select( $table, $vars, $conds = '', $fname = __METHOD__,
410 $options = [], $join_conds = []
411 ) {
412 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
413 if ( isset( $options['EXPLAIN'] ) ) {
414 try {
415 $this->scrollableCursor = false;
416 $this->prepareStatements = false;
417 $this->query( "SET SHOWPLAN_ALL ON" );
418 $ret = $this->query( $sql, $fname );
419 $this->query( "SET SHOWPLAN_ALL OFF" );
420 } catch ( DBQueryError $dqe ) {
421 if ( isset( $options['FOR COUNT'] ) ) {
422 // likely don't have privs for SHOWPLAN, so run a select count instead
423 $this->query( "SET SHOWPLAN_ALL OFF" );
424 unset( $options['EXPLAIN'] );
425 $ret = $this->select(
426 $table,
427 'COUNT(*) AS EstimateRows',
428 $conds,
429 $fname,
430 $options,
431 $join_conds
432 );
433 } else {
434 // someone actually wanted the query plan instead of an est row count
435 // let them know of the error
436 $this->scrollableCursor = true;
437 $this->prepareStatements = true;
438 throw $dqe;
439 }
440 }
441 $this->scrollableCursor = true;
442 $this->prepareStatements = true;
443 return $ret;
444 }
445 return $this->query( $sql, $fname );
446 }
447
461 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
462 $options = [], $join_conds = []
463 ) {
464 if ( isset( $options['EXPLAIN'] ) ) {
465 unset( $options['EXPLAIN'] );
466 }
467
468 $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
469
470 // try to rewrite aggregations of bit columns (currently MAX and MIN)
471 if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) {
472 $bitColumns = [];
473 if ( is_array( $table ) ) {
474 $tables = $table;
475 while ( $tables ) {
476 $t = array_pop( $tables );
477 if ( is_array( $t ) ) {
478 $tables = array_merge( $tables, $t );
479 } else {
480 $bitColumns += $this->getBitColumns( $this->tableName( $t ) );
481 }
482 }
483 } else {
484 $bitColumns = $this->getBitColumns( $this->tableName( $table ) );
485 }
486
487 foreach ( $bitColumns as $col => $info ) {
488 $replace = [
489 "MAX({$col})" => "MAX(CAST({$col} AS tinyint))",
490 "MIN({$col})" => "MIN(CAST({$col} AS tinyint))",
491 ];
492 $sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql );
493 }
494 }
495
496 return $sql;
497 }
498
499 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
500 $fname = __METHOD__
501 ) {
502 $this->scrollableCursor = false;
503 try {
504 parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname );
505 } catch ( Exception $e ) {
506 $this->scrollableCursor = true;
507 throw $e;
508 }
509 $this->scrollableCursor = true;
510 }
511
512 public function delete( $table, $conds, $fname = __METHOD__ ) {
513 $this->scrollableCursor = false;
514 try {
515 parent::delete( $table, $conds, $fname );
516 } catch ( Exception $e ) {
517 $this->scrollableCursor = true;
518 throw $e;
519 }
520 $this->scrollableCursor = true;
521 }
522
537 public function estimateRowCount( $table, $var = '*', $conds = '',
538 $fname = __METHOD__, $options = [], $join_conds = []
539 ) {
540 $conds = $this->normalizeConditions( $conds, $fname );
541 $column = $this->extractSingleFieldFromList( $var );
542 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
543 $conds[] = "$column IS NOT NULL";
544 }
545
546 // http://msdn2.microsoft.com/en-us/library/aa259203.aspx
547 $options['EXPLAIN'] = true;
548 $options['FOR COUNT'] = true;
549 $res = $this->select( $table, $var, $conds, $fname, $options, $join_conds );
550
551 $rows = -1;
552 if ( $res ) {
553 $row = $this->fetchRow( $res );
554
555 if ( isset( $row['EstimateRows'] ) ) {
556 $rows = (int)$row['EstimateRows'];
557 }
558 }
559
560 return $rows;
561 }
562
571 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
572 # This does not return the same info as MYSQL would, but that's OK
573 # because MediaWiki never uses the returned value except to check for
574 # the existence of indexes.
575 $sql = "sp_helpindex '" . $this->tableName( $table ) . "'";
576 $res = $this->query( $sql, $fname );
577
578 if ( !$res ) {
579 return null;
580 }
581
582 $result = [];
583 foreach ( $res as $row ) {
584 if ( $row->index_name == $index ) {
585 $row->Non_unique = !stristr( $row->index_description, "unique" );
586 $cols = explode( ", ", $row->index_keys );
587 foreach ( $cols as $col ) {
588 $row->Column_name = trim( $col );
589 $result[] = clone $row;
590 }
591 } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
592 $row->Non_unique = 0;
593 $cols = explode( ", ", $row->index_keys );
594 foreach ( $cols as $col ) {
595 $row->Column_name = trim( $col );
596 $result[] = clone $row;
597 }
598 }
599 }
600
601 return $result ?: false;
602 }
603
619 public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = [] ) {
620 # No rows to insert, easy just return now
621 if ( !count( $arrToInsert ) ) {
622 return true;
623 }
624
625 if ( !is_array( $options ) ) {
626 $options = [ $options ];
627 }
628
629 $table = $this->tableName( $table );
630
631 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row
632 $arrToInsert = [ 0 => $arrToInsert ]; // make everything multi row compatible
633 }
634
635 // We know the table we're inserting into, get its identity column
636 $identity = null;
637 // strip matching square brackets and the db/schema from table name
638 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
639 $tableRaw = array_pop( $tableRawArr );
640 $res = $this->doQuery(
641 "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " .
642 "WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'"
643 );
644 if ( $res && sqlsrv_has_rows( $res ) ) {
645 // There is an identity for this table.
646 $identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC );
647 $identity = array_pop( $identityArr );
648 }
649 sqlsrv_free_stmt( $res );
650
651 // Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF
652 $binaryColumns = $this->getBinaryColumns( $table );
653
654 // INSERT IGNORE is not supported by SQL Server
655 // remove IGNORE from options list and set ignore flag to true
656 if ( in_array( 'IGNORE', $options ) ) {
657 $options = array_diff( $options, [ 'IGNORE' ] );
658 $this->ignoreDupKeyErrors = true;
659 }
660
661 $ret = null;
662 foreach ( $arrToInsert as $a ) {
663 // start out with empty identity column, this is so we can return
664 // it as a result of the INSERT logic
665 $sqlPre = '';
666 $sqlPost = '';
667 $identityClause = '';
668
669 // if we have an identity column
670 if ( $identity ) {
671 // iterate through
672 foreach ( $a as $k => $v ) {
673 if ( $k == $identity ) {
674 if ( !is_null( $v ) ) {
675 // there is a value being passed to us,
676 // we need to turn on and off inserted identity
677 $sqlPre = "SET IDENTITY_INSERT $table ON;";
678 $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
679 } else {
680 // we can't insert NULL into an identity column,
681 // so remove the column from the insert.
682 unset( $a[$k] );
683 }
684 }
685 }
686
687 // we want to output an identity column as result
688 $identityClause = "OUTPUT INSERTED.$identity ";
689 }
690
691 $keys = array_keys( $a );
692
693 // Build the actual query
694 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
695 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
696
697 $first = true;
698 foreach ( $a as $key => $value ) {
699 if ( isset( $binaryColumns[$key] ) ) {
700 $value = new MssqlBlob( $value );
701 }
702 if ( $first ) {
703 $first = false;
704 } else {
705 $sql .= ',';
706 }
707 if ( is_null( $value ) ) {
708 $sql .= 'null';
709 } elseif ( is_array( $value ) || is_object( $value ) ) {
710 if ( is_object( $value ) && $value instanceof Blob ) {
711 $sql .= $this->addQuotes( $value );
712 } else {
713 $sql .= $this->addQuotes( serialize( $value ) );
714 }
715 } else {
716 $sql .= $this->addQuotes( $value );
717 }
718 }
719 $sql .= ')' . $sqlPost;
720
721 // Run the query
722 $this->scrollableCursor = false;
723 try {
724 $ret = $this->query( $sql );
725 } catch ( Exception $e ) {
726 $this->scrollableCursor = true;
727 $this->ignoreDupKeyErrors = false;
728 throw $e;
729 }
730 $this->scrollableCursor = true;
731
732 if ( $ret instanceof ResultWrapper && !is_null( $identity ) ) {
733 // Then we want to get the identity column value we were assigned and save it off
734 $row = $ret->fetchObject();
735 if ( is_object( $row ) ) {
736 $this->lastInsertId = $row->$identity;
737 // It seems that mAffectedRows is -1 sometimes when OUTPUT INSERTED.identity is
738 // used if we got an identity back, we know for sure a row was affected, so
739 // adjust that here
740 if ( $this->lastAffectedRowCount == -1 ) {
741 $this->lastAffectedRowCount = 1;
742 }
743 }
744 }
745 }
746
747 $this->ignoreDupKeyErrors = false;
748
749 return $ret;
750 }
751
768 public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
769 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
770 ) {
771 $this->scrollableCursor = false;
772 try {
773 $ret = parent::nativeInsertSelect(
774 $destTable,
775 $srcTable,
776 $varMap,
777 $conds,
778 $fname,
779 $insertOptions,
780 $selectOptions,
781 $selectJoinConds
782 );
783 } catch ( Exception $e ) {
784 $this->scrollableCursor = true;
785 throw $e;
786 }
787 $this->scrollableCursor = true;
788
789 return $ret;
790 }
791
817 function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
818 $table = $this->tableName( $table );
819 $binaryColumns = $this->getBinaryColumns( $table );
820
821 $opts = $this->makeUpdateOptions( $options );
822 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns );
823
824 if ( $conds !== [] && $conds !== '*' ) {
825 $sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns );
826 }
827
828 $this->scrollableCursor = false;
829 try {
830 $this->query( $sql );
831 } catch ( Exception $e ) {
832 $this->scrollableCursor = true;
833 throw $e;
834 }
835 $this->scrollableCursor = true;
836 return true;
837 }
838
855 public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = [] ) {
856 if ( !is_array( $a ) ) {
857 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
858 }
859
860 if ( $mode != LIST_NAMES ) {
861 // In MS SQL, values need to be specially encoded when they are
862 // inserted into binary fields. Perform this necessary encoding
863 // for the specified set of columns.
864 foreach ( array_keys( $a ) as $field ) {
865 if ( !isset( $binaryColumns[$field] ) ) {
866 continue;
867 }
868
869 if ( is_array( $a[$field] ) ) {
870 foreach ( $a[$field] as &$v ) {
871 $v = new MssqlBlob( $v );
872 }
873 unset( $v );
874 } else {
875 $a[$field] = new MssqlBlob( $a[$field] );
876 }
877 }
878 }
879
880 return parent::makeList( $a, $mode );
881 }
882
888 public function textFieldSize( $table, $field ) {
889 $table = $this->tableName( $table );
890 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
891 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
892 $res = $this->query( $sql );
893 $row = $this->fetchRow( $res );
894 $size = -1;
895 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) {
896 $size = $row['CHARACTER_MAXIMUM_LENGTH'];
897 }
898
899 return $size;
900 }
901
912 public function limitResult( $sql, $limit, $offset = false ) {
913 if ( $offset === false || $offset == 0 ) {
914 if ( strpos( $sql, "SELECT" ) === false ) {
915 return "TOP {$limit} " . $sql;
916 } else {
917 return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi',
918 'SELECT$1 TOP ' . $limit, $sql, 1 );
919 }
920 } else {
921 // This one is fun, we need to pull out the select list as well as any ORDER BY clause
922 $select = $orderby = [];
923 $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select );
924 $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby );
925 $postOrder = '';
926 $first = $offset + 1;
927 $last = $offset + $limit;
928 $sub1 = 'sub_' . $this->subqueryId;
929 $sub2 = 'sub_' . ( $this->subqueryId + 1 );
930 $this->subqueryId += 2;
931 if ( !$s1 ) {
932 // wat
933 throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" );
934 }
935 if ( !$s2 ) {
936 // no ORDER BY
937 $overOrder = 'ORDER BY (SELECT 1)';
938 } else {
939 if ( !isset( $orderby[2] ) || !$orderby[2] ) {
940 // don't need to strip it out if we're using a FOR XML clause
941 $sql = str_replace( $orderby[1], '', $sql );
942 }
943 $overOrder = $orderby[1];
944 $postOrder = ' ' . $overOrder;
945 }
946 $sql = "SELECT {$select[1]}
947 FROM (
948 SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, *
949 FROM ({$sql}) {$sub1}
950 ) {$sub2}
951 WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}";
952
953 return $sql;
954 }
955 }
956
967 public function LimitToTopN( $sql ) {
968 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
969 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
970 if ( preg_match( $pattern, $sql, $matches ) ) {
971 $row_count = $matches[4];
972 $offset = $matches[3] ?: $matches[6] ?: false;
973
974 // strip the matching LIMIT clause out
975 $sql = str_replace( $matches[0], '', $sql );
976
977 return $this->limitResult( $sql, $row_count, $offset );
978 }
979
980 return $sql;
981 }
982
986 public function getSoftwareLink() {
987 return "[{{int:version-db-mssql-url}} MS SQL Server]";
988 }
989
993 public function getServerVersion() {
994 $server_info = sqlsrv_server_info( $this->conn );
995 $version = 'Error';
996 if ( isset( $server_info['SQLServerVersion'] ) ) {
997 $version = $server_info['SQLServerVersion'];
998 }
999
1000 return $version;
1001 }
1002
1008 public function tableExists( $table, $fname = __METHOD__ ) {
1009 list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
1010
1011 if ( $db !== false ) {
1012 // remote database
1013 $this->queryLogger->error( "Attempting to call tableExists on a remote table" );
1014 return false;
1015 }
1016
1017 if ( $schema === false ) {
1019 }
1020
1021 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES
1022 WHERE TABLE_TYPE = 'BASE TABLE'
1023 AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" );
1024
1025 if ( $res->numRows() ) {
1026 return true;
1027 } else {
1028 return false;
1029 }
1030 }
1031
1039 public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1040 list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
1041
1042 if ( $db !== false ) {
1043 // remote database
1044 $this->queryLogger->error( "Attempting to call fieldExists on a remote table" );
1045 return false;
1046 }
1047
1048 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
1049 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
1050
1051 if ( $res->numRows() ) {
1052 return true;
1053 } else {
1054 return false;
1055 }
1056 }
1057
1058 public function fieldInfo( $table, $field ) {
1059 list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
1060
1061 if ( $db !== false ) {
1062 // remote database
1063 $this->queryLogger->error( "Attempting to call fieldInfo on a remote table" );
1064 return false;
1065 }
1066
1067 $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS
1068 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
1069
1070 $meta = $res->fetchRow();
1071 if ( $meta ) {
1072 return new MssqlField( $meta );
1073 }
1074
1075 return false;
1076 }
1077
1078 protected function doSavepoint( $identifier, $fname ) {
1079 $this->query( 'SAVE TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname );
1080 }
1081
1082 protected function doReleaseSavepoint( $identifier, $fname ) {
1083 // Not supported. Also not really needed, a new doSavepoint() for the
1084 // same identifier will overwrite the old.
1085 }
1086
1087 protected function doRollbackToSavepoint( $identifier, $fname ) {
1088 $this->query( 'ROLLBACK TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname );
1089 }
1090
1095 protected function doBegin( $fname = __METHOD__ ) {
1096 sqlsrv_begin_transaction( $this->conn );
1097 $this->trxLevel = 1;
1098 }
1099
1104 protected function doCommit( $fname = __METHOD__ ) {
1105 sqlsrv_commit( $this->conn );
1106 $this->trxLevel = 0;
1107 }
1108
1114 protected function doRollback( $fname = __METHOD__ ) {
1115 sqlsrv_rollback( $this->conn );
1116 $this->trxLevel = 0;
1117 }
1118
1123 public function strencode( $s ) {
1124 // Should not be called by us
1125 return str_replace( "'", "''", $s );
1126 }
1127
1132 public function addQuotes( $s ) {
1133 if ( $s instanceof MssqlBlob ) {
1134 return $s->fetch();
1135 } elseif ( $s instanceof Blob ) {
1136 // this shouldn't really ever be called, but it's here if needed
1137 // (and will quite possibly make the SQL error out)
1138 $blob = new MssqlBlob( $s->fetch() );
1139 return $blob->fetch();
1140 } else {
1141 if ( is_bool( $s ) ) {
1142 $s = $s ? 1 : 0;
1143 }
1144 return parent::addQuotes( $s );
1145 }
1146 }
1147
1152 public function addIdentifierQuotes( $s ) {
1153 // http://msdn.microsoft.com/en-us/library/aa223962.aspx
1154 return '[' . $s . ']';
1155 }
1156
1161 public function isQuotedIdentifier( $name ) {
1162 return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']';
1163 }
1164
1172 protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
1173 return str_replace( [ $escapeChar, '%', '_', '[', ']', '^' ],
1174 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_",
1175 "{$escapeChar}[", "{$escapeChar}]", "{$escapeChar}^" ],
1176 $s );
1177 }
1178
1183 public function selectDB( $db ) {
1184 try {
1185 $this->dbName = $db;
1186 $this->query( "USE $db" );
1187 return true;
1188 } catch ( Exception $e ) {
1189 return false;
1190 }
1191 }
1192
1198 public function makeSelectOptions( $options ) {
1199 $tailOpts = '';
1200 $startOpts = '';
1201
1202 $noKeyOptions = [];
1203 foreach ( $options as $key => $option ) {
1204 if ( is_numeric( $key ) ) {
1205 $noKeyOptions[$option] = true;
1206 }
1207 }
1208
1209 $tailOpts .= $this->makeGroupByWithHaving( $options );
1210
1211 $tailOpts .= $this->makeOrderBy( $options );
1212
1213 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1214 $startOpts .= 'DISTINCT';
1215 }
1216
1217 if ( isset( $noKeyOptions['FOR XML'] ) ) {
1218 // used in group concat field emulation
1219 $tailOpts .= " FOR XML PATH('')";
1220 }
1221
1222 // we want this to be compatible with the output of parent::makeSelectOptions()
1223 return [ $startOpts, '', $tailOpts, '', '' ];
1224 }
1225
1226 public function getType() {
1227 return 'mssql';
1228 }
1229
1234 public function buildConcat( $stringList ) {
1235 return implode( ' + ', $stringList );
1236 }
1237
1255 public function buildGroupConcatField( $delim, $table, $field, $conds = '',
1256 $join_conds = []
1257 ) {
1258 $gcsq = 'gcsq_' . $this->subqueryId;
1259 $this->subqueryId++;
1260
1261 $delimLen = strlen( $delim );
1262 $fld = "{$field} + {$this->addQuotes( $delim )}";
1263 $sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM ("
1264 . $this->selectSQLText( $table, $fld, $conds, null, [ 'FOR XML' ], $join_conds )
1265 . ") {$gcsq} ({$field}))";
1266
1267 return $sql;
1268 }
1269
1270 public function buildSubstring( $input, $startPosition, $length = null ) {
1271 $this->assertBuildSubstringParams( $startPosition, $length );
1272 if ( $length === null ) {
1278 $length = 2147483647;
1279 }
1280 return 'SUBSTRING(' . implode( ',', [ $input, $startPosition, $length ] ) . ')';
1281 }
1282
1289 private function getBinaryColumns( $table ) {
1290 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
1291 $tableRaw = array_pop( $tableRawArr );
1292
1293 if ( $this->binaryColumnCache === null ) {
1294 $this->populateColumnCaches();
1295 }
1296
1297 return isset( $this->binaryColumnCache[$tableRaw] )
1298 ? $this->binaryColumnCache[$tableRaw]
1299 : [];
1300 }
1301
1306 private function getBitColumns( $table ) {
1307 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
1308 $tableRaw = array_pop( $tableRawArr );
1309
1310 if ( $this->bitColumnCache === null ) {
1311 $this->populateColumnCaches();
1312 }
1313
1314 return isset( $this->bitColumnCache[$tableRaw] )
1315 ? $this->bitColumnCache[$tableRaw]
1316 : [];
1317 }
1318
1319 private function populateColumnCaches() {
1320 $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
1321 [
1322 'TABLE_CATALOG' => $this->dbName,
1323 'TABLE_SCHEMA' => $this->schema,
1324 'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
1325 ] );
1326
1327 $this->binaryColumnCache = [];
1328 $this->bitColumnCache = [];
1329 foreach ( $res as $row ) {
1330 if ( $row->DATA_TYPE == 'bit' ) {
1331 $this->bitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
1332 } else {
1333 $this->binaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
1334 }
1335 }
1336 }
1337
1343 function tableName( $name, $format = 'quoted' ) {
1344 # Replace reserved words with better ones
1345 switch ( $name ) {
1346 case 'user':
1347 return $this->realTableName( 'mwuser', $format );
1348 default:
1349 return $this->realTableName( $name, $format );
1350 }
1351 }
1352
1359 function realTableName( $name, $format = 'quoted' ) {
1360 $table = parent::tableName( $name, $format );
1361 if ( $format == 'split' ) {
1362 // Used internally, we want the schema split off from the table name and returned
1363 // as a list with 3 elements (database, schema, table)
1364 $table = explode( '.', $table );
1365 while ( count( $table ) < 3 ) {
1366 array_unshift( $table, false );
1367 }
1368 }
1369 return $table;
1370 }
1371
1379 public function dropTable( $tableName, $fName = __METHOD__ ) {
1380 if ( !$this->tableExists( $tableName, $fName ) ) {
1381 return false;
1382 }
1383
1384 // parent function incorrectly appends CASCADE, which we don't want
1385 $sql = "DROP TABLE " . $this->tableName( $tableName );
1386
1387 return $this->query( $sql, $fName );
1388 }
1389
1396 public function prepareStatements( $value = null ) {
1398 if ( $value !== null ) {
1399 $this->prepareStatements = $value;
1400 }
1401
1402 return $old;
1403 }
1404
1411 public function scrollableCursor( $value = null ) {
1413 if ( $value !== null ) {
1414 $this->scrollableCursor = $value;
1415 }
1416
1417 return $old;
1418 }
1419}
1420
1421class_alias( DatabaseMssql::class, 'DatabaseMssql' );
serialize()
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:112
insert( $table, $arrToInsert, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
buildSubstring( $input, $startPosition, $length=null)
escapeLikeInternal( $s, $escapeChar='`')
MS SQL supports more pattern operators than other databases (ex: [,],^)
closeConnection()
Closes a database connection, if it is open Returns success, true if already closed.
doCommit( $fname=__METHOD__)
End a transaction.
fieldInfo( $table, $field)
mysql_fetch_field() wrapper Returns false if the field doesn't exist
doSavepoint( $identifier, $fname)
Create a savepoint.
prepareStatements( $value=null)
Called in the installer and updater.
insertId()
This must be called after nextSequenceVal.
implicitOrderby()
Returns true if this database does an implicit order by when the column has an index For example: SEL...
estimateRowCount( $table, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Estimate rows in dataset Returns estimated count, based on SHOWPLAN_ALL output This is not necessaril...
open( $server, $user, $password, $dbName)
Usually aborts on failure.
LimitToTopN( $sql)
If there is a limit clause, parse it, strip it, and pass the remaining SQL through limitResult() with...
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
SELECT wrapper.
realTableName( $name, $format='quoted')
call this instead of tableName() in the updater when renaming tables
buildGroupConcatField( $delim, $table, $field, $conds='', $join_conds=[])
Build a GROUP_CONCAT or equivalent statement for a query.
nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname=__METHOD__, $insertOptions=[], $selectOptions=[], $selectJoinConds=[])
INSERT SELECT wrapper $varMap must be an associative array of the form [ 'dest1' => 'source1',...
limitResult( $sql, $limit, $offset=false)
Construct a LIMIT query with optional offset This is used for query pages.
dropTable( $tableName, $fName=__METHOD__)
Delete a table.
doRollbackToSavepoint( $identifier, $fname)
Rollback to a savepoint.
freeResult( $res)
Free a result object returned by query() or select().
fieldExists( $table, $field, $fname=__METHOD__)
Query whether a given column exists in the mediawiki schema.
makeList( $a, $mode=LIST_COMMA, $binaryColumns=[])
Makes an encoded list of strings from an array.
indexInfo( $table, $index, $fname=__METHOD__)
Returns information about an index If errors are explicitly ignored, returns NULL on failure.
tableName( $name, $format='quoted')
scrollableCursor( $value=null)
Called in the installer and updater.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
SELECT wrapper.
doRollback( $fname=__METHOD__)
Rollback a transaction.
doBegin( $fname=__METHOD__)
Begin a transaction, committing any previously open transaction.
stdClass[][] null $bitColumnCache
unionSupportsOrderAndLimit()
Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries within th...
tableExists( $table, $fname=__METHOD__)
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
implicitGroupby()
Returns true if this database does an implicit sort when doing GROUP BY.
doReleaseSavepoint( $identifier, $fname)
Release a savepoint.
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
stdClass[][] null $binaryColumnCache
getType()
Get the type of the DBMS, as it appears in $wgDBtype.
getBinaryColumns( $table)
Returns an associative array for fields that are of type varbinary, binary, or image $table can be ei...
Relational database abstraction object.
Definition Database.php:48
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
string $user
User that this instance is currently connected under the name of.
Definition Database.php:81
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
resource null $conn
Database connection.
Definition Database.php:108
trxLevel()
Gets the current transaction level.
Definition Database.php:577
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
string $password
Password used to establish the current connection.
Definition Database.php:83
string $server
Server that this instance is currently connected to.
Definition Database.php:79
normalizeConditions( $conds, $fname)
makeOrderBy( $options)
Returns an optional ORDER BY.
string $dbName
Database that this instance is currently connected to.
Definition Database.php:85
close()
Close the database connection.
Definition Database.php:900
Result wrapper for grabbing data queried from an IDatabase object.
We use the convention $dbr for read and $dbw for write to help you keep track of whether the database object is a the world will explode Or to be a subsequent write query which succeeded on the master may fail when replicated to the slave due to a unique key collision Replication on the slave will stop and it may take hours to repair the database and get it back online Setting read_only in my cnf on the slave will avoid this but given the dire we prefer to have as many checks as possible We provide a but the wrapper functions like select() and insert() are usually more convenient. They take care of things like table prefixes and escaping for you. If you really need to make your own SQL
$res
Definition database.txt:21
For a write query
Definition database.txt:26
We use the convention $dbr for read and $dbw for write to help you keep track of whether the database object is a the world will explode Or to be a subsequent write query which succeeded on the master may fail when replicated to the slave due to a unique key collision Replication on the slave will stop and it may take hours to repair the database and get it back online Setting read_only in my cnf on the slave will avoid this but given the dire we prefer to have as many checks as possible We provide a but the wrapper functions like please read the documentation for tableName() and addQuotes(). You will need both of them. ------------------------------------------------------------------------ Basic query optimisation ------------------------------------------------------------------------ MediaWiki developers who need to write DB queries should have some understanding of databases and the performance issues associated with them. Patches containing unacceptably slow features will not be accepted. Unindexed queries are generally not welcome in MediaWiki
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
Definition hooks.txt:2228
namespace being checked & $result
Definition hooks.txt:2323
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition hooks.txt:2783
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1015
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2001
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2005
returning false will NOT prevent logging $e
Definition hooks.txt:2176
const LIST_NAMES
Definition Defines.php:55
const LIST_COMMA
Definition Defines.php:52
const LIST_SET
Definition Defines.php:54
const LIST_AND
Definition Defines.php:53
if(is_array($mode)) switch( $mode) $input
$last
$params