28use Psr\Log\LoggerAwareInterface;
29use Psr\Log\LoggerInterface;
30use Wikimedia\ScopedCallback;
31use Wikimedia\Timestamp\ConvertibleTimestamp;
35use InvalidArgumentException;
250 $password =
$params[
'password'];
253 $this->mSchema =
$params[
'schema'];
254 $this->mTablePrefix =
$params[
'tablePrefix'];
256 $this->cliMode =
$params[
'cliMode'];
258 $this->agent = str_replace(
'/',
'-',
$params[
'agent'] );
260 $this->mFlags =
$params[
'flags'];
261 if ( $this->mFlags & self::DBO_DEFAULT ) {
262 if ( $this->cliMode ) {
269 $this->mSessionVars =
$params[
'variables'];
271 $this->srvCache = isset(
$params[
'srvCache'] )
275 $this->profiler =
$params[
'profiler'];
276 $this->trxProfiler =
$params[
'trxProfiler'];
277 $this->connLogger =
$params[
'connLogger'];
278 $this->queryLogger =
$params[
'queryLogger'];
279 $this->errorLogger =
$params[
'errorLogger'];
285 $this->
open( $server,
$user, $password, $dbName );
287 throw new InvalidArgumentException(
"No database user provided." );
291 if ( $this->mDBname !=
'' ) {
293 $this->currentDomain =
new DatabaseDomain( $this->mDBname,
null, $this->mTablePrefix );
338 final public static function factory( $dbType, $p = [] ) {
339 static $canonicalDBTypes = [
340 'mysql' => [
'mysqli',
'mysql' ],
346 static $classAliases = [
347 'DatabaseMssql' => DatabaseMssql::class,
348 'DatabaseMysql' => DatabaseMysql::class,
349 'DatabaseMysqli' => DatabaseMysqli::class,
350 'DatabaseSqlite' => DatabaseSqlite::class,
351 'DatabasePostgres' => DatabasePostgres::class
355 $dbType = strtolower( $dbType );
356 if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
357 $possibleDrivers = $canonicalDBTypes[$dbType];
358 if ( !empty( $p[
'driver'] ) ) {
359 if ( in_array( $p[
'driver'], $possibleDrivers ) ) {
360 $driver = $p[
'driver'];
362 throw new InvalidArgumentException( __METHOD__ .
363 " type '$dbType' does not support driver '{$p['driver']}'" );
366 foreach ( $possibleDrivers
as $posDriver ) {
367 if ( extension_loaded( $posDriver ) ) {
368 $driver = $posDriver;
377 if ( $driver ===
false || $driver ===
'' ) {
378 throw new InvalidArgumentException( __METHOD__ .
379 " no viable database extension found for type '$dbType'" );
382 $class =
'Database' . ucfirst( $driver );
383 if ( isset( $classAliases[$class] ) ) {
384 $class = $classAliases[$class];
387 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
389 $p[
'host'] = isset( $p[
'host'] ) ? $p[
'host'] :
false;
390 $p[
'user'] = isset( $p[
'user'] ) ? $p[
'user'] :
false;
391 $p[
'password'] = isset( $p[
'password'] ) ? $p[
'password'] :
false;
392 $p[
'dbname'] = isset( $p[
'dbname'] ) ? $p[
'dbname'] :
false;
393 $p[
'flags'] = isset( $p[
'flags'] ) ? $p[
'flags'] : 0;
394 $p[
'variables'] = isset( $p[
'variables'] ) ? $p[
'variables'] : [];
395 $p[
'tablePrefix'] = isset( $p[
'tablePrefix'] ) ? $p[
'tablePrefix'] :
'';
396 $p[
'schema'] = isset( $p[
'schema'] ) ? $p[
'schema'] :
'';
397 $p[
'cliMode'] = isset( $p[
'cliMode'] ) ? $p[
'cliMode'] : ( PHP_SAPI ===
'cli' );
398 $p[
'agent'] = isset( $p[
'agent'] ) ? $p[
'agent'] :
'';
399 if ( !isset( $p[
'connLogger'] ) ) {
400 $p[
'connLogger'] = new \Psr\Log\NullLogger();
402 if ( !isset( $p[
'queryLogger'] ) ) {
403 $p[
'queryLogger'] = new \Psr\Log\NullLogger();
405 $p[
'profiler'] = isset( $p[
'profiler'] ) ? $p[
'profiler'] :
null;
406 if ( !isset( $p[
'trxProfiler'] ) ) {
409 if ( !isset( $p[
'errorLogger'] ) ) {
410 $p[
'errorLogger'] =
function ( Exception
$e ) {
411 trigger_error( get_class(
$e ) .
': ' .
$e->getMessage(), E_USER_WARNING );
415 $conn =
new $class( $p );
431 $this->queryLogger = $logger;
443 : $this->
setFlag( self::DBO_NOBUFFER );
463 if ( $ignoreErrors !==
null ) {
465 ? $this->
setFlag( self::DBO_IGNORE )
477 return $this->mTrxLevel ? $this->mTrxTimestamp :
null;
482 if ( $prefix !==
null ) {
483 $this->mTablePrefix = $prefix;
484 $this->currentDomain = ( $this->mDBname !=
'' )
494 if ( $schema !==
null ) {
495 $this->mSchema = $schema;
502 if ( is_null(
$name ) ) {
505 if ( array_key_exists(
$name, $this->mLBInfo ) ) {
506 return $this->mLBInfo[
$name];
514 if ( is_null(
$value ) ) {
515 $this->mLBInfo =
$name;
522 $this->lazyMasterHandle = $conn;
551 return $this->mLastWriteTime ?:
false;
559 return $this->mTrxLevel && (
565 if ( !$this->mTrxLevel ) {
567 } elseif ( !$this->mTrxDoneWrites ) {
572 case self::ESTIMATE_DB_APPLY:
574 $rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
575 $applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
578 $applyTime += self::TINY_WRITE_SEC * $omitted;
587 return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
601 if ( !$this->mTrxLevel ) {
607 $this->mTrxIdleCallbacks,
608 $this->mTrxPreCommitCallbacks,
609 $this->mTrxEndCallbacks
611 foreach ( $callbacks
as $callback ) {
612 $fnames[] = $callback[1];
623 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
624 if ( $remember === self::REMEMBER_PRIOR ) {
625 array_push( $this->priorFlags, $this->mFlags );
627 $this->mFlags |= $flag;
630 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
631 if ( $remember === self::REMEMBER_PRIOR ) {
632 array_push( $this->priorFlags, $this->mFlags );
634 $this->mFlags &= ~$flag;
638 if ( !$this->priorFlags ) {
642 if ( $state === self::RESTORE_INITIAL ) {
643 $this->mFlags = reset( $this->priorFlags );
644 $this->priorFlags = [];
646 $this->mFlags = array_pop( $this->priorFlags );
651 return !!( $this->mFlags & $flag );
664 return $this->currentDomain->getId();
692 $this->mPHPError =
false;
693 $this->htmlErrors = ini_set(
'html_errors',
'0' );
694 set_error_handler( [ $this,
'connectionErrorLogger' ] );
703 restore_error_handler();
704 if ( $this->htmlErrors !==
false ) {
705 ini_set(
'html_errors', $this->htmlErrors );
715 if ( $this->mPHPError ) {
716 $error = preg_replace(
'!\[<a.*</a>\]!',
'', $this->mPHPError );
717 $error = preg_replace(
'!^.*?:\s?(.*)$!',
'$1', $error );
733 $this->mPHPError = $errstr;
745 'db_server' => $this->mServer,
746 'db_name' => $this->mDBname,
747 'db_user' => $this->mUser,
754 if ( $this->mConn ) {
756 $this->
commit( __METHOD__, self::FLUSHING_INTERNAL );
760 $this->mConn =
false;
762 $this->mTrxIdleCallbacks ||
763 $this->mTrxPreCommitCallbacks ||
764 $this->mTrxEndCallbacks
766 throw new RuntimeException(
"Transaction callbacks still pending." );
770 $this->mOpened =
false;
821 '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
829 return preg_match(
'/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) :
null;
844 [
'BEGIN',
'COMMIT',
'ROLLBACK',
'SHOW',
'SET',
'CREATE',
'ALTER' ],
855 '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
859 $this->mSessionTempTables[
$matches[1]] = 1;
862 } elseif ( preg_match(
863 '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
867 $isTemp = isset( $this->mSessionTempTables[
$matches[1]] );
868 unset( $this->mSessionTempTables[
$matches[1]] );
871 } elseif ( preg_match(
872 '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
876 return isset( $this->mSessionTempTables[
$matches[1]] );
877 } elseif ( preg_match(
878 '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
882 return isset( $this->mSessionTempTables[
$matches[1]] );
888 public function query( $sql,
$fname = __METHOD__, $tempIgnore =
false ) {
890 $this->mLastQuery = $sql;
896 $isNonTempWrite =
false;
900 # In theory, non-persistent writes are allowed in read-only mode, but due to things
903 if ( $reason !==
false ) {
906 # Set a flag indicating that writes have been done
907 $this->mLastWriteTime = microtime(
true );
910 # Add trace comment to the begin of the sql string, right after the operator.
911 # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
912 $commentedSql = preg_replace( '/\s|$/', " ", $sql, 1 );
914 # Start implicit transactions that wrap the request if DBO_TRX is enabled
915 if ( !$this->mTrxLevel && $this->getFlag( self::DBO_TRX )
916 && $this->isTransactableQuery( $sql )
918 $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
919 $this->mTrxAutomatic = true;
922 # Keep track of whether the transaction has write queries pending
923 if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
924 $this->mTrxDoneWrites = true;
925 $this->trxProfiler->transactionWritingIn(
926 $this->mServer, $this->mDBname, $this->mTrxShortId );
929 if ( $this->getFlag( self::DBO_DEBUG ) ) {
930 $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
933 # Avoid fatals if close() was called
936 # Send the query to the server
937 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
939 # Try reconnecting if the connection was lost
940 if ( false === $ret && $this->wasErrorReissuable() ) {
941 $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
942 # Stash the last error values before anything might clear them
943 $lastError = $this->lastError();
944 $lastErrno = $this->lastErrno();
945 # Update state tracking to reflect transaction loss due to disconnection
946 $this->handleSessionLoss();
947 if ( $this->reconnect() ) {
948 $msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
949 $this->connLogger->warning( $msg );
950 $this->queryLogger->warning(
951 "$msg:\n" . ( new RuntimeException() )->getTraceAsString() );
953 if ( !$recoverable ) {
954 # Callers may catch the exception and continue to use the DB
955 $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
957 # Should be safe to silently retry the query
958 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
961 $msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
962 $this->connLogger->error( $msg );
966 if ( false === $ret ) {
967 # Deadlocks cause the entire transaction to abort, not just the statement.
968 # https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
969 # https://www.postgresql.org/docs/9.1/static/explicit-locking.html
970 if ( $this->wasDeadlock() ) {
971 if ( $this->explicitTrxActive() || $priorWritesPending ) {
972 $tempIgnore = false; // not recoverable
974 # Update state tracking to reflect transaction loss
975 $this->handleSessionLoss();
978 $this->reportQueryError(
979 $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
982 $res = $this->resultObject( $ret );
998 private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
999 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
1000 # generalizeSQL() will probably cut down the query to reasonable
1001 # logging size most of the time. The substr is really just a sanity check.
1003 $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1005 $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1008 # Include query transaction state
1009 $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
1011 $startTime = microtime( true );
1012 if ( $this->profiler ) {
1013 call_user_func( [ $this->profiler, 'profileIn' ], $queryProf );
1015 $ret = $this->doQuery( $commentedSql );
1016 if ( $this->profiler ) {
1017 call_user_func( [ $this->profiler, 'profileOut' ], $queryProf );
1019 $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1021 unset( $queryProfSection ); // profile out (if set)
1023 if ( $ret !== false ) {
1024 $this->lastPing = $startTime;
1025 if ( $isWrite && $this->mTrxLevel ) {
1026 $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1027 $this->mTrxWriteCallers[] = $fname;
1031 if ( $sql === self::PING_QUERY ) {
1032 $this->mRTTEstimate = $queryRuntime;
1035 $this->trxProfiler->recordQueryCompletion(
1036 $queryProf, $startTime, $isWrite, $this->affectedRows()
1038 $this->queryLogger->debug( $sql, [
1040 'master' => $isMaster,
1041 'runtime' => $queryRuntime,
1059 private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1060 // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1061 $indicativeOfReplicaRuntime = true;
1062 if ( $runtime > self::SLOW_WRITE_SEC ) {
1063 $verb = $this->getQueryVerb( $sql );
1064 // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1065 if ( $verb === 'INSERT' ) {
1066 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1067 } elseif ( $verb === 'REPLACE' ) {
1068 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1072 $this->mTrxWriteDuration += $runtime;
1073 $this->mTrxWriteQueryCount += 1;
1074 $this->mTrxWriteAffectedRows += $affected;
1075 if ( $indicativeOfReplicaRuntime ) {
1076 $this->mTrxWriteAdjDuration += $runtime;
1077 $this->mTrxWriteAdjQueryCount += 1;
1091 private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1092 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1093 # Dropped connections also mean that named locks are automatically released.
1094 # Only allow error suppression in autocommit mode or when the lost transaction
1095 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1096 if ( $this->mNamedLocksHeld ) {
1097 return false; // possible critical section violation
1098 } elseif ( $sql === 'COMMIT' ) {
1099 return !$priorWritesPending; // nothing written anyway? (T127428)
1100 } elseif ( $sql === 'ROLLBACK' ) {
1101 return true; // transaction lost...which is also what was requested :)
1102 } elseif ( $this->explicitTrxActive() ) {
1103 return false; // don't drop atomocity
1104 } elseif ( $priorWritesPending ) {
1105 return false; // prior writes lost from implicit transaction
1116 private function handleSessionLoss() {
1117 $this->mTrxLevel = 0;
1118 $this->mTrxIdleCallbacks = []; // T67263
1119 $this->mTrxPreCommitCallbacks = []; // T67263
1120 $this->mSessionTempTables = [];
1121 $this->mNamedLocksHeld = [];
1123 // Handle callbacks in mTrxEndCallbacks
1124 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1125 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1127 } catch ( Exception $e ) {
1128 // Already logged; move on...
1133 public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1134 if ( $this->ignoreErrors() || $tempIgnore ) {
1135 $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1137 $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1138 $this->queryLogger->error(
1139 "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1140 $this->getLogContext( [
1141 'method' => __METHOD__,
1144 'sql1line' => $sql1line,
1148 $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1149 throw new DBQueryError( $this, $error, $errno, $sql, $fname );
1153 public function freeResult( $res ) {
1156 public function selectField(
1157 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1159 if ( $var === '*' ) { // sanity
1160 throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1163 if ( !is_array( $options ) ) {
1164 $options = [ $options ];
1167 $options['LIMIT'] = 1;
1169 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1170 if ( $res === false || !$this->numRows( $res ) ) {
1174 $row = $this->fetchRow( $res );
1176 if ( $row !== false ) {
1177 return reset( $row );
1183 public function selectFieldValues(
1184 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1186 if ( $var === '*' ) { // sanity
1187 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1188 } elseif ( !is_string( $var ) ) { // sanity
1189 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1192 if ( !is_array( $options ) ) {
1193 $options = [ $options ];
1196 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1197 if ( $res === false ) {
1202 foreach ( $res as $row ) {
1203 $values[] = $row->$var;
1218 protected function makeSelectOptions( $options ) {
1219 $preLimitTail = $postLimitTail = '';
1224 foreach ( $options as $key => $option ) {
1225 if ( is_numeric( $key ) ) {
1226 $noKeyOptions[$option] = true;
1230 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1232 $preLimitTail .= $this->makeOrderBy( $options );
1234 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1235 $postLimitTail .= ' FOR UPDATE';
1238 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1239 $postLimitTail .= ' LOCK IN SHARE MODE';
1242 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1243 $startOpts .= 'DISTINCT';
1246 # Various MySQL extensions
1247 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1248 $startOpts .= ' /*! STRAIGHT_JOIN */';
1251 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1252 $startOpts .= ' HIGH_PRIORITY';
1255 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1256 $startOpts .= ' SQL_BIG_RESULT';
1259 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1260 $startOpts .= ' SQL_BUFFER_RESULT';
1263 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1264 $startOpts .= ' SQL_SMALL_RESULT';
1267 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1268 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1271 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1272 $startOpts .= ' SQL_CACHE';
1275 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1276 $startOpts .= ' SQL_NO_CACHE';
1279 if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1280 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1284 if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1285 $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1290 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1301 protected function makeGroupByWithHaving( $options ) {
1303 if ( isset( $options['GROUP BY'] ) ) {
1304 $gb = is_array( $options['GROUP BY'] )
1305 ? implode( ',', $options['GROUP BY'] )
1306 : $options['GROUP BY'];
1307 $sql .= ' GROUP BY ' . $gb;
1309 if ( isset( $options['HAVING'] ) ) {
1310 $having = is_array( $options['HAVING'] )
1311 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1312 : $options['HAVING'];
1313 $sql .= ' HAVING ' . $having;
1327 protected function makeOrderBy( $options ) {
1328 if ( isset( $options['ORDER BY'] ) ) {
1329 $ob = is_array( $options['ORDER BY'] )
1330 ? implode( ',', $options['ORDER BY'] )
1331 : $options['ORDER BY'];
1333 return ' ORDER BY ' . $ob;
1339 public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1340 $options = [], $join_conds = [] ) {
1341 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1343 return $this->query( $sql, $fname );
1346 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1347 $options = [], $join_conds = []
1349 if ( is_array( $vars ) ) {
1350 $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1353 $options = (array)$options;
1354 $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1355 ? $options['USE INDEX']
1358 isset( $options['IGNORE INDEX'] ) &&
1359 is_array( $options['IGNORE INDEX'] )
1361 ? $options['IGNORE INDEX']
1364 if ( is_array( $table ) ) {
1366 $this->tableNamesWithIndexClauseOrJOIN(
1367 $table, $useIndexes, $ignoreIndexes, $join_conds );
1368 } elseif ( $table != '' ) {
1369 if ( $table[0] == ' ' ) {
1370 $from = ' FROM ' . $table;
1373 $this->tableNamesWithIndexClauseOrJOIN(
1374 [ $table ], $useIndexes, $ignoreIndexes, [] );
1380 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1381 $this->makeSelectOptions( $options );
1383 if ( !empty( $conds ) ) {
1384 if ( is_array( $conds ) ) {
1385 $conds = $this->makeList( $conds, self::LIST_AND );
1387 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
1388 "WHERE $conds $preLimitTail";
1390 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
1393 if ( isset( $options['LIMIT'] ) ) {
1394 $sql = $this->limitResult( $sql, $options['LIMIT'],
1395 isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1397 $sql = "$sql $postLimitTail";
1399 if ( isset( $options['EXPLAIN'] ) ) {
1400 $sql = 'EXPLAIN ' . $sql;
1406 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1407 $options = [], $join_conds = []
1409 $options = (array)$options;
1410 $options['LIMIT'] = 1;
1411 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1413 if ( $res === false ) {
1417 if ( !$this->numRows( $res ) ) {
1421 $obj = $this->fetchObject( $res );
1426 public function estimateRowCount(
1427 $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1430 $res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1433 $row = $this->fetchRow( $res );
1434 $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1440 public function selectRowCount(
1441 $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1444 $sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1445 // The identifier quotes is primarily for MSSQL.
1446 $rowCountCol = $this->addIdentifierQuotes( "rowcount" );
1447 $tableName = $this->addIdentifierQuotes( "tmp_count" );
1448 $res = $this->query( "SELECT COUNT(*) AS $rowCountCol FROM ($sql) $tableName", $fname );
1451 $row = $this->fetchRow( $res );
1452 $rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1466 protected static function generalizeSQL( $sql ) {
1467 # This does the same as the regexp below would do, but in such a way
1468 # as to avoid crashing php on some large strings.
1469 # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1471 $sql = str_replace(
"\\\\",
'', $sql );
1472 $sql = str_replace(
"\\'",
'', $sql );
1473 $sql = str_replace(
"\\\"",
'', $sql );
1474 $sql = preg_replace(
"/'.*'/s",
"'X'", $sql );
1475 $sql = preg_replace(
'/".*"/s',
"'X'", $sql );
1477 # All newlines, tabs, etc replaced by single space
1478 $sql = preg_replace(
'/\s+/',
' ', $sql );
1481 # except the ones surrounded by characters, e.g. l10n
1482 $sql = preg_replace(
'/-?\d+(,-?\d+)+/s',
'N,...,N', $sql );
1483 $sql = preg_replace(
'/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s',
'N', $sql );
1489 $info = $this->
fieldInfo( $table, $field );
1500 if ( is_null( $info ) ) {
1503 return $info !==
false;
1508 $tableRaw = $this->
tableName( $table,
'raw' );
1509 if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
1514 $ignoreErrors =
true;
1515 $res = $this->
query(
"SELECT 1 FROM $table LIMIT 1",
$fname, $ignoreErrors );
1521 $indexInfo = $this->
indexInfo( $table, $index );
1523 if ( !$indexInfo ) {
1527 return !$indexInfo[0]->Non_unique;
1541 # No rows to insert, easy just return now
1542 if ( !count( $a ) ) {
1553 if ( isset(
$options[
'fileHandle'] ) ) {
1558 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1560 $keys = array_keys( $a[0] );
1563 $keys = array_keys( $a );
1567 " INTO $table (" . implode(
',',
$keys ) .
') VALUES ';
1571 foreach ( $a
as $row ) {
1577 $sql .=
'(' . $this->
makeList( $row ) .
')';
1580 $sql .=
'(' . $this->
makeList( $a ) .
')';
1583 if ( $fh !==
null &&
false === fwrite( $fh, $sql ) ) {
1585 } elseif ( $fh !==
null ) {
1605 if ( in_array(
'IGNORE',
$options ) ) {
1621 return implode(
' ', $opts );
1627 $sql =
"UPDATE $opts $table SET " . $this->
makeList( $values, self::LIST_SET );
1629 if ( $conds !== [] && $conds !==
'*' ) {
1630 $sql .=
" WHERE " . $this->
makeList( $conds, self::LIST_AND );
1636 public function makeList( $a, $mode = self::LIST_COMMA ) {
1637 if ( !is_array( $a ) ) {
1638 throw new DBUnexpectedError( $this, __METHOD__ .
' called with incorrect parameters' );
1644 foreach ( $a
as $field =>
$value ) {
1646 if ( $mode == self::LIST_AND ) {
1648 } elseif ( $mode == self::LIST_OR ) {
1657 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
1658 $list .=
"($value)";
1659 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
1662 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array(
$value )
1665 $includeNull =
false;
1666 foreach ( array_keys(
$value,
null,
true )
as $nullKey ) {
1667 $includeNull =
true;
1668 unset(
$value[$nullKey] );
1670 if ( count(
$value ) == 0 && !$includeNull ) {
1671 throw new InvalidArgumentException(
1672 __METHOD__ .
": empty input for field $field" );
1673 } elseif ( count(
$value ) == 0 ) {
1675 $list .=
"$field IS NULL";
1678 if ( $includeNull ) {
1682 if ( count(
$value ) == 1 ) {
1692 if ( $includeNull ) {
1693 $list .=
" OR $field IS NULL)";
1696 } elseif (
$value ===
null ) {
1697 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
1698 $list .=
"$field IS ";
1699 } elseif ( $mode == self::LIST_SET ) {
1700 $list .=
"$field = ";
1705 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
1707 $list .=
"$field = ";
1719 foreach ( $data
as $base => $sub ) {
1720 if ( count( $sub ) ) {
1722 [ $baseKey =>
$base, $subKey => array_keys( $sub ) ],
1728 return $this->
makeList( $conds, self::LIST_OR );
1743 public function bitAnd( $fieldLeft, $fieldRight ) {
1744 return "($fieldLeft & $fieldRight)";
1747 public function bitOr( $fieldLeft, $fieldRight ) {
1748 return "($fieldLeft | $fieldRight)";
1752 return 'CONCAT(' . implode(
',', $stringList ) .
')';
1756 $delim, $table, $field, $conds =
'', $join_conds = []
1758 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->
addQuotes( $delim ) .
')';
1760 return '(' . $this->
selectSQLText( $table, $fld, $conds,
null, [], $join_conds ) .
')';
1772 # Stub. Shouldn't cause serious problems if it's not overridden, but
1773 # if your database engine supports a concept similar to MySQL's
1774 # databases you may as well.
1775 $this->mDBname = $db;
1789 # Skip the entire process when we have a string quoted on both ends.
1790 # Note that we check the end so that we will still quote any use of
1791 # use of `database`.table. But won't break things if someone wants
1792 # to query a database table with a dot in the name.
1797 # Lets test for any bits of text that should never show up in a table
1798 # name. Basically anything like JOIN or ON which are actually part of
1799 # SQL queries, but may end up inside of the table value to combine
1800 # sql. Such as how the API is doing.
1801 # Note that we use a whitespace test rather than a \b test to avoid
1802 # any remote case where a word like on may be inside of a table name
1803 # surrounded by symbols which may be considered word breaks.
1804 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i',
$name ) !== 0 ) {
1808 # Split database and table into proper variables.
1811 # Quote $table and apply the prefix if not quoted.
1812 # $tableName might be empty if this is called from Database::replaceVars()
1813 $tableName =
"{$prefix}{$table}";
1814 if ( $format ===
'quoted'
1816 && $tableName !==
''
1821 # Quote $schema and $database and merge them with the table name if needed
1835 # We reverse the explode so that database.table and table both output the correct table.
1836 $dbDetails = explode(
'.',
$name, 3 );
1837 if ( count( $dbDetails ) == 3 ) {
1838 list( $database, $schema, $table ) = $dbDetails;
1839 # We don't want any prefix added in this case
1841 } elseif ( count( $dbDetails ) == 2 ) {
1842 list( $database, $table ) = $dbDetails;
1843 # We don't want any prefix added in this case
1845 # In dbs that support it, $database may actually be the schema
1846 # but that doesn't affect any of the functionality here
1849 list( $table ) = $dbDetails;
1850 if ( isset( $this->tableAliases[$table] ) ) {
1851 $database = $this->tableAliases[$table][
'dbname'];
1852 $schema = is_string( $this->tableAliases[$table][
'schema'] )
1853 ? $this->tableAliases[$table][
'schema']
1855 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
1856 ? $this->tableAliases[$table][
'prefix']
1865 return [ $database, $schema, $prefix, $table ];
1875 if ( strlen( $namespace ) ) {
1879 $relation = $namespace .
'.' . $relation;
1886 $inArray = func_get_args();
1889 foreach ( $inArray
as $name ) {
1897 $inArray = func_get_args();
1900 foreach ( $inArray
as $name ) {
1916 if ( !$alias || $alias ==
$name ) {
1931 foreach (
$tables as $alias => $table ) {
1932 if ( is_numeric( $alias ) ) {
1950 if ( !$alias || (
string)$alias === (
string)
$name ) {
1965 foreach ( $fields
as $alias => $field ) {
1966 if ( is_numeric( $alias ) ) {
1986 $tables, $use_index = [], $ignore_index = [], $join_conds = []
1990 $use_index = (
array)$use_index;
1991 $ignore_index = (
array)$ignore_index;
1992 $join_conds = (
array)$join_conds;
1994 foreach (
$tables as $alias => $table ) {
1995 if ( !is_string( $alias ) ) {
2000 if ( isset( $join_conds[$alias] ) ) {
2001 list( $joinType, $conds ) = $join_conds[$alias];
2002 $tableClause = $joinType;
2004 if ( isset( $use_index[$alias] ) ) {
2007 $tableClause .=
' ' . $use;
2010 if ( isset( $ignore_index[$alias] ) ) {
2012 implode(
',', (
array)$ignore_index[$alias] ) );
2013 if ( $ignore !=
'' ) {
2014 $tableClause .=
' ' . $ignore;
2019 $tableClause .=
' ON (' . $on .
')';
2022 $retJOIN[] = $tableClause;
2023 } elseif ( isset( $use_index[$alias] ) ) {
2027 implode(
',', (
array)$use_index[$alias] )
2030 $ret[] = $tableClause;
2031 } elseif ( isset( $ignore_index[$alias] ) ) {
2035 implode(
',', (
array)$ignore_index[$alias] )
2038 $ret[] = $tableClause;
2042 $ret[] = $tableClause;
2047 $implicitJoins = !empty(
$ret ) ? implode(
',',
$ret ) :
"";
2048 $explicitJoins = !empty( $retJOIN ) ? implode(
' ', $retJOIN ) :
"";
2051 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
2065 if (
$s instanceof
Blob ) {
2068 if (
$s ===
null ) {
2070 } elseif ( is_bool(
$s ) ) {
2073 # This will also quote numeric values. This should be harmless,
2074 # and protects against weird problems that occur when they really
2075 # _are_ strings such as article titles and string->number->string
2076 # conversion is not 1:1.
2091 return '"' . str_replace(
'"',
'""',
$s ) .
'"';
2104 return $name[0] ==
'"' && substr(
$name, -1, 1 ) ==
'"';
2113 return str_replace( [ $escapeChar,
'%',
'_' ],
2114 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
2185 $quotedTable = $this->
tableName( $table );
2187 if ( count(
$rows ) == 0 ) {
2192 if ( !is_array( reset(
$rows ) ) ) {
2198 # Delete rows which collide
2199 if ( $uniqueIndexes ) {
2200 $sql =
"DELETE FROM $quotedTable WHERE ";
2202 foreach ( $uniqueIndexes
as $index ) {
2209 if ( is_array( $index ) ) {
2211 foreach ( $index
as $col ) {
2217 $sql .= $col .
'=' . $this->
addQuotes( $row[$col] );
2220 $sql .= $index .
'=' . $this->
addQuotes( $row[$index] );
2227 # Now insert the row
2246 if ( !is_array( reset(
$rows ) ) ) {
2250 $sql =
"REPLACE INTO $table (" . implode(
',', array_keys(
$rows[0] ) ) .
') VALUES ';
2260 $sql .=
'(' . $this->
makeList( $row ) .
')';
2269 if ( !count(
$rows ) ) {
2273 if ( !is_array( reset(
$rows ) ) ) {
2277 if ( count( $uniqueIndexes ) ) {
2280 foreach ( $uniqueIndexes
as $index ) {
2281 $index = is_array( $index ) ? $index : [ $index ];
2283 foreach ( $index
as $column ) {
2284 $rowKey[$column] = $row[$column];
2286 $clauses[] = $this->
makeList( $rowKey, self::LIST_AND );
2289 $where = [ $this->
makeList( $clauses, self::LIST_OR ) ];
2296 $this->
begin(
$fname, self::TRANSACTION_INTERNAL );
2299 # Update any existing conflicting row(s)
2300 if ( $where !==
false ) {
2305 # Now insert any non-conflicting row(s)
2307 }
catch ( Exception
$e ) {
2320 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2327 $delTable = $this->
tableName( $delTable );
2328 $joinTable = $this->
tableName( $joinTable );
2329 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2330 if ( $conds !=
'*' ) {
2331 $sql .=
'WHERE ' . $this->
makeList( $conds, self::LIST_AND );
2340 $sql =
"SHOW COLUMNS FROM $table LIKE \"$field\";";
2341 $res = $this->
query( $sql, __METHOD__ );
2346 if ( preg_match(
'/\((.*)\)/', $row->Type, $m ) ) {
2355 public function delete( $table, $conds,
$fname = __METHOD__ ) {
2357 throw new DBUnexpectedError( $this, __METHOD__ .
' called with no conditions' );
2361 $sql =
"DELETE FROM $table";
2363 if ( $conds !=
'*' ) {
2364 if ( is_array( $conds ) ) {
2365 $conds = $this->
makeList( $conds, self::LIST_AND );
2367 $sql .=
' WHERE ' . $conds;
2374 $destTable, $srcTable, $varMap, $conds,
2375 $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2377 if ( $this->cliMode ) {
2396 foreach ( $varMap
as $dstColumn => $sourceColumnOrSql ) {
2399 $selectOptions[] =
'FOR UPDATE';
2401 $srcTable, implode(
',', $fields ), $conds,
$fname, $selectOptions, $selectJoinConds
2408 foreach (
$res as $row ) {
2432 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2434 $destTable = $this->
tableName( $destTable );
2436 if ( !is_array( $insertOptions ) ) {
2437 $insertOptions = [ $insertOptions ];
2444 array_values( $varMap ),
2451 $sql =
"INSERT $insertOptions" .
2452 " INTO $destTable (" . implode(
',', array_keys( $varMap ) ) .
') ' .
2478 if ( !is_numeric( $limit ) ) {
2480 "Invalid non-numeric limit passed to limitResult()\n" );
2483 return "$sql LIMIT "
2484 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
2493 $glue = $all ?
') UNION ALL (' :
') UNION (';
2495 return '(' . implode( $glue, $sqls ) .
')';
2499 $table,
$vars,
array $permute_conds, $extra_conds =
'',
$fname = __METHOD__,
2504 foreach ( $permute_conds
as $field => $values ) {
2509 $values = array_unique( $values );
2511 foreach ( $conds
as $cond ) {
2514 $newConds[] = $cond;
2520 $extra_conds = $extra_conds ===
'' ? [] : (
array)$extra_conds;
2524 if ( count( $conds ) === 1 &&
2543 if ( array_key_exists(
'INNER ORDER BY',
$options ) ) {
2546 if ( $limit !==
null && is_numeric( $offset ) && $offset != 0 ) {
2550 $options[
'LIMIT'] = $limit + $offset;
2556 foreach ( $conds
as $cond ) {
2562 if ( $limit !==
null ) {
2563 $sql = $this->
limitResult( $sql, $limit, $offset );
2570 if ( is_array( $cond ) ) {
2571 $cond = $this->
makeList( $cond, self::LIST_AND );
2574 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2578 return "REPLACE({$orig}, {$old}, {$new})";
2612 $args = func_get_args();
2613 $function = array_shift(
$args );
2616 $this->
begin( __METHOD__ );
2623 $retVal = call_user_func_array( $function,
$args );
2628 usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2634 }
while ( --$tries > 0 );
2636 if ( $tries <= 0 ) {
2641 $this->
commit( __METHOD__ );
2648 # Real waits are implemented in the subclass.
2667 if ( !$this->mTrxLevel ) {
2670 $this->mTrxEndCallbacks[] = [ $callback,
$fname ];
2674 $this->mTrxIdleCallbacks[] = [ $callback,
$fname ];
2675 if ( !$this->mTrxLevel ) {
2681 if ( $this->mTrxLevel || $this->
getFlag( self::DBO_TRX ) ) {
2685 $this->mTrxPreCommitCallbacks[] = [ $callback,
$fname ];
2690 call_user_func( $callback );
2692 }
catch ( Exception
$e ) {
2693 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
2701 $this->mTrxRecurringCallbacks[
$name] = $callback;
2703 unset( $this->mTrxRecurringCallbacks[
$name] );
2716 $this->mTrxEndCallbacksSuppressed = $suppress;
2729 if ( $this->mTrxEndCallbacksSuppressed ) {
2733 $autoTrx = $this->
getFlag( self::DBO_TRX );
2737 $callbacks = array_merge(
2738 $this->mTrxIdleCallbacks,
2739 $this->mTrxEndCallbacks
2741 $this->mTrxIdleCallbacks = [];
2742 $this->mTrxEndCallbacks = [];
2743 foreach ( $callbacks
as $callback ) {
2745 list( $phpCallback ) = $callback;
2747 call_user_func_array( $phpCallback, [ $trigger ] );
2749 $this->
setFlag( self::DBO_TRX );
2753 }
catch ( Exception $ex ) {
2754 call_user_func( $this->errorLogger, $ex );
2759 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
2763 }
while ( count( $this->mTrxIdleCallbacks ) );
2765 if (
$e instanceof Exception ) {
2782 $this->mTrxPreCommitCallbacks = [];
2783 foreach ( $callbacks
as $callback ) {
2785 list( $phpCallback ) = $callback;
2786 call_user_func( $phpCallback );
2787 }
catch ( Exception $ex ) {
2788 call_user_func( $this->errorLogger, $ex );
2792 }
while ( count( $this->mTrxPreCommitCallbacks ) );
2794 if (
$e instanceof Exception ) {
2809 if ( $this->mTrxEndCallbacksSuppressed ) {
2816 foreach ( $this->mTrxRecurringCallbacks
as $phpCallback ) {
2818 $phpCallback( $trigger, $this );
2819 }
catch ( Exception $ex ) {
2820 call_user_func( $this->errorLogger, $ex );
2825 if (
$e instanceof Exception ) {
2831 if ( !$this->mTrxLevel ) {
2832 $this->
begin(
$fname, self::TRANSACTION_INTERNAL );
2835 if ( !$this->
getFlag( self::DBO_TRX ) ) {
2836 $this->mTrxAutomaticAtomic =
true;
2840 $this->mTrxAtomicLevels[] =
$fname;
2844 if ( !$this->mTrxLevel ) {
2845 throw new DBUnexpectedError( $this,
"No atomic transaction is open (got $fname)." );
2847 if ( !$this->mTrxAtomicLevels ||
2848 array_pop( $this->mTrxAtomicLevels ) !==
$fname
2850 throw new DBUnexpectedError( $this,
"Invalid atomic section ended (got $fname)." );
2853 if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2861 $res = call_user_func_array( $callback, [ $this,
$fname ] );
2862 }
catch ( Exception
$e ) {
2871 final public function begin(
$fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2873 if ( $this->mTrxLevel ) {
2874 if ( $this->mTrxAtomicLevels ) {
2875 $levels = implode(
', ', $this->mTrxAtomicLevels );
2876 $msg =
"$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2878 } elseif ( !$this->mTrxAutomatic ) {
2879 $msg =
"$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2883 $msg =
"$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2884 $this->queryLogger->error( $msg );
2887 } elseif ( $this->
getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2889 $msg =
"$fname: Implicit transaction expected (DBO_TRX set).";
2890 $this->queryLogger->error( $msg );
2898 $this->mTrxTimestamp = microtime(
true );
2899 $this->mTrxFname =
$fname;
2900 $this->mTrxDoneWrites =
false;
2901 $this->mTrxAutomaticAtomic =
false;
2902 $this->mTrxAtomicLevels = [];
2903 $this->mTrxShortId = sprintf(
'%06x', mt_rand( 0, 0xffffff ) );
2904 $this->mTrxWriteDuration = 0.0;
2905 $this->mTrxWriteQueryCount = 0;
2906 $this->mTrxWriteAffectedRows = 0;
2907 $this->mTrxWriteAdjDuration = 0.0;
2908 $this->mTrxWriteAdjQueryCount = 0;
2909 $this->mTrxWriteCallers = [];
2914 $this->mTrxReplicaLag =
$status[
'lag'] + ( microtime(
true ) -
$status[
'since'] );
2918 $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
2929 $this->mTrxLevel = 1;
2933 if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2935 $levels = implode(
', ', $this->mTrxAtomicLevels );
2938 "$fname: Got COMMIT while atomic sections $levels are still open."
2942 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2943 if ( !$this->mTrxLevel ) {
2945 } elseif ( !$this->mTrxAutomatic ) {
2948 "$fname: Flushing an explicit transaction, getting out of sync."
2952 if ( !$this->mTrxLevel ) {
2953 $this->queryLogger->error(
2954 "$fname: No transaction to commit, something got out of sync." );
2956 } elseif ( $this->mTrxAutomatic ) {
2958 $msg =
"$fname: Explicit commit of implicit transaction.";
2959 $this->queryLogger->error( $msg );
2970 if ( $this->mTrxDoneWrites ) {
2971 $this->mLastWriteTime = microtime(
true );
2972 $this->trxProfiler->transactionWritingOut(
2977 $this->mTrxWriteAffectedRows
2992 if ( $this->mTrxLevel ) {
2994 $this->mTrxLevel = 0;
2999 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3000 if ( !$this->mTrxLevel ) {
3004 if ( !$this->mTrxLevel ) {
3005 $this->queryLogger->error(
3006 "$fname: No transaction to rollback, something got out of sync." );
3008 } elseif ( $this->
getFlag( self::DBO_TRX ) ) {
3011 "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
3020 $this->mTrxAtomicLevels = [];
3021 if ( $this->mTrxDoneWrites ) {
3022 $this->trxProfiler->transactionWritingOut(
3029 $this->mTrxIdleCallbacks = [];
3030 $this->mTrxPreCommitCallbacks = [];
3042 if ( $this->mTrxLevel ) {
3043 # Disconnects cause rollback anyway, so ignore those errors
3044 $ignoreErrors =
true;
3045 $this->
query(
'ROLLBACK',
$fname, $ignoreErrors );
3046 $this->mTrxLevel = 0;
3056 "$fname: Cannot flush snapshot because writes are pending ($fnames)."
3068 $oldName, $newName, $temporary =
false,
$fname = __METHOD__
3070 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
3074 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
3078 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
3082 $t =
new ConvertibleTimestamp( $ts );
3084 return $t->getTimestamp( TS_MW );
3088 if ( is_null( $ts ) ) {
3113 } elseif (
$result ===
true ) {
3121 public function ping( &$rtt =
null ) {
3123 if ( $this->
isOpen() && ( microtime(
true ) - $this->lastPing ) < self::PING_TTL ) {
3124 if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
3131 $this->
clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
3132 $ok = ( $this->
query( self::PING_QUERY, __METHOD__,
true ) !==
false );
3149 $this->mOpened =
false;
3150 $this->mConn =
false;
3152 $this->
open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3153 $this->lastPing = microtime(
true );
3178 return $this->mTrxLevel
3192 'since' => microtime(
true )
3215 $res = [
'lag' => 0,
'since' => INF,
'pending' =>
false ];
3216 foreach ( func_get_args()
as $db ) {
3218 $status = $db->getSessionLagStatus();
3219 if (
$status[
'lag'] ===
false ) {
3220 $res[
'lag'] =
false;
3221 } elseif (
$res[
'lag'] !==
false ) {
3225 $res[
'pending'] =
$res[
'pending'] ?: $db->writesPending();
3244 if ( $b instanceof
Blob ) {
3255 callable $lineCallback =
null,
3256 callable $resultCallback =
null,
3258 callable $inputCallback =
null
3260 MediaWiki\suppressWarnings();
3261 $fp = fopen( $filename,
'r' );
3262 MediaWiki\restoreWarnings();
3264 if (
false === $fp ) {
3265 throw new RuntimeException(
"Could not open \"{$filename}\".\n" );
3269 $fname = __METHOD__ .
"( $filename )";
3274 $fp, $lineCallback, $resultCallback,
$fname, $inputCallback );
3275 }
catch ( Exception
$e ) {
3286 $this->mSchemaVars =
$vars;
3291 callable $lineCallback =
null,
3292 callable $resultCallback =
null,
3294 callable $inputCallback =
null
3298 while ( !feof( $fp ) ) {
3299 if ( $lineCallback ) {
3300 call_user_func( $lineCallback );
3303 $line = trim( fgets( $fp ) );
3305 if (
$line ==
'' ) {
3321 if ( $done || feof( $fp ) ) {
3324 if ( !$inputCallback || call_user_func( $inputCallback, $cmd ) ) {
3327 if ( $resultCallback ) {
3328 call_user_func( $resultCallback,
$res, $this );
3331 if (
false ===
$res ) {
3334 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3352 if ( $this->delimiter ) {
3354 $newLine = preg_replace(
3355 '/' . preg_quote( $this->delimiter,
'/' ) .
'$/',
'', $newLine );
3356 if ( $newLine != $prev ) {
3386 return preg_replace_callback(
3388 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3389 \'\{\$ (\w+) }\' | # 3. addQuotes
3390 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
3391 /\*\$ (\w+) \*/ # 5. leave unencoded
3396 if ( isset( $m[1] ) && $m[1] !==
'' ) {
3397 if ( $m[1] ===
'i' ) {
3402 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3],
$vars ) ) {
3404 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4],
$vars ) ) {
3406 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5],
$vars ) ) {
3407 return $vars[$m[5]];
3423 if ( $this->mSchemaVars ) {
3446 public function lock( $lockName, $method, $timeout = 5 ) {
3447 $this->mNamedLocksHeld[$lockName] = 1;
3452 public function unlock( $lockName, $method ) {
3453 unset( $this->mNamedLocksHeld[$lockName] );
3464 "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
3468 if ( !$this->
lock( $lockKey,
$fname, $timeout ) ) {
3472 $unlocker =
new ScopedCallback(
function ()
use ( $lockKey,
$fname ) {
3503 throw new DBUnexpectedError( $this,
"Transaction writes or callbacks still pending." );
3552 public function dropTable( $tableName, $fName = __METHOD__ ) {
3553 if ( !$this->
tableExists( $tableName, $fName ) ) {
3556 $sql =
"DROP TABLE " . $this->
tableName( $tableName ) .
" CASCADE";
3558 return $this->
query( $sql, $fName );
3566 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() )
3572 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() ) {
3576 return ConvertibleTimestamp::convert( $format, $expiry );
3591 $reason = $this->
getLBInfo(
'readOnlyReason' );
3593 return is_string( $reason ) ? $reason :
false;
3597 $this->tableAliases = $aliases;
3620 if ( !$this->mConn ) {
3623 'DB connection was already closed or the connection dropped.'
3643 $this->connLogger->warning(
3644 "Cloning " . static::class .
" is not recomended; forking connection:\n" .
3645 (
new RuntimeException() )->getTraceAsString()
3650 $this->mOpened =
false;
3651 $this->mConn =
false;
3652 $this->mTrxEndCallbacks = [];
3654 $this->
open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3655 $this->lastPing = microtime(
true );
3665 throw new RuntimeException(
'Database serialization may cause problems, since ' .
3666 'the connection is not restored on wakeup.' );
3673 if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3674 trigger_error(
"Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3678 if ( $danglingWriters ) {
3679 $fnames = implode(
', ', $danglingWriters );
3680 trigger_error(
"DB transaction writes or callbacks still pending ($fnames)." );
3683 if ( $this->mConn ) {
3686 \MediaWiki\suppressWarnings();
3688 \MediaWiki\restoreWarnings();
3689 $this->mConn =
false;
3690 $this->mOpened =
false;
3695class_alias( Database::class,
'DatabaseBase' );
3696class_alias( Database::class,
'Database' );
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
if(!defined( 'MEDIAWIKI')) $fname
This file is not a valid entry point, perform no further processing unless MEDIAWIKI is defined.
interface is intended to be more or less compatible with the PHP memcached client.
Simple store for keeping values in an associative array for the current process.
Class to handle database/prefix specification for IDatabase domains.
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
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
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
the array() calling protocol came about after MediaWiki 1.4rc1.
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
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
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account incomplete not yet checked for validity & $retval
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
presenting them properly to the user as errors is done by the caller return true use this to change the list i e rollback
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
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
Allows to change the fields on the form that will be generated $name
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and insert
processing should stop and the error should be shown to the user * false
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
returning false will NOT prevent logging $e
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Advanced database interface for IDatabase handles that include maintenance methods.