MediaWiki REL1_33
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
80 protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
81 # Test for driver support, to avoid suppressed fatal error
82 if ( !function_exists( 'sqlsrv_connect' ) ) {
83 throw new DBConnectionError(
84 $this,
85 "Microsoft SQL Server Native (sqlsrv) functions missing.
86 You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n"
87 );
88 }
89
90 # e.g. the class is being loaded
91 if ( !strlen( $user ) ) {
92 return null;
93 }
94
95 $this->close();
96 $this->server = $server;
97 $this->user = $user;
98 $this->password = $password;
99
100 $connectionInfo = [];
101
102 if ( $dbName != '' ) {
103 $connectionInfo['Database'] = $dbName;
104 }
105
106 // Decide which auth scenerio to use
107 // if we are using Windows auth, then don't add credentials to $connectionInfo
108 if ( !$this->useWindowsAuth ) {
109 $connectionInfo['UID'] = $user;
110 $connectionInfo['PWD'] = $password;
111 }
112
114 $this->conn = sqlsrv_connect( $server, $connectionInfo );
116
117 if ( $this->conn === false ) {
118 throw new DBConnectionError( $this, $this->lastError() );
119 }
120
121 $this->opened = true;
122 $this->currentDomain = new DatabaseDomain(
123 ( $dbName != '' ) ? $dbName : null,
124 null,
125 $tablePrefix
126 );
127
128 return (bool)$this->conn;
129 }
130
136 protected function closeConnection() {
137 return sqlsrv_close( $this->conn );
138 }
139
144 protected function resultObject( $result ) {
145 if ( !$result ) {
146 return false;
147 } elseif ( $result instanceof MssqlResultWrapper ) {
148 return $result;
149 } elseif ( $result === true ) {
150 // Successful write query
151 return $result;
152 } else {
153 return new MssqlResultWrapper( $this, $result );
154 }
155 }
156
161 protected function doQuery( $sql ) {
162 // several extensions seem to think that all databases support limits
163 // via LIMIT N after the WHERE clause, but MSSQL uses SELECT TOP N,
164 // so to catch any of those extensions we'll do a quick check for a
165 // LIMIT clause and pass $sql through $this->LimitToTopN() which parses
166 // the LIMIT clause and passes the result to $this->limitResult();
167 if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
168 // massage LIMIT -> TopN
169 $sql = $this->LimitToTopN( $sql );
170 }
171
172 // MSSQL doesn't have EXTRACT(epoch FROM XXX)
173 if ( preg_match( '#\bEXTRACT\s*?\‍(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
174 // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
175 $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
176 }
177
178 // perform query
179
180 // SQLSRV_CURSOR_STATIC is slower than SQLSRV_CURSOR_CLIENT_BUFFERED (one of the two is
181 // needed if we want to be able to seek around the result set), however CLIENT_BUFFERED
182 // has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty
183 // strings make php throw a fatal error "Severe error translating Unicode"
184 if ( $this->scrollableCursor ) {
185 $scrollArr = [ 'Scrollable' => SQLSRV_CURSOR_STATIC ];
186 } else {
187 $scrollArr = [];
188 }
189
190 if ( $this->prepareStatements ) {
191 // we do prepare + execute so we can get its field metadata for later usage if desired
192 $stmt = sqlsrv_prepare( $this->conn, $sql, [], $scrollArr );
193 $success = sqlsrv_execute( $stmt );
194 } else {
195 $stmt = sqlsrv_query( $this->conn, $sql, [], $scrollArr );
196 $success = (bool)$stmt;
197 }
198
199 // Make a copy to ensure what we add below does not get reflected in future queries
201
202 if ( $this->ignoreDupKeyErrors ) {
203 // ignore duplicate key errors
204 // this emulates INSERT IGNORE in MySQL
205 $ignoreErrors[] = '2601'; // duplicate key error caused by unique index
206 $ignoreErrors[] = '2627'; // duplicate key error caused by primary key
207 $ignoreErrors[] = '3621'; // generic "the statement has been terminated" error
208 }
209
210 if ( $success === false ) {
211 $errors = sqlsrv_errors();
212 $success = true;
213
214 foreach ( $errors as $err ) {
215 if ( !in_array( $err['code'], $ignoreErrors ) ) {
216 $success = false;
217 break;
218 }
219 }
220
221 if ( $success === false ) {
222 return false;
223 }
224 }
225 // remember number of rows affected
226 $this->lastAffectedRowCount = sqlsrv_rows_affected( $stmt );
227
228 return $stmt;
229 }
230
231 public function freeResult( $res ) {
232 if ( $res instanceof ResultWrapper ) {
233 $res = $res->result;
234 }
235
237 }
238
243 public function fetchObject( $res ) {
244 // $res is expected to be an instance of MssqlResultWrapper here
245 return $res->fetchObject();
246 }
247
252 public function fetchRow( $res ) {
253 return $res->fetchRow();
254 }
255
260 public function numRows( $res ) {
261 if ( $res instanceof ResultWrapper ) {
262 $res = $res->result;
263 }
264
266
267 if ( $ret === false ) {
268 // we cannot get an amount of rows from this cursor type
269 // has_rows returns bool true/false if the result has rows
271 }
272
273 return $ret;
274 }
275
280 public function numFields( $res ) {
281 if ( $res instanceof ResultWrapper ) {
282 $res = $res->result;
283 }
284
285 return sqlsrv_num_fields( $res );
286 }
287
293 public function fieldName( $res, $n ) {
294 if ( $res instanceof ResultWrapper ) {
295 $res = $res->result;
296 }
297
298 return sqlsrv_field_metadata( $res )[$n]['Name'];
299 }
300
305 public function insertId() {
306 return $this->lastInsertId;
307 }
308
314 public function dataSeek( $res, $row ) {
315 return $res->seek( $row );
316 }
317
321 public function lastError() {
322 $strRet = '';
323 $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
324 if ( $retErrors != null ) {
325 foreach ( $retErrors as $arrError ) {
326 $strRet .= $this->formatError( $arrError ) . "\n";
327 }
328 } else {
329 $strRet = "No errors found";
330 }
331
332 return $strRet;
333 }
334
339 private function formatError( $err ) {
340 return '[SQLSTATE ' .
341 $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message'];
342 }
343
347 public function lastErrno() {
348 $err = sqlsrv_errors( SQLSRV_ERR_ALL );
349 if ( $err !== null && isset( $err[0] ) ) {
350 return $err[0]['code'];
351 } else {
352 return 0;
353 }
354 }
355
356 protected function wasKnownStatementRollbackError() {
357 $errors = sqlsrv_errors( SQLSRV_ERR_ALL );
358 if ( !$errors ) {
359 return false;
360 }
361 // The transaction vs statement rollback behavior depends on XACT_ABORT, so make sure
362 // that the "statement has been terminated" error (3621) is specifically present.
363 // https://docs.microsoft.com/en-us/sql/t-sql/statements/set-xact-abort-transact-sql
364 $statementOnly = false;
365 $codeWhitelist = [ '2601', '2627', '547' ];
366 foreach ( $errors as $error ) {
367 if ( $error['code'] == '3621' ) {
368 $statementOnly = true;
369 } elseif ( !in_array( $error['code'], $codeWhitelist ) ) {
370 $statementOnly = false;
371 break;
372 }
373 }
374
375 return $statementOnly;
376 }
377
381 protected function fetchAffectedRowCount() {
383 }
384
403 public function select( $table, $vars, $conds = '', $fname = __METHOD__,
404 $options = [], $join_conds = []
405 ) {
406 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
407 if ( isset( $options['EXPLAIN'] ) ) {
408 try {
409 $this->scrollableCursor = false;
410 $this->prepareStatements = false;
411 $this->query( "SET SHOWPLAN_ALL ON" );
412 $ret = $this->query( $sql, $fname );
413 $this->query( "SET SHOWPLAN_ALL OFF" );
414 } catch ( DBQueryError $dqe ) {
415 if ( isset( $options['FOR COUNT'] ) ) {
416 // likely don't have privs for SHOWPLAN, so run a select count instead
417 $this->query( "SET SHOWPLAN_ALL OFF" );
418 unset( $options['EXPLAIN'] );
419 $ret = $this->select(
420 $table,
421 'COUNT(*) AS EstimateRows',
422 $conds,
423 $fname,
424 $options,
425 $join_conds
426 );
427 } else {
428 // someone actually wanted the query plan instead of an est row count
429 // let them know of the error
430 $this->scrollableCursor = true;
431 $this->prepareStatements = true;
432 throw $dqe;
433 }
434 }
435 $this->scrollableCursor = true;
436 $this->prepareStatements = true;
437 return $ret;
438 }
439 return $this->query( $sql, $fname );
440 }
441
455 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
456 $options = [], $join_conds = []
457 ) {
458 if ( isset( $options['EXPLAIN'] ) ) {
459 unset( $options['EXPLAIN'] );
460 }
461
462 $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
463
464 // try to rewrite aggregations of bit columns (currently MAX and MIN)
465 if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) {
466 $bitColumns = [];
467 if ( is_array( $table ) ) {
468 $tables = $table;
469 while ( $tables ) {
470 $t = array_pop( $tables );
471 if ( is_array( $t ) ) {
473 } else {
474 $bitColumns += $this->getBitColumns( $this->tableName( $t ) );
475 }
476 }
477 } else {
478 $bitColumns = $this->getBitColumns( $this->tableName( $table ) );
479 }
480
481 foreach ( $bitColumns as $col => $info ) {
482 $replace = [
483 "MAX({$col})" => "MAX(CAST({$col} AS tinyint))",
484 "MIN({$col})" => "MIN(CAST({$col} AS tinyint))",
485 ];
486 $sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql );
487 }
488 }
489
490 return $sql;
491 }
492
493 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
494 $fname = __METHOD__
495 ) {
496 $this->scrollableCursor = false;
497 try {
498 parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname );
499 } catch ( Exception $e ) {
500 $this->scrollableCursor = true;
501 throw $e;
502 }
503 $this->scrollableCursor = true;
504 }
505
506 public function delete( $table, $conds, $fname = __METHOD__ ) {
507 $this->scrollableCursor = false;
508 try {
509 parent::delete( $table, $conds, $fname );
510 } catch ( Exception $e ) {
511 $this->scrollableCursor = true;
512 throw $e;
513 }
514 $this->scrollableCursor = true;
515
516 return true;
517 }
518
533 public function estimateRowCount( $table, $var = '*', $conds = '',
534 $fname = __METHOD__, $options = [], $join_conds = []
535 ) {
536 $conds = $this->normalizeConditions( $conds, $fname );
537 $column = $this->extractSingleFieldFromList( $var );
538 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
539 $conds[] = "$column IS NOT NULL";
540 }
541
542 // http://msdn2.microsoft.com/en-us/library/aa259203.aspx
543 $options['EXPLAIN'] = true;
544 $options['FOR COUNT'] = true;
545 $res = $this->select( $table, $var, $conds, $fname, $options, $join_conds );
546
547 $rows = -1;
548 if ( $res ) {
549 $row = $this->fetchRow( $res );
550
551 if ( isset( $row['EstimateRows'] ) ) {
552 $rows = (int)$row['EstimateRows'];
553 }
554 }
555
556 return $rows;
557 }
558
567 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
568 # This does not return the same info as MYSQL would, but that's OK
569 # because MediaWiki never uses the returned value except to check for
570 # the existence of indexes.
571 $sql = "sp_helpindex '" . $this->tableName( $table ) . "'";
572 $res = $this->query( $sql, $fname );
573
574 if ( !$res ) {
575 return null;
576 }
577
578 $result = [];
579 foreach ( $res as $row ) {
580 if ( $row->index_name == $index ) {
581 $row->Non_unique = !stristr( $row->index_description, "unique" );
582 $cols = explode( ", ", $row->index_keys );
583 foreach ( $cols as $col ) {
584 $row->Column_name = trim( $col );
585 $result[] = clone $row;
586 }
587 } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
588 $row->Non_unique = 0;
589 $cols = explode( ", ", $row->index_keys );
590 foreach ( $cols as $col ) {
591 $row->Column_name = trim( $col );
592 $result[] = clone $row;
593 }
594 }
595 }
596
597 return $result ?: false;
598 }
599
615 public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = [] ) {
616 # No rows to insert, easy just return now
617 if ( !count( $arrToInsert ) ) {
618 return true;
619 }
620
621 if ( !is_array( $options ) ) {
622 $options = [ $options ];
623 }
624
625 $table = $this->tableName( $table );
626
627 if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row
628 $arrToInsert = [ 0 => $arrToInsert ]; // make everything multi row compatible
629 }
630
631 // We know the table we're inserting into, get its identity column
632 $identity = null;
633 // strip matching square brackets and the db/schema from table name
634 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
635 $tableRaw = array_pop( $tableRawArr );
636 $res = $this->doQuery(
637 "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " .
638 "WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'"
639 );
640 if ( $res && sqlsrv_has_rows( $res ) ) {
641 // There is an identity for this table.
642 $identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC );
643 $identity = array_pop( $identityArr );
644 }
646
647 // Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF
648 $binaryColumns = $this->getBinaryColumns( $table );
649
650 // INSERT IGNORE is not supported by SQL Server
651 // remove IGNORE from options list and set ignore flag to true
652 if ( in_array( 'IGNORE', $options ) ) {
653 $options = array_diff( $options, [ 'IGNORE' ] );
654 $this->ignoreDupKeyErrors = true;
655 }
656
657 $ret = null;
658 foreach ( $arrToInsert as $a ) {
659 // start out with empty identity column, this is so we can return
660 // it as a result of the INSERT logic
661 $sqlPre = '';
662 $sqlPost = '';
663 $identityClause = '';
664
665 // if we have an identity column
666 if ( $identity ) {
667 // iterate through
668 foreach ( $a as $k => $v ) {
669 if ( $k == $identity ) {
670 if ( !is_null( $v ) ) {
671 // there is a value being passed to us,
672 // we need to turn on and off inserted identity
673 $sqlPre = "SET IDENTITY_INSERT $table ON;";
674 $sqlPost = ";SET IDENTITY_INSERT $table OFF;";
675 } else {
676 // we can't insert NULL into an identity column,
677 // so remove the column from the insert.
678 unset( $a[$k] );
679 }
680 }
681 }
682
683 // we want to output an identity column as result
684 $identityClause = "OUTPUT INSERTED.$identity ";
685 }
686
687 $keys = array_keys( $a );
688
689 // Build the actual query
690 $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
691 " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
692
693 $first = true;
694 foreach ( $a as $key => $value ) {
695 if ( isset( $binaryColumns[$key] ) ) {
696 $value = new MssqlBlob( $value );
697 }
698 if ( $first ) {
699 $first = false;
700 } else {
701 $sql .= ',';
702 }
703 if ( is_null( $value ) ) {
704 $sql .= 'null';
705 } else {
706 $sql .= $this->addQuotes( $value );
707 }
708 }
709 $sql .= ')' . $sqlPost;
710
711 // Run the query
712 $this->scrollableCursor = false;
713 try {
714 $ret = $this->query( $sql );
715 } catch ( Exception $e ) {
716 $this->scrollableCursor = true;
717 $this->ignoreDupKeyErrors = false;
718 throw $e;
719 }
720 $this->scrollableCursor = true;
721
722 if ( $ret instanceof ResultWrapper && !is_null( $identity ) ) {
723 // Then we want to get the identity column value we were assigned and save it off
724 $row = $ret->fetchObject();
725 if ( is_object( $row ) ) {
726 $this->lastInsertId = $row->$identity;
727 // It seems that mAffectedRows is -1 sometimes when OUTPUT INSERTED.identity is
728 // used if we got an identity back, we know for sure a row was affected, so
729 // adjust that here
730 if ( $this->lastAffectedRowCount == -1 ) {
731 $this->lastAffectedRowCount = 1;
732 }
733 }
734 }
735 }
736
737 $this->ignoreDupKeyErrors = false;
738
739 return true;
740 }
741
757 protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
758 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
759 ) {
760 $this->scrollableCursor = false;
761 try {
762 parent::nativeInsertSelect(
763 $destTable,
764 $srcTable,
765 $varMap,
766 $conds,
767 $fname,
768 $insertOptions,
769 $selectOptions,
770 $selectJoinConds
771 );
772 } catch ( Exception $e ) {
773 $this->scrollableCursor = true;
774 throw $e;
775 }
776 $this->scrollableCursor = true;
777 }
778
804 function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
805 $table = $this->tableName( $table );
806 $binaryColumns = $this->getBinaryColumns( $table );
807
808 $opts = $this->makeUpdateOptions( $options );
809 $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns );
810
811 if ( $conds !== [] && $conds !== '*' ) {
812 $sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns );
813 }
814
815 $this->scrollableCursor = false;
816 try {
817 $this->query( $sql );
818 } catch ( Exception $e ) {
819 $this->scrollableCursor = true;
820 throw $e;
821 }
822 $this->scrollableCursor = true;
823 return true;
824 }
825
842 public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = [] ) {
843 if ( !is_array( $a ) ) {
844 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
845 }
846
847 if ( $mode != LIST_NAMES ) {
848 // In MS SQL, values need to be specially encoded when they are
849 // inserted into binary fields. Perform this necessary encoding
850 // for the specified set of columns.
851 foreach ( array_keys( $a ) as $field ) {
852 if ( !isset( $binaryColumns[$field] ) ) {
853 continue;
854 }
855
856 if ( is_array( $a[$field] ) ) {
857 foreach ( $a[$field] as &$v ) {
858 $v = new MssqlBlob( $v );
859 }
860 unset( $v );
861 } else {
862 $a[$field] = new MssqlBlob( $a[$field] );
863 }
864 }
865 }
866
867 return parent::makeList( $a, $mode );
868 }
869
875 public function textFieldSize( $table, $field ) {
876 $table = $this->tableName( $table );
877 $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
878 WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
879 $res = $this->query( $sql );
880 $row = $this->fetchRow( $res );
881 $size = -1;
882 if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) {
883 $size = $row['CHARACTER_MAXIMUM_LENGTH'];
884 }
885
886 return $size;
887 }
888
899 public function limitResult( $sql, $limit, $offset = false ) {
900 if ( $offset === false || $offset == 0 ) {
901 if ( strpos( $sql, "SELECT" ) === false ) {
902 return "TOP {$limit} " . $sql;
903 } else {
904 return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi',
905 'SELECT$1 TOP ' . $limit, $sql, 1 );
906 }
907 } else {
908 // This one is fun, we need to pull out the select list as well as any ORDER BY clause
909 $select = $orderby = [];
910 $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select );
911 $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby );
912 $postOrder = '';
913 $first = $offset + 1;
914 $last = $offset + $limit;
915 $sub1 = 'sub_' . $this->subqueryId;
916 $sub2 = 'sub_' . ( $this->subqueryId + 1 );
917 $this->subqueryId += 2;
918 if ( !$s1 ) {
919 // wat
920 throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" );
921 }
922 if ( !$s2 ) {
923 // no ORDER BY
924 $overOrder = 'ORDER BY (SELECT 1)';
925 } else {
926 if ( !isset( $orderby[2] ) || !$orderby[2] ) {
927 // don't need to strip it out if we're using a FOR XML clause
928 $sql = str_replace( $orderby[1], '', $sql );
929 }
930 $overOrder = $orderby[1];
931 $postOrder = ' ' . $overOrder;
932 }
933 $sql = "SELECT {$select[1]}
934 FROM (
935 SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, *
936 FROM ({$sql}) {$sub1}
937 ) {$sub2}
938 WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}";
939
940 return $sql;
941 }
942 }
943
954 public function LimitToTopN( $sql ) {
955 // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
956 $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
957 if ( preg_match( $pattern, $sql, $matches ) ) {
958 $row_count = $matches[4];
959 $offset = $matches[3] ?: $matches[6] ?: false;
960
961 // strip the matching LIMIT clause out
962 $sql = str_replace( $matches[0], '', $sql );
963
964 return $this->limitResult( $sql, $row_count, $offset );
965 }
966
967 return $sql;
968 }
969
973 public function getSoftwareLink() {
974 return "[{{int:version-db-mssql-url}} MS SQL Server]";
975 }
976
980 public function getServerVersion() {
981 $server_info = sqlsrv_server_info( $this->conn );
982 $version = $server_info['SQLServerVersion'] ?? 'Error';
983
984 return $version;
985 }
986
992 public function tableExists( $table, $fname = __METHOD__ ) {
993 list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
994
995 if ( $db !== false ) {
996 // remote database
997 $this->queryLogger->error( "Attempting to call tableExists on a remote table" );
998 return false;
999 }
1000
1001 if ( $schema === false ) {
1002 $schema = $this->dbSchema();
1003 }
1004
1005 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES
1006 WHERE TABLE_TYPE = 'BASE TABLE'
1007 AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" );
1008
1009 if ( $res->numRows() ) {
1010 return true;
1011 } else {
1012 return false;
1013 }
1014 }
1015
1023 public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1024 list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
1025
1026 if ( $db !== false ) {
1027 // remote database
1028 $this->queryLogger->error( "Attempting to call fieldExists on a remote table" );
1029 return false;
1030 }
1031
1032 $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
1033 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
1034
1035 if ( $res->numRows() ) {
1036 return true;
1037 } else {
1038 return false;
1039 }
1040 }
1041
1042 public function fieldInfo( $table, $field ) {
1043 list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
1044
1045 if ( $db !== false ) {
1046 // remote database
1047 $this->queryLogger->error( "Attempting to call fieldInfo on a remote table" );
1048 return false;
1049 }
1050
1051 $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS
1052 WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
1053
1054 $meta = $res->fetchRow();
1055 if ( $meta ) {
1056 return new MssqlField( $meta );
1057 }
1058
1059 return false;
1060 }
1061
1062 protected function doSavepoint( $identifier, $fname ) {
1063 $this->query( 'SAVE TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname );
1064 }
1065
1066 protected function doReleaseSavepoint( $identifier, $fname ) {
1067 // Not supported. Also not really needed, a new doSavepoint() for the
1068 // same identifier will overwrite the old.
1069 }
1070
1071 protected function doRollbackToSavepoint( $identifier, $fname ) {
1072 $this->query( 'ROLLBACK TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname );
1073 }
1074
1079 protected function doBegin( $fname = __METHOD__ ) {
1080 sqlsrv_begin_transaction( $this->conn );
1081 $this->trxLevel = 1;
1082 }
1083
1088 protected function doCommit( $fname = __METHOD__ ) {
1089 sqlsrv_commit( $this->conn );
1090 $this->trxLevel = 0;
1091 }
1092
1098 protected function doRollback( $fname = __METHOD__ ) {
1099 sqlsrv_rollback( $this->conn );
1100 $this->trxLevel = 0;
1101 }
1102
1107 public function strencode( $s ) {
1108 // Should not be called by us
1109 return str_replace( "'", "''", $s );
1110 }
1111
1116 public function addQuotes( $s ) {
1117 if ( $s instanceof MssqlBlob ) {
1118 return $s->fetch();
1119 } elseif ( $s instanceof Blob ) {
1120 // this shouldn't really ever be called, but it's here if needed
1121 // (and will quite possibly make the SQL error out)
1122 $blob = new MssqlBlob( $s->fetch() );
1123 return $blob->fetch();
1124 } else {
1125 if ( is_bool( $s ) ) {
1126 $s = $s ? 1 : 0;
1127 }
1128 return parent::addQuotes( $s );
1129 }
1130 }
1131
1136 public function addIdentifierQuotes( $s ) {
1137 // http://msdn.microsoft.com/en-us/library/aa223962.aspx
1138 return '[' . $s . ']';
1139 }
1140
1145 public function isQuotedIdentifier( $name ) {
1146 return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']';
1147 }
1148
1156 protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
1157 return str_replace( [ $escapeChar, '%', '_', '[', ']', '^' ],
1158 [ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_",
1159 "{$escapeChar}[", "{$escapeChar}]", "{$escapeChar}^" ],
1160 $s );
1161 }
1162
1163 protected function doSelectDomain( DatabaseDomain $domain ) {
1164 if ( $domain->getSchema() !== null ) {
1165 throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
1166 }
1167
1168 $database = $domain->getDatabase();
1169 if ( $database !== $this->getDBname() ) {
1170 $encDatabase = $this->addIdentifierQuotes( $database );
1171 $res = $this->doQuery( "USE $encDatabase" );
1172 if ( !$res ) {
1173 throw new DBExpectedError( $this, "Could not select database '$database'." );
1174 }
1175 }
1176 // Update that domain fields on success (no exception thrown)
1177 $this->currentDomain = $domain;
1178
1179 return true;
1180 }
1181
1187 public function makeSelectOptions( $options ) {
1188 $tailOpts = '';
1189 $startOpts = '';
1190
1191 $noKeyOptions = [];
1192 foreach ( $options as $key => $option ) {
1193 if ( is_numeric( $key ) ) {
1194 $noKeyOptions[$option] = true;
1195 }
1196 }
1197
1198 $tailOpts .= $this->makeGroupByWithHaving( $options );
1199
1200 $tailOpts .= $this->makeOrderBy( $options );
1201
1202 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1203 $startOpts .= 'DISTINCT';
1204 }
1205
1206 if ( isset( $noKeyOptions['FOR XML'] ) ) {
1207 // used in group concat field emulation
1208 $tailOpts .= " FOR XML PATH('')";
1209 }
1210
1211 // we want this to be compatible with the output of parent::makeSelectOptions()
1212 return [ $startOpts, '', $tailOpts, '', '' ];
1213 }
1214
1215 public function getType() {
1216 return 'mssql';
1217 }
1218
1223 public function buildConcat( $stringList ) {
1224 return implode( ' + ', $stringList );
1225 }
1226
1244 public function buildGroupConcatField( $delim, $table, $field, $conds = '',
1245 $join_conds = []
1246 ) {
1247 $gcsq = 'gcsq_' . $this->subqueryId;
1248 $this->subqueryId++;
1249
1250 $delimLen = strlen( $delim );
1251 $fld = "{$field} + {$this->addQuotes( $delim )}";
1252 $sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM ("
1253 . $this->selectSQLText( $table, $fld, $conds, null, [ 'FOR XML' ], $join_conds )
1254 . ") {$gcsq} ({$field}))";
1255
1256 return $sql;
1257 }
1258
1259 public function buildSubstring( $input, $startPosition, $length = null ) {
1260 $this->assertBuildSubstringParams( $startPosition, $length );
1261 if ( $length === null ) {
1267 $length = 2147483647;
1268 }
1269 return 'SUBSTRING(' . implode( ',', [ $input, $startPosition, $length ] ) . ')';
1270 }
1271
1278 private function getBinaryColumns( $table ) {
1279 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
1280 $tableRaw = array_pop( $tableRawArr );
1281
1282 if ( $this->binaryColumnCache === null ) {
1283 $this->populateColumnCaches();
1284 }
1285
1286 return $this->binaryColumnCache[$tableRaw] ?? [];
1287 }
1288
1293 private function getBitColumns( $table ) {
1294 $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
1295 $tableRaw = array_pop( $tableRawArr );
1296
1297 if ( $this->bitColumnCache === null ) {
1298 $this->populateColumnCaches();
1299 }
1300
1301 return $this->bitColumnCache[$tableRaw] ?? [];
1302 }
1303
1304 private function populateColumnCaches() {
1305 $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
1306 [
1307 'TABLE_CATALOG' => $this->getDBname(),
1308 'TABLE_SCHEMA' => $this->dbSchema(),
1309 'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
1310 ] );
1311
1312 $this->binaryColumnCache = [];
1313 $this->bitColumnCache = [];
1314 foreach ( $res as $row ) {
1315 if ( $row->DATA_TYPE == 'bit' ) {
1316 $this->bitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
1317 } else {
1318 $this->binaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
1319 }
1320 }
1321 }
1322
1329 function tableName( $name, $format = 'quoted' ) {
1330 # Replace reserved words with better ones
1331 switch ( $name ) {
1332 case 'user':
1333 return $this->realTableName( 'mwuser', $format );
1334 default:
1335 return $this->realTableName( $name, $format );
1336 }
1337 }
1338
1347 function realTableName( $name, $format = 'quoted' ) {
1348 $table = parent::tableName( $name, $format );
1349 if ( $format == 'split' ) {
1350 // Used internally, we want the schema split off from the table name and returned
1351 // as a list with 3 elements (database, schema, table)
1352 return array_pad( explode( '.', $table, 3 ), -3, false );
1353 }
1354 return $table;
1355 }
1356
1364 public function dropTable( $tableName, $fName = __METHOD__ ) {
1365 if ( !$this->tableExists( $tableName, $fName ) ) {
1366 return false;
1367 }
1368
1369 // parent function incorrectly appends CASCADE, which we don't want
1370 $sql = "DROP TABLE " . $this->tableName( $tableName );
1371
1372 return $this->query( $sql, $fName );
1373 }
1374
1381 public function prepareStatements( $value = null ) {
1383 if ( $value !== null ) {
1384 $this->prepareStatements = $value;
1385 }
1386
1387 return $old;
1388 }
1389
1396 public function scrollableCursor( $value = null ) {
1398 if ( $value !== null ) {
1399 $this->scrollableCursor = $value;
1400 }
1401
1402 return $old;
1403 }
1404
1405 public function buildStringCast( $field ) {
1406 return "CAST( $field AS NVARCHAR )";
1407 }
1408
1409 public static function getAttributes() {
1410 return [ self::ATTR_SCHEMAS_AS_TABLE_GROUPS => true ];
1411 }
1412}
1413
1417class_alias( DatabaseMssql::class, 'DatabaseMssql' );
and that you know you can do these things To protect your we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights These restrictions translate to certain responsibilities for you if you distribute copies of the or if you modify it For if you distribute copies of such a whether gratis or for a you must give the recipients all the rights that you have You must make sure that receive or can get the source code And you must show them these terms so they know their rights We protect your rights with two and(2) offer you this license which gives you legal permission to copy
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:123
Base class for the more common types of database errors.
Class to handle database/prefix specification for IDatabase domains.
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...
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.
open( $server, $user, $password, $dbName, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
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
doSelectDomain(DatabaseDomain $domain)
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:49
makeUpdateOptions( $options)
Make UPDATE options for the Database::update function.
object resource null $conn
Database connection.
Definition Database.php:109
dbSchema( $schema=null)
Get/set the db schema.
Definition Database.php:620
string $user
User that this instance is currently connected under the name of.
Definition Database.php:84
makeGroupByWithHaving( $options)
Returns an optional GROUP BY with an optional HAVING.
trxLevel()
Gets the current transaction level.
Definition Database.php:591
assertBuildSubstringParams( $startPosition, $length)
Check type and bounds for parameters to self::buildSubstring()
string $password
Password used to establish the current connection.
Definition Database.php:86
string $server
Server that this instance is currently connected to.
Definition Database.php:82
normalizeConditions( $conds, $fname)
makeOrderBy( $options)
Returns an optional ORDER BY.
getDBname()
Get the current DB name.
close()
Close the database connection.
Definition Database.php:943
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:2340
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:2818
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:1999
this hook is for auditing only 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:996
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 just before the function returns a value If you return true
Definition hooks.txt:2004
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:2003
returning false will NOT prevent logging $e
Definition hooks.txt:2175
const LIST_NAMES
Definition Defines.php:54
const LIST_COMMA
Definition Defines.php:51
const LIST_SET
Definition Defines.php:53
const LIST_AND
Definition Defines.php:52
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
if(is_array($mode)) switch( $mode) $input
$last
$params