28use Psr\Log\LoggerAwareInterface;
29use Psr\Log\LoggerInterface;
30use Psr\Log\NullLogger;
31use Wikimedia\ScopedCallback;
32use Wikimedia\Timestamp\ConvertibleTimestamp;
37use InvalidArgumentException;
38use UnexpectedValueException;
65 const ATTR_DB_LEVEL_LOCKING =
'db-level-locking';
68 const NEW_UNCONNECTED = 0;
70 const NEW_CONNECTED = 1;
283 const STATUS_TRX_ERROR = 1;
285 const STATUS_TRX_OK = 2;
287 const STATUS_TRX_NONE = 3;
294 foreach ( [
'host',
'user',
'password',
'dbname' ] as $name ) {
298 $this->schema =
$params[
'schema'];
301 $this->cliMode =
$params[
'cliMode'];
303 $this->agent = str_replace(
'/',
'-',
$params[
'agent'] );
305 $this->
flags = $params[
'flags'];
306 if ( $this->
flags & self::DBO_DEFAULT ) {
307 if ( $this->cliMode ) {
316 $this->sessionVars =
$params[
'variables'];
318 $this->srvCache = isset(
$params[
'srvCache'] )
322 $this->profiler =
$params[
'profiler'];
323 $this->trxProfiler =
$params[
'trxProfiler'];
324 $this->connLogger =
$params[
'connLogger'];
325 $this->queryLogger =
$params[
'queryLogger'];
326 $this->errorLogger =
$params[
'errorLogger'];
327 $this->deprecationLogger =
$params[
'deprecationLogger'];
329 if ( isset(
$params[
'nonNativeInsertSelectBatchSize'] ) ) {
330 $this->nonNativeInsertSelectBatchSize =
$params[
'nonNativeInsertSelectBatchSize'];
347 throw new LogicException( __METHOD__ .
': already connected.' );
352 if ( $this->dbName !=
'' ) {
366 if ( strlen( $this->connectionParams[
'user'] ) ) {
368 $this->connectionParams[
'host'],
369 $this->connectionParams[
'user'],
370 $this->connectionParams[
'password'],
371 $this->connectionParams[
'dbname']
374 throw new InvalidArgumentException(
"No database user provided." );
422 final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
423 $class =
self::getClass( $dbType, isset( $p[
'driver'] ) ? $p[
'driver'] : null );
425 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
427 $p[
'host'] = isset( $p[
'host'] ) ? $p[
'host'] :
false;
428 $p[
'user'] = isset( $p[
'user'] ) ? $p[
'user'] :
false;
429 $p[
'password'] = isset( $p[
'password'] ) ? $p[
'password'] :
false;
430 $p[
'dbname'] = isset( $p[
'dbname'] ) ? $p[
'dbname'] :
false;
431 $p[
'flags'] = isset( $p[
'flags'] ) ? $p[
'flags'] : 0;
432 $p[
'variables'] = isset( $p[
'variables'] ) ? $p[
'variables'] : [];
433 $p[
'tablePrefix'] = isset( $p[
'tablePrefix'] ) ? $p[
'tablePrefix'] :
'';
434 $p[
'schema'] = isset( $p[
'schema'] ) ? $p[
'schema'] :
'';
435 $p[
'cliMode'] = isset( $p[
'cliMode'] )
437 : ( PHP_SAPI ===
'cli' || PHP_SAPI ===
'phpdbg' );
438 $p[
'agent'] = isset( $p[
'agent'] ) ? $p[
'agent'] :
'';
439 if ( !isset( $p[
'connLogger'] ) ) {
440 $p[
'connLogger'] =
new NullLogger();
442 if ( !isset( $p[
'queryLogger'] ) ) {
443 $p[
'queryLogger'] =
new NullLogger();
445 $p[
'profiler'] = isset( $p[
'profiler'] ) ? $p[
'profiler'] :
null;
446 if ( !isset( $p[
'trxProfiler'] ) ) {
449 if ( !isset( $p[
'errorLogger'] ) ) {
450 $p[
'errorLogger'] =
function ( Exception
$e ) {
451 trigger_error( get_class(
$e ) .
': ' .
$e->getMessage(), E_USER_WARNING );
454 if ( !isset( $p[
'deprecationLogger'] ) ) {
455 $p[
'deprecationLogger'] =
function ( $msg ) {
456 trigger_error( $msg, E_USER_DEPRECATED );
461 $conn =
new $class( $p );
462 if ( $connect == self::NEW_CONNECTED ) {
463 $conn->initConnection();
480 static $defaults = [ self::ATTR_DB_LEVEL_LOCKING =>
false ];
484 return call_user_func( [ $class,
'getAttributes' ] ) + $defaults;
493 private static function getClass( $dbType, $driver =
null ) {
500 static $builtinTypes = [
501 'mssql' => DatabaseMssql::class,
502 'mysql' => [
'mysqli' => DatabaseMysqli::class ],
503 'sqlite' => DatabaseSqlite::class,
504 'postgres' => DatabasePostgres::class,
507 $dbType = strtolower( $dbType );
510 if ( isset( $builtinTypes[$dbType] ) ) {
511 $possibleDrivers = $builtinTypes[$dbType];
512 if ( is_string( $possibleDrivers ) ) {
513 $class = $possibleDrivers;
515 if ( (
string)$driver !==
'' ) {
516 if ( !isset( $possibleDrivers[$driver] ) ) {
517 throw new InvalidArgumentException( __METHOD__ .
518 " type '$dbType' does not support driver '{$driver}'" );
520 $class = $possibleDrivers[$driver];
523 foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
524 if ( extension_loaded( $posDriver ) ) {
525 $class = $possibleClass;
532 $class =
'Database' . ucfirst( $dbType );
535 if ( $class ===
false ) {
536 throw new InvalidArgumentException( __METHOD__ .
537 " no viable database extension found for type '$dbType'" );
559 $this->queryLogger = $logger;
571 : $this->
setFlag( self::DBO_NOBUFFER );
595 if ( $prefix !==
null ) {
597 $this->currentDomain = ( $this->dbName !=
'' )
615 if ( is_null( $name ) ) {
618 if ( array_key_exists( $name, $this->lbInfo ) ) {
619 return $this->lbInfo[
$name];
627 if ( is_null(
$value ) ) {
628 $this->lbInfo =
$name;
635 $this->lazyMasterHandle =
$conn;
664 return $this->lastWriteTime ?:
false;
673 $this->trxDoneWrites ||
674 $this->trxIdleCallbacks ||
675 $this->trxPreCommitCallbacks ||
685 if ( $this->
getFlag( self::DBO_TRX ) ) {
688 return is_string( $id ) ? $id :
null;
697 } elseif ( !$this->trxDoneWrites ) {
702 case self::ESTIMATE_DB_APPLY:
704 $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
705 $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
708 $applyTime += self::TINY_WRITE_SEC * $omitted;
717 return $this->
trxLevel ? $this->trxWriteCallers : [];
737 $this->trxIdleCallbacks,
738 $this->trxPreCommitCallbacks,
739 $this->trxEndCallbacks
741 foreach ( $callbacks as $callback ) {
742 $fnames[] = $callback[1];
753 return array_reduce( $this->trxAtomicLevels,
function ( $accum, $v ) {
754 return $accum ===
null ? $v[0] :
"$accum, " . $v[0];
762 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
763 if ( ( $flag & self::DBO_IGNORE ) ) {
764 throw new UnexpectedValueException(
"Modifying DBO_IGNORE is not allowed." );
767 if ( $remember === self::REMEMBER_PRIOR ) {
768 array_push( $this->priorFlags, $this->
flags );
770 $this->
flags |= $flag;
773 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
774 if ( ( $flag & self::DBO_IGNORE ) ) {
775 throw new UnexpectedValueException(
"Modifying DBO_IGNORE is not allowed." );
778 if ( $remember === self::REMEMBER_PRIOR ) {
779 array_push( $this->priorFlags, $this->
flags );
781 $this->
flags &= ~$flag;
785 if ( !$this->priorFlags ) {
789 if ( $state === self::RESTORE_INITIAL ) {
790 $this->
flags = reset( $this->priorFlags );
791 $this->priorFlags = [];
793 $this->
flags = array_pop( $this->priorFlags );
798 return !!( $this->
flags & $flag );
811 return $this->currentDomain->getId();
839 $this->phpError =
false;
840 $this->htmlErrors = ini_set(
'html_errors',
'0' );
841 set_error_handler( [ $this,
'connectionErrorLogger' ] );
850 restore_error_handler();
851 if ( $this->htmlErrors !==
false ) {
852 ini_set(
'html_errors', $this->htmlErrors );
862 if ( $this->phpError ) {
863 $error = preg_replace(
'!\[<a.*</a>\]!',
'', $this->phpError );
864 $error = preg_replace(
'!^.*?:\s?(.*)$!',
'$1', $error );
880 $this->phpError = $errstr;
892 'db_server' => $this->server,
893 'db_name' => $this->dbName,
894 'db_user' => $this->user,
906 if ( $this->trxAtomicLevels ) {
911 __METHOD__ .
": atomic sections $levels are still open."
913 } elseif ( $this->trxAutomatic ) {
919 ": mass commit/rollback of peer transaction required (DBO_TRX set)."
924 $this->queryLogger->warning(
925 __METHOD__ .
": writes or callbacks still pending.",
926 [
'trace' => (
new RuntimeException() )->getTraceAsString() ]
930 if ( $this->trxEndCallbacksSuppressed ) {
933 __METHOD__ .
': callbacks are suppressed; cannot properly commit.'
938 if ( $this->
trxStatus === self::STATUS_TRX_OK && !$exception ) {
941 $this->trxAutomatic ? self::FLUSHING_INTERNAL : self::FLUSHING_ONE
944 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
955 $this->opened =
false;
958 if ( $exception instanceof Exception ) {
964 $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks
966 throw new RuntimeException(
967 "Transaction callbacks are still pending:\n" .
1027 '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
1035 return preg_match(
'/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) :
null;
1050 [
'BEGIN',
'COMMIT',
'ROLLBACK',
'SHOW',
'SET',
'CREATE',
'ALTER' ],
1061 '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
1065 $this->sessionTempTables[
$matches[1]] = 1;
1068 } elseif ( preg_match(
1069 '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
1073 $isTemp = isset( $this->sessionTempTables[
$matches[1]] );
1074 unset( $this->sessionTempTables[
$matches[1]] );
1077 } elseif ( preg_match(
1078 '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
1082 return isset( $this->sessionTempTables[
$matches[1]] );
1083 } elseif ( preg_match(
1084 '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
1088 return isset( $this->sessionTempTables[
$matches[1]] );
1094 public function query( $sql,
$fname = __METHOD__, $tempIgnore =
false ) {
1097 # Avoid fatals if close() was called
1107 $isNonTempWrite =
false;
1111 if ( $this->
getLBInfo(
'replica' ) ===
true ) {
1114 'Write operations are not allowed on replica database connections.'
1117 # In theory, non-persistent writes are allowed in read-only mode, but due to things
1120 if ( $reason !==
false ) {
1123 # Set a flag indicating that writes have been done
1124 $this->lastWriteTime = microtime(
true );
1127 # Add trace comment to the begin of the sql string, right after the operator.
1128 # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
1129 $commentedSql = preg_replace( '/\s|$/', " ", $sql, 1 );
1131 # Start implicit transactions that wrap the request if DBO_TRX is enabled
1132 if ( !$this->trxLevel && $this->getFlag( self::DBO_TRX )
1133 && $this->isTransactableQuery( $sql )
1135 $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
1136 $this->trxAutomatic = true;
1139 # Keep track of whether the transaction has write queries pending
1140 if ( $this->trxLevel && !$this->trxDoneWrites && $isWrite ) {
1141 $this->trxDoneWrites = true;
1142 $this->trxProfiler->transactionWritingIn(
1143 $this->server, $this->dbName, $this->trxShortId );
1146 if ( $this->getFlag( self::DBO_DEBUG ) ) {
1147 $this->queryLogger->debug( "{$this->dbName} {$commentedSql}" );
1150 # Send the query to the server and fetch any corresponding errors
1151 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
1152 $lastError = $this->lastError();
1153 $lastErrno = $this->lastErrno();
1155 # Try reconnecting if the connection was lost
1156 if ( $ret === false && $this->wasConnectionLoss() ) {
1157 # Check if any meaningful session state was lost
1158 $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
1159 # Update session state tracking and try to restore the connection
1160 $reconnected = $this->replaceLostConnection( __METHOD__ );
1161 # Silently resend the query to the server if it is safe and possible
1162 if ( $reconnected && $recoverable ) {
1163 $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
1164 $lastError = $this->lastError();
1165 $lastErrno = $this->lastErrno();
1167 if ( $ret === false && $this->wasConnectionLoss() ) {
1168 # Query probably causes disconnects; reconnect and do not re-run it
1169 $this->replaceLostConnection( __METHOD__ );
1174 if ( $ret === false ) {
1175 if ( $this->trxLevel ) {
1176 if ( !$this->wasKnownStatementRollbackError() ) {
1177 # Either the query was aborted or all queries after BEGIN where aborted.
1178 if ( $this->explicitTrxActive() || $priorWritesPending ) {
1179 # In the first case, the only options going forward are (a) ROLLBACK, or
1180 # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1181 # option is ROLLBACK, since the snapshots would have been released.
1182 $this->trxStatus = self::STATUS_TRX_ERROR;
1183 $this->trxStatusCause =
1184 $this->makeQueryException( $lastError, $lastErrno, $sql, $fname );
1185 $tempIgnore = false; // cannot recover
1187 # Nothing prior was there to lose from the transaction,
1188 # so just roll it back.
1189 $this->rollback( __METHOD__ . " ($fname)", self::FLUSHING_INTERNAL );
1191 $this->trxStatusIgnoredCause = null;
1193 # We're ignoring an error that caused just the current query to be aborted.
1194 # But log the cause so we can log a deprecation notice if a
1195 # caller actually does ignore it.
1196 $this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
1200 $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
1203 return $this->resultObject( $ret );
1216 private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
1217 $isMaster = !is_null( $this->getLBInfo( 'master' ) );
1218 # generalizeSQL() will probably cut down the query to reasonable
1219 # logging size most of the time. The substr is really just a sanity check.
1221 $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1223 $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
1226 # Include query transaction state
1227 $queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : "";
1229 $startTime = microtime( true );
1230 if ( $this->profiler ) {
1231 call_user_func( [ $this->profiler, 'profileIn' ], $queryProf );
1233 $this->affectedRowCount = null;
1234 $ret = $this->doQuery( $commentedSql );
1235 $this->affectedRowCount = $this->affectedRows();
1236 if ( $this->profiler ) {
1237 call_user_func( [ $this->profiler, 'profileOut' ], $queryProf );
1239 $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
1241 unset( $queryProfSection ); // profile out (if set)
1243 if ( $ret !== false ) {
1244 $this->lastPing = $startTime;
1245 if ( $isWrite && $this->trxLevel ) {
1246 $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
1247 $this->trxWriteCallers[] = $fname;
1251 if ( $sql === self::PING_QUERY ) {
1252 $this->rttEstimate = $queryRuntime;
1255 $this->trxProfiler->recordQueryCompletion(
1256 $queryProf, $startTime, $isWrite, $this->affectedRows()
1258 $this->queryLogger->debug( $sql, [
1260 'master' => $isMaster,
1261 'runtime' => $queryRuntime,
1279 private function updateTrxWriteQueryTime( $sql, $runtime, $affected ) {
1280 // Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1281 $indicativeOfReplicaRuntime = true;
1282 if ( $runtime > self::SLOW_WRITE_SEC ) {
1283 $verb = $this->getQueryVerb( $sql );
1284 // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1285 if ( $verb === 'INSERT' ) {
1286 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1287 } elseif ( $verb === 'REPLACE' ) {
1288 $indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1292 $this->trxWriteDuration += $runtime;
1293 $this->trxWriteQueryCount += 1;
1294 $this->trxWriteAffectedRows += $affected;
1295 if ( $indicativeOfReplicaRuntime ) {
1296 $this->trxWriteAdjDuration += $runtime;
1297 $this->trxWriteAdjQueryCount += 1;
1306 private function assertTransactionStatus( $sql, $fname ) {
1307 if ( $this->getQueryVerb( $sql ) === 'ROLLBACK' ) { // transaction/savepoint
1311 if ( $this->trxStatus < self::STATUS_TRX_OK ) {
1312 throw new DBTransactionStateError(
1314 "Cannot execute query from $fname while transaction status is ERROR.",
1316 $this->trxStatusCause
1318 } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1319 list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause;
1320 call_user_func( $this->deprecationLogger,
1321 "Caller from $fname ignored an error originally raised from $iFname: " .
1322 "[$iLastErrno] $iLastError"
1324 $this->trxStatusIgnoredCause = null;
1338 private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1339 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1340 # Dropped connections also mean that named locks are automatically released.
1341 # Only allow error suppression in autocommit mode or when the lost transaction
1342 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1343 if ( $this->namedLocksHeld ) {
1344 return false; // possible critical section violation
1345 } elseif ( $this->sessionTempTables ) {
1346 return false; // tables might be queried latter
1347 } elseif ( $sql === 'COMMIT' ) {
1348 return !$priorWritesPending; // nothing written anyway? (T127428)
1349 } elseif ( $sql === 'ROLLBACK' ) {
1350 return true; // transaction lost...which is also what was requested :)
1351 } elseif ( $this->explicitTrxActive() ) {
1352 return false; // don't drop atomocity and explicit snapshots
1353 } elseif ( $priorWritesPending ) {
1354 return false; // prior writes lost from implicit transaction
1363 private function handleSessionLoss() {
1364 // Clean up tracking of session-level things...
1365 // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
1366 // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1367 $this->sessionTempTables = [];
1368 // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1369 // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1370 $this->namedLocksHeld = [];
1371 // Session loss implies transaction loss
1372 $this->handleTransactionLoss();
1378 private function handleTransactionLoss() {
1379 $this->trxLevel = 0;
1380 $this->trxAtomicCounter = 0;
1381 $this->trxIdleCallbacks = []; // T67263; transaction already lost
1382 $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
1384 // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
1385 // If callback suppression is set then the array will remain unhandled.
1386 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1387 } catch ( Exception $ex ) {
1388 // Already logged; move on...
1391 // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
1392 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1393 } catch ( Exception $ex ) {
1394 // Already logged; move on...
1408 protected function wasQueryTimeout( $error, $errno ) {
1423 public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1424 if ( $tempIgnore ) {
1425 $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1427 $exception = $this->makeQueryException( $error, $errno, $sql, $fname );
1440 private function makeQueryException( $error, $errno, $sql, $fname ) {
1441 $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1442 $this->queryLogger->error(
1443 "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1444 $this->getLogContext( [
1445 'method' => __METHOD__,
1448 'sql1line' => $sql1line,
1452 $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1453 $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno );
1454 if ( $wasQueryTimeout ) {
1455 $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1457 $e = new DBQueryError( $this, $error, $errno, $sql, $fname );
1463 public function freeResult( $res ) {
1466 public function selectField(
1467 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1469 if ( $var === '*' ) { // sanity
1470 throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1473 if ( !is_array( $options ) ) {
1474 $options = [ $options ];
1477 $options['LIMIT'] = 1;
1479 $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1480 if ( $res === false || !$this->numRows( $res ) ) {
1484 $row = $this->fetchRow( $res );
1486 if ( $row !== false ) {
1487 return reset( $row );
1493 public function selectFieldValues(
1494 $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1496 if ( $var === '*' ) { // sanity
1497 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1498 } elseif ( !is_string( $var ) ) { // sanity
1499 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1502 if ( !is_array( $options ) ) {
1503 $options = [ $options ];
1506 $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1507 if ( $res === false ) {
1512 foreach ( $res as $row ) {
1513 $values[] = $row->value;
1528 protected function makeSelectOptions( $options ) {
1529 $preLimitTail = $postLimitTail = '';
1534 foreach ( $options as $key => $option ) {
1535 if ( is_numeric( $key ) ) {
1536 $noKeyOptions[$option] = true;
1540 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1542 $preLimitTail .= $this->makeOrderBy( $options );
1544 if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1545 $postLimitTail .= ' FOR UPDATE';
1548 if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1549 $postLimitTail .= ' LOCK IN SHARE MODE';
1552 if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1553 $startOpts .= 'DISTINCT';
1556 # Various MySQL extensions
1557 if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1558 $startOpts .= ' /*! STRAIGHT_JOIN */';
1561 if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1562 $startOpts .= ' HIGH_PRIORITY';
1565 if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1566 $startOpts .= ' SQL_BIG_RESULT';
1569 if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1570 $startOpts .= ' SQL_BUFFER_RESULT';
1573 if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1574 $startOpts .= ' SQL_SMALL_RESULT';
1577 if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1578 $startOpts .= ' SQL_CALC_FOUND_ROWS';
1581 if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1582 $startOpts .= ' SQL_CACHE';
1585 if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1586 $startOpts .= ' SQL_NO_CACHE';
1589 if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1590 $useIndex = $this->useIndexClause( $options['USE INDEX'] );
1594 if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1595 $ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1600 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1611 protected function makeGroupByWithHaving( $options ) {
1613 if ( isset( $options['GROUP BY'] ) ) {
1614 $gb = is_array( $options['GROUP BY'] )
1615 ? implode( ',', $options['GROUP BY'] )
1616 : $options['GROUP BY'];
1617 $sql .= ' GROUP BY ' . $gb;
1619 if ( isset( $options['HAVING'] ) ) {
1620 $having = is_array( $options['HAVING'] )
1621 ? $this->makeList( $options['HAVING'], self::LIST_AND )
1622 : $options['HAVING'];
1623 $sql .= ' HAVING ' . $having;
1637 protected function makeOrderBy( $options ) {
1638 if ( isset( $options['ORDER BY'] ) ) {
1639 $ob = is_array( $options['ORDER BY'] )
1640 ? implode( ',', $options['ORDER BY'] )
1641 : $options['ORDER BY'];
1643 return ' ORDER BY ' . $ob;
1649 public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1650 $options = [], $join_conds = [] ) {
1651 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1653 return $this->query( $sql, $fname );
1656 public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1657 $options = [], $join_conds = []
1659 if ( is_array( $vars ) ) {
1660 $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1663 $options = (array)$options;
1664 $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1665 ? $options['USE INDEX']
1668 isset( $options['IGNORE INDEX'] ) &&
1669 is_array( $options['IGNORE INDEX'] )
1671 ? $options['IGNORE INDEX']
1674 if ( is_array( $table ) ) {
1676 $this->tableNamesWithIndexClauseOrJOIN(
1677 $table, $useIndexes, $ignoreIndexes, $join_conds );
1678 } elseif ( $table != '' ) {
1680 $this->tableNamesWithIndexClauseOrJOIN(
1681 [ $table ], $useIndexes, $ignoreIndexes, [] );
1686 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1687 $this->makeSelectOptions( $options );
1689 if ( is_array( $conds ) ) {
1690 $conds = $this->makeList( $conds, self::LIST_AND );
1693 if ( $conds === null || $conds === false ) {
1694 $this->queryLogger->warning(
1698 . ' with incorrect parameters: $conds must be a string or an array'
1703 if ( $conds === '' ) {
1704 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
1705 } elseif ( is_string( $conds ) ) {
1706 $sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
1707 "WHERE $conds $preLimitTail";
1709 throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1712 if ( isset( $options['LIMIT'] ) ) {
1713 $sql = $this->limitResult( $sql, $options['LIMIT'],
1714 isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1716 $sql = "$sql $postLimitTail";
1718 if ( isset( $options['EXPLAIN'] ) ) {
1719 $sql = 'EXPLAIN ' . $sql;
1725 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1726 $options = [], $join_conds = []
1728 $options = (array)$options;
1729 $options['LIMIT'] = 1;
1730 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1732 if ( $res === false ) {
1736 if ( !$this->numRows( $res ) ) {
1740 $obj = $this->fetchObject( $res );
1745 public function estimateRowCount(
1746 $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1748 $conds = $this->normalizeConditions( $conds, $fname );
1749 $column = $this->extractSingleFieldFromList( $var );
1750 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1751 $conds[] = "$column IS NOT NULL";
1754 $res = $this->select(
1755 $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
1757 $row = $res ? $this->fetchRow( $res ) : [];
1759 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1762 public function selectRowCount(
1763 $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1765 $conds = $this->normalizeConditions( $conds, $fname );
1766 $column = $this->extractSingleFieldFromList( $var );
1767 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1768 $conds[] = "$column IS NOT NULL";
1771 $res = $this->select(
1773 'tmp_count' => $this->buildSelectSubquery(
1782 [ 'rowcount' => 'COUNT(*)' ],
1786 $row = $res ? $this->fetchRow( $res ) : [];
1788 return isset( $row['rowcount'] ) ? (int)$row['rowcount'] : 0;
1796 final protected function normalizeConditions( $conds, $fname ) {
1797 if ( $conds === null || $conds === false ) {
1798 $this->queryLogger->warning(
1802 . ' with incorrect parameters: $conds must be a string or an array'
1807 if ( !is_array( $conds ) ) {
1808 $conds = ( $conds === '' ) ? [] : [ $conds ];
1819 final protected function extractSingleFieldFromList( $var ) {
1820 if ( is_array( $var ) ) {
1823 } elseif ( count( $var ) == 1 ) {
1824 $column = isset( $var[0] ) ? $var[0] : reset( $var );
1826 throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
1843 protected static function generalizeSQL( $sql ) {
1844 # This does the same as the regexp below would do, but in such a way
1845 # as to avoid crashing php on some large strings.
1846 # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1848 $sql = str_replace(
"\\\\",
'', $sql );
1849 $sql = str_replace(
"\\'",
'', $sql );
1850 $sql = str_replace(
"\\\"",
'', $sql );
1851 $sql = preg_replace(
"/'.*'/s",
"'X'", $sql );
1852 $sql = preg_replace(
'/".*"/s',
"'X'", $sql );
1854 # All newlines, tabs, etc replaced by single space
1855 $sql = preg_replace(
'/\s+/',
' ', $sql );
1858 # except the ones surrounded by characters, e.g. l10n
1859 $sql = preg_replace(
'/-?\d+(,-?\d+)+/s',
'N,...,N', $sql );
1860 $sql = preg_replace(
'/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s',
'N', $sql );
1866 $info = $this->
fieldInfo( $table, $field );
1877 if ( is_null( $info ) ) {
1880 return $info !==
false;
1885 $tableRaw = $this->
tableName( $table,
'raw' );
1886 if ( isset( $this->sessionTempTables[$tableRaw] ) ) {
1891 $ignoreErrors =
true;
1892 $res = $this->
query(
"SELECT 1 FROM $table LIMIT 1",
$fname, $ignoreErrors );
1898 $indexInfo = $this->
indexInfo( $table, $index );
1900 if ( !$indexInfo ) {
1904 return !$indexInfo[0]->Non_unique;
1918 # No rows to insert, easy just return now
1919 if ( !count( $a ) ) {
1930 if ( isset(
$options[
'fileHandle'] ) ) {
1935 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1937 $keys = array_keys( $a[0] );
1940 $keys = array_keys( $a );
1944 " INTO $table (" . implode(
',',
$keys ) .
') VALUES ';
1948 foreach ( $a as $row ) {
1954 $sql .=
'(' . $this->
makeList( $row ) .
')';
1957 $sql .=
'(' . $this->
makeList( $a ) .
')';
1960 if ( $fh !==
null &&
false === fwrite( $fh, $sql ) ) {
1962 } elseif ( $fh !==
null ) {
1982 if ( in_array(
'IGNORE',
$options ) ) {
1998 return implode(
' ', $opts );
2004 $sql =
"UPDATE $opts $table SET " . $this->
makeList( $values, self::LIST_SET );
2006 if ( $conds !== [] && $conds !==
'*' ) {
2007 $sql .=
" WHERE " . $this->
makeList( $conds, self::LIST_AND );
2013 public function makeList( $a, $mode = self::LIST_COMMA ) {
2014 if ( !is_array( $a ) ) {
2015 throw new DBUnexpectedError( $this, __METHOD__ .
' called with incorrect parameters' );
2021 foreach ( $a as $field =>
$value ) {
2023 if ( $mode == self::LIST_AND ) {
2025 } elseif ( $mode == self::LIST_OR ) {
2034 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2035 $list .=
"($value)";
2036 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2039 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array(
$value )
2042 $includeNull =
false;
2043 foreach ( array_keys(
$value,
null,
true ) as $nullKey ) {
2044 $includeNull =
true;
2045 unset(
$value[$nullKey] );
2047 if ( count(
$value ) == 0 && !$includeNull ) {
2048 throw new InvalidArgumentException(
2049 __METHOD__ .
": empty input for field $field" );
2050 } elseif ( count(
$value ) == 0 ) {
2052 $list .=
"$field IS NULL";
2055 if ( $includeNull ) {
2059 if ( count(
$value ) == 1 ) {
2069 if ( $includeNull ) {
2070 $list .=
" OR $field IS NULL)";
2073 } elseif (
$value ===
null ) {
2074 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2075 $list .=
"$field IS ";
2076 } elseif ( $mode == self::LIST_SET ) {
2077 $list .=
"$field = ";
2082 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2084 $list .=
"$field = ";
2096 foreach ( $data as
$base => $sub ) {
2097 if ( count( $sub ) ) {
2099 [ $baseKey =>
$base, $subKey => array_keys( $sub ) ],
2105 return $this->
makeList( $conds, self::LIST_OR );
2120 public function bitAnd( $fieldLeft, $fieldRight ) {
2121 return "($fieldLeft & $fieldRight)";
2124 public function bitOr( $fieldLeft, $fieldRight ) {
2125 return "($fieldLeft | $fieldRight)";
2129 return 'CONCAT(' . implode(
',', $stringList ) .
')';
2133 $delim, $table, $field, $conds =
'', $join_conds = []
2135 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->
addQuotes( $delim ) .
')';
2137 return '(' . $this->
selectSQLText( $table, $fld, $conds,
null, [], $join_conds ) .
')';
2142 $functionBody =
"$input FROM $startPosition";
2143 if ( $length !==
null ) {
2144 $functionBody .=
" FOR $length";
2146 return 'SUBSTRING(' . $functionBody .
')';
2162 if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2163 throw new InvalidArgumentException(
2164 '$startPosition must be a positive integer'
2167 if ( !( is_int( $length ) && $length >= 0 || $length ===
null ) ) {
2168 throw new InvalidArgumentException(
2169 '$length must be null or an integer greater than or equal to 0'
2179 return 'CAST( ' . $field .
' AS INTEGER )';
2196 # Stub. Shouldn't cause serious problems if it's not overridden, but
2197 # if your database engine supports a concept similar to MySQL's
2198 # databases you may as well.
2199 $this->dbName = $db;
2216 __METHOD__ .
': got Subquery instance when expecting a string.'
2220 # Skip the entire process when we have a string quoted on both ends.
2221 # Note that we check the end so that we will still quote any use of
2222 # use of `database`.table. But won't break things if someone wants
2223 # to query a database table with a dot in the name.
2228 # Lets test for any bits of text that should never show up in a table
2229 # name. Basically anything like JOIN or ON which are actually part of
2230 # SQL queries, but may end up inside of the table value to combine
2231 # sql. Such as how the API is doing.
2232 # Note that we use a whitespace test rather than a \b test to avoid
2233 # any remote case where a word like on may be inside of a table name
2234 # surrounded by symbols which may be considered word breaks.
2235 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2236 $this->queryLogger->warning(
2237 __METHOD__ .
": use of subqueries is not supported this way.",
2238 [
'exception' =>
new RuntimeException() ]
2244 # Split database and table into proper variables.
2247 # Quote $table and apply the prefix if not quoted.
2248 # $tableName might be empty if this is called from Database::replaceVars()
2249 $tableName =
"{$prefix}{$table}";
2250 if ( $format ===
'quoted'
2252 && $tableName !==
''
2257 # Quote $schema and $database and merge them with the table name if needed
2271 # We reverse the explode so that database.table and table both output the correct table.
2272 $dbDetails = explode(
'.', $name, 3 );
2273 if ( count( $dbDetails ) == 3 ) {
2275 # We don't want any prefix added in this case
2277 } elseif ( count( $dbDetails ) == 2 ) {
2278 list( $database, $table ) = $dbDetails;
2279 # We don't want any prefix added in this case
2281 # In dbs that support it, $database may actually be the schema
2282 # but that doesn't affect any of the functionality here
2285 list( $table ) = $dbDetails;
2286 if ( isset( $this->tableAliases[$table] ) ) {
2287 $database = $this->tableAliases[$table][
'dbname'];
2288 $schema = is_string( $this->tableAliases[$table][
'schema'] )
2289 ? $this->tableAliases[$table][
'schema']
2291 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
2292 ? $this->tableAliases[$table][
'prefix']
2301 return [ $database,
$schema, $prefix, $table ];
2311 if ( strlen( $namespace ) ) {
2315 $relation = $namespace .
'.' . $relation;
2322 $inArray = func_get_args();
2325 foreach ( $inArray as $name ) {
2333 $inArray = func_get_args();
2336 foreach ( $inArray as $name ) {
2355 if ( is_string( $table ) ) {
2356 $quotedTable = $this->
tableName( $table );
2357 } elseif ( $table instanceof
Subquery ) {
2358 $quotedTable = (
string)$table;
2360 throw new InvalidArgumentException(
"Table must be a string or Subquery." );
2363 if ( !strlen( $alias ) || $alias === $table ) {
2364 if ( $table instanceof
Subquery ) {
2365 throw new InvalidArgumentException(
"Subquery table missing alias." );
2368 return $quotedTable;
2382 foreach (
$tables as $alias => $table ) {
2383 if ( is_numeric( $alias ) ) {
2401 if ( !$alias || (
string)$alias === (
string)$name ) {
2416 foreach ( $fields as $alias => $field ) {
2417 if ( is_numeric( $alias ) ) {
2437 $tables, $use_index = [], $ignore_index = [], $join_conds = []
2441 $use_index = (
array)$use_index;
2442 $ignore_index = (
array)$ignore_index;
2443 $join_conds = (
array)$join_conds;
2445 foreach (
$tables as $alias => $table ) {
2446 if ( !is_string( $alias ) ) {
2451 if ( is_array( $table ) ) {
2453 if ( count( $table ) > 1 ) {
2454 $joinedTable =
'(' .
2456 $table, $use_index, $ignore_index, $join_conds ) .
')';
2459 $innerTable = reset( $table );
2460 $innerAlias =
key( $table );
2463 is_string( $innerAlias ) ? $innerAlias : $innerTable
2471 if ( isset( $join_conds[$alias] ) ) {
2472 list( $joinType, $conds ) = $join_conds[$alias];
2473 $tableClause = $joinType;
2474 $tableClause .=
' ' . $joinedTable;
2475 if ( isset( $use_index[$alias] ) ) {
2476 $use = $this->
useIndexClause( implode(
',', (array)$use_index[$alias] ) );
2478 $tableClause .=
' ' . $use;
2481 if ( isset( $ignore_index[$alias] ) ) {
2483 implode(
',', (array)$ignore_index[$alias] ) );
2484 if ( $ignore !=
'' ) {
2485 $tableClause .=
' ' . $ignore;
2488 $on = $this->
makeList( (array)$conds, self::LIST_AND );
2490 $tableClause .=
' ON (' . $on .
')';
2493 $retJOIN[] = $tableClause;
2494 } elseif ( isset( $use_index[$alias] ) ) {
2496 $tableClause = $joinedTable;
2498 implode(
',', (array)$use_index[$alias] )
2501 $ret[] = $tableClause;
2502 } elseif ( isset( $ignore_index[$alias] ) ) {
2504 $tableClause = $joinedTable;
2506 implode(
',', (array)$ignore_index[$alias] )
2509 $ret[] = $tableClause;
2511 $tableClause = $joinedTable;
2513 $ret[] = $tableClause;
2518 $implicitJoins =
$ret ? implode(
',',
$ret ) :
"";
2519 $explicitJoins = $retJOIN ? implode(
' ', $retJOIN ) :
"";
2522 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
2532 return isset( $this->indexAliases[$index] )
2533 ? $this->indexAliases[$index]
2538 if (
$s instanceof
Blob ) {
2541 if (
$s ===
null ) {
2543 } elseif ( is_bool(
$s ) ) {
2546 # This will also quote numeric values. This should be harmless,
2547 # and protects against weird problems that occur when they really
2548 # _are_ strings such as article titles and string->number->string
2549 # conversion is not 1:1.
2564 return '"' . str_replace(
'"',
'""',
$s ) .
'"';
2577 return $name[0] ==
'"' && substr( $name, -1, 1 ) ==
'"';
2586 return str_replace( [ $escapeChar,
'%',
'_' ],
2587 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
2659 if ( count(
$rows ) == 0 ) {
2664 if ( !is_array( reset(
$rows ) ) ) {
2671 foreach (
$rows as $row ) {
2673 $indexWhereClauses = [];
2674 foreach ( $uniqueIndexes as $index ) {
2675 $indexColumns = (
array)$index;
2676 $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
2677 if ( count( $indexRowValues ) != count( $indexColumns ) ) {
2680 'New record does not provide all values for unique key (' .
2681 implode(
', ', $indexColumns ) .
')'
2683 } elseif ( in_array(
null, $indexRowValues,
true ) ) {
2686 'New record has a null value for unique key (' .
2687 implode(
', ', $indexColumns ) .
')'
2693 if ( $indexWhereClauses ) {
2704 }
catch ( Exception
$e ) {
2724 if ( !is_array( reset(
$rows ) ) ) {
2728 $sql =
"REPLACE INTO $table (" . implode(
',', array_keys(
$rows[0] ) ) .
') VALUES ';
2731 foreach (
$rows as $row ) {
2738 $sql .=
'(' . $this->
makeList( $row ) .
')';
2744 public function upsert( $table, array
$rows, array $uniqueIndexes, array $set,
2747 if ( !count(
$rows ) ) {
2751 if ( !is_array( reset(
$rows ) ) ) {
2755 if ( count( $uniqueIndexes ) ) {
2757 foreach (
$rows as $row ) {
2758 foreach ( $uniqueIndexes as $index ) {
2759 $index = is_array( $index ) ? $index : [ $index ];
2761 foreach ( $index as $column ) {
2762 $rowKey[$column] = $row[$column];
2764 $clauses[] = $this->
makeList( $rowKey, self::LIST_AND );
2767 $where = [ $this->
makeList( $clauses, self::LIST_OR ) ];
2775 # Update any existing conflicting row(s)
2776 if ( $where !==
false ) {
2782 # Now insert any non-conflicting row(s)
2787 }
catch ( Exception
$e ) {
2795 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2802 $delTable = $this->
tableName( $delTable );
2803 $joinTable = $this->
tableName( $joinTable );
2804 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2805 if ( $conds !=
'*' ) {
2806 $sql .=
'WHERE ' . $this->
makeList( $conds, self::LIST_AND );
2815 $sql =
"SHOW COLUMNS FROM $table LIKE \"$field\";";
2816 $res = $this->
query( $sql, __METHOD__ );
2821 if ( preg_match(
'/\((.*)\)/', $row->Type, $m ) ) {
2830 public function delete( $table, $conds,
$fname = __METHOD__ ) {
2832 throw new DBUnexpectedError( $this, __METHOD__ .
' called with no conditions' );
2836 $sql =
"DELETE FROM $table";
2838 if ( $conds !=
'*' ) {
2839 if ( is_array( $conds ) ) {
2840 $conds = $this->
makeList( $conds, self::LIST_AND );
2842 $sql .=
' WHERE ' . $conds;
2849 $destTable, $srcTable, $varMap, $conds,
2850 $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2852 static $hints = [
'NO_AUTO_COLUMNS' ];
2854 $insertOptions = (
array)$insertOptions;
2855 $selectOptions = (
array)$selectOptions;
2857 if ( $this->cliMode && $this->
isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
2866 array_diff( $insertOptions, $hints ),
2878 array_diff( $insertOptions, $hints ),
2911 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2917 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
2920 $selectOptions[] =
'FOR UPDATE';
2922 $srcTable, implode(
',', $fields ), $conds,
$fname, $selectOptions, $selectJoinConds
2933 foreach (
$res as $row ) {
2946 if (
$rows && $ok ) {
2959 }
catch ( Exception
$e ) {
2982 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
2984 $destTable = $this->
tableName( $destTable );
2986 if ( !is_array( $insertOptions ) ) {
2987 $insertOptions = [ $insertOptions ];
2994 array_values( $varMap ),
3001 $sql =
"INSERT $insertOptions" .
3002 " INTO $destTable (" . implode(
',', array_keys( $varMap ) ) .
') ' .
3028 if ( !is_numeric( $limit ) ) {
3030 "Invalid non-numeric limit passed to limitResult()\n" );
3033 return "$sql LIMIT "
3034 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
3043 $glue = $all ?
') UNION ALL (' :
') UNION (';
3045 return '(' . implode( $glue, $sqls ) .
')';
3049 $table,
$vars, array $permute_conds, $extra_conds =
'',
$fname = __METHOD__,
3054 foreach ( $permute_conds as $field => $values ) {
3059 $values = array_unique( $values );
3061 foreach ( $conds as $cond ) {
3062 foreach ( $values as
$value ) {
3064 $newConds[] = $cond;
3070 $extra_conds = $extra_conds ===
'' ? [] : (
array)$extra_conds;
3074 if ( count( $conds ) === 1 &&
3093 if ( array_key_exists(
'INNER ORDER BY',
$options ) ) {
3096 if ( $limit !==
null && is_numeric( $offset ) && $offset != 0 ) {
3100 $options[
'LIMIT'] = $limit + $offset;
3106 foreach ( $conds as $cond ) {
3112 if ( $limit !==
null ) {
3113 $sql = $this->
limitResult( $sql, $limit, $offset );
3120 if ( is_array( $cond ) ) {
3121 $cond = $this->
makeList( $cond, self::LIST_AND );
3124 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3128 return "REPLACE({$orig}, {$old}, {$new})";
3180 $args = func_get_args();
3181 $function = array_shift(
$args );
3184 $this->
begin( __METHOD__ );
3191 $retVal = call_user_func_array( $function,
$args );
3196 usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
3202 }
while ( --$tries > 0 );
3204 if ( $tries <= 0 ) {
3209 $this->
commit( __METHOD__ );
3216 # Real waits are implemented in the subclass.
3244 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3245 $this->trxAutomatic =
true;
3257 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3258 $this->trxAutomatic =
true;
3265 $this->
startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3267 call_user_func( $callback );
3269 }
catch ( Exception
$e ) {
3280 if ( $this->
trxLevel && $this->trxAtomicLevels ) {
3281 $levelInfo = end( $this->trxAtomicLevels );
3283 return $levelInfo[1];
3296 foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3297 if ( $info[2] === $old ) {
3298 $this->trxPreCommitCallbacks[$key][2] = $new;
3301 foreach ( $this->trxIdleCallbacks as $key => $info ) {
3302 if ( $info[2] === $old ) {
3303 $this->trxIdleCallbacks[$key][2] = $new;
3306 foreach ( $this->trxEndCallbacks as $key => $info ) {
3307 if ( $info[2] === $old ) {
3308 $this->trxEndCallbacks[$key][2] = $new;
3319 $this->trxIdleCallbacks = array_filter(
3320 $this->trxIdleCallbacks,
3321 function ( $entry ) use ( $sectionIds ) {
3322 return !in_array( $entry[2], $sectionIds,
true );
3325 $this->trxPreCommitCallbacks = array_filter(
3326 $this->trxPreCommitCallbacks,
3327 function ( $entry ) use ( $sectionIds ) {
3328 return !in_array( $entry[2], $sectionIds,
true );
3332 foreach ( $this->trxEndCallbacks as $key => $entry ) {
3333 if ( in_array( $entry[2], $sectionIds,
true ) ) {
3334 $callback = $entry[0];
3335 $this->trxEndCallbacks[$key][0] =
function () use ( $callback ) {
3336 return $callback( self::TRIGGER_ROLLBACK );
3344 $this->trxRecurringCallbacks[
$name] = $callback;
3346 unset( $this->trxRecurringCallbacks[$name] );
3359 $this->trxEndCallbacksSuppressed = $suppress;
3372 if ( $this->trxEndCallbacksSuppressed ) {
3376 $autoTrx = $this->
getFlag( self::DBO_TRX );
3380 $callbacks = array_merge(
3381 $this->trxIdleCallbacks,
3382 $this->trxEndCallbacks
3384 $this->trxIdleCallbacks = [];
3385 $this->trxEndCallbacks = [];
3386 foreach ( $callbacks as $callback ) {
3388 list( $phpCallback ) = $callback;
3390 call_user_func_array( $phpCallback, [ $trigger ] );
3392 $this->
setFlag( self::DBO_TRX );
3396 }
catch ( Exception $ex ) {
3397 call_user_func( $this->errorLogger, $ex );
3402 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
3406 }
while ( count( $this->trxIdleCallbacks ) );
3408 if (
$e instanceof Exception ) {
3425 $this->trxPreCommitCallbacks = [];
3426 foreach ( $callbacks as $callback ) {
3428 list( $phpCallback ) = $callback;
3429 call_user_func( $phpCallback );
3430 }
catch ( Exception $ex ) {
3431 call_user_func( $this->errorLogger, $ex );
3435 }
while ( count( $this->trxPreCommitCallbacks ) );
3437 if (
$e instanceof Exception ) {
3452 if ( $this->trxEndCallbacksSuppressed ) {
3459 foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
3461 $phpCallback( $trigger, $this );
3462 }
catch ( Exception $ex ) {
3463 call_user_func( $this->errorLogger, $ex );
3468 if (
$e instanceof Exception ) {
3521 if ( strlen( $savepointId ) > 30 ) {
3526 'There have been an excessively large number of atomic sections in a transaction'
3527 .
" started by $this->trxFname (at $fname)"
3531 return $savepointId;
3535 $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
3540 $this->
begin(
$fname, self::TRANSACTION_INTERNAL );
3543 if ( $this->
getFlag( self::DBO_TRX ) ) {
3549 $this->trxAutomaticAtomic =
true;
3551 } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
3557 $this->trxAtomicLevels[] = [
$fname, $sectionId, $savepointId ];
3563 if ( !$this->
trxLevel || !$this->trxAtomicLevels ) {
3564 throw new DBUnexpectedError( $this,
"No atomic section is open (got $fname)." );
3568 $pos = count( $this->trxAtomicLevels ) - 1;
3569 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3571 if ( $savedFname !==
$fname ) {
3574 "Invalid atomic section ended (got $fname but expected $savedFname)."
3579 array_pop( $this->trxAtomicLevels );
3581 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
3583 } elseif ( $savepointId !==
null && $savepointId !== self::$NOT_APPLICABLE ) {
3590 if ( $currentSectionId ) {
3598 if ( !$this->
trxLevel || !$this->trxAtomicLevels ) {
3599 throw new DBUnexpectedError( $this,
"No atomic section is open (got $fname)." );
3602 if ( $sectionId !==
null ) {
3605 foreach ( $this->trxAtomicLevels as $i =>
list( $asFname, $asId, $spId ) ) {
3606 if ( $asId === $sectionId ) {
3615 $len = count( $this->trxAtomicLevels );
3616 for ( $i = $pos + 1; $i < $len; ++$i ) {
3617 $excisedIds[] = $this->trxAtomicLevels[$i][1];
3619 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
3624 $pos = count( $this->trxAtomicLevels ) - 1;
3625 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3627 if ( $savedFname !==
$fname ) {
3630 "Invalid atomic section ended (got $fname but expected $savedFname)."
3635 array_pop( $this->trxAtomicLevels );
3638 if ( $savepointId !==
null ) {
3640 if ( $savepointId === self::$NOT_APPLICABLE ) {
3645 $this->trxStatusIgnoredCause =
null;
3647 } elseif ( $this->
trxStatus > self::STATUS_TRX_ERROR ) {
3649 $this->
trxStatus = self::STATUS_TRX_ERROR;
3652 "Uncancelable atomic section canceled (got $fname)."
3656 $this->affectedRowCount = 0;
3660 $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
3664 $res = call_user_func_array( $callback, [ $this,
$fname ] );
3665 }
catch ( Exception
$e ) {
3675 final public function begin(
$fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
3676 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
3677 if ( !in_array( $mode, $modes,
true ) ) {
3678 throw new DBUnexpectedError( $this,
"$fname: invalid mode parameter '$mode'." );
3683 if ( $this->trxAtomicLevels ) {
3685 $msg =
"$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
3687 } elseif ( !$this->trxAutomatic ) {
3688 $msg =
"$fname: Explicit transaction already active (from {$this->trxFname}).";
3691 $msg =
"$fname: Implicit transaction already active (from {$this->trxFname}).";
3694 } elseif ( $this->
getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
3695 $msg =
"$fname: Implicit transaction expected (DBO_TRX set).";
3704 $this->trxStatusIgnoredCause =
null;
3705 $this->trxAtomicCounter = 0;
3707 $this->trxFname =
$fname;
3708 $this->trxDoneWrites =
false;
3709 $this->trxAutomaticAtomic =
false;
3710 $this->trxAtomicLevels = [];
3711 $this->trxShortId = sprintf(
'%06x', mt_rand( 0, 0xffffff ) );
3712 $this->trxWriteDuration = 0.0;
3713 $this->trxWriteQueryCount = 0;
3714 $this->trxWriteAffectedRows = 0;
3715 $this->trxWriteAdjDuration = 0.0;
3716 $this->trxWriteAdjQueryCount = 0;
3717 $this->trxWriteCallers = [];
3720 $this->trxReplicaLag =
null;
3725 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
3739 final public function commit(
$fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
3740 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
3741 if ( !in_array( $flush, $modes,
true ) ) {
3742 throw new DBUnexpectedError( $this,
"$fname: invalid flush parameter '$flush'." );
3745 if ( $this->
trxLevel && $this->trxAtomicLevels ) {
3750 "$fname: Got COMMIT while atomic sections $levels are still open."
3754 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3757 } elseif ( !$this->trxAutomatic ) {
3760 "$fname: Flushing an explicit transaction, getting out of sync."
3765 $this->queryLogger->error(
3766 "$fname: No transaction to commit, something got out of sync." );
3768 } elseif ( $this->trxAutomatic ) {
3771 "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
3782 $this->
trxStatus = self::STATUS_TRX_NONE;
3783 if ( $this->trxDoneWrites ) {
3784 $this->lastWriteTime = microtime(
true );
3785 $this->trxProfiler->transactionWritingOut(
3790 $this->trxWriteAffectedRows
3814 if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS ) {
3815 if ( $this->
getFlag( self::DBO_TRX ) ) {
3818 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
3828 $this->
trxStatus = self::STATUS_TRX_NONE;
3829 $this->trxAtomicLevels = [];
3830 if ( $this->trxDoneWrites ) {
3831 $this->trxProfiler->transactionWritingOut(
3841 $this->trxIdleCallbacks = [];
3842 $this->trxPreCommitCallbacks = [];
3847 }
catch ( Exception
$e ) {
3852 }
catch ( Exception
$e ) {
3856 $this->affectedRowCount = 0;
3868 # Disconnects cause rollback anyway, so ignore those errors
3869 $ignoreErrors =
true;
3870 $this->
query(
'ROLLBACK',
$fname, $ignoreErrors );
3881 "$fname: Cannot flush snapshot because writes are pending ($fnames)."
3893 $oldName, $newName, $temporary =
false,
$fname = __METHOD__
3895 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
3899 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
3903 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
3907 $t =
new ConvertibleTimestamp( $ts );
3909 return $t->getTimestamp( TS_MW );
3913 if ( is_null( $ts ) ) {
3921 return ( $this->affectedRowCount ===
null )
3949 } elseif ( $result ===
true ) {
3957 public function ping( &$rtt =
null ) {
3959 if ( $this->
isOpen() && ( microtime(
true ) - $this->lastPing ) < self::PING_TTL ) {
3960 if ( !func_num_args() || $this->rttEstimate > 0 ) {
3967 $this->
clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
3968 $ok = ( $this->
query( self::PING_QUERY, __METHOD__,
true ) !==
false );
3986 $this->opened =
false;
3987 $this->conn =
false;
3989 $this->
open( $this->server, $this->user, $this->password, $this->dbName );
3990 $this->lastPing = microtime(
true );
3993 $this->connLogger->warning(
3994 $fname .
': lost connection to {dbserver}; reconnected',
3997 'exception' =>
new RuntimeException()
4003 $this->connLogger->error(
4004 $fname .
': lost connection to {dbserver} permanently',
4032 return ( $this->
trxLevel && $this->trxReplicaLag !==
null )
4046 'since' => microtime(
true )
4070 $res = [
'lag' => 0,
'since' => INF,
'pending' =>
false ];
4071 foreach ( func_get_args() as $db ) {
4073 $status = $db->getSessionLagStatus();
4074 if (
$status[
'lag'] ===
false ) {
4075 $res[
'lag'] =
false;
4076 } elseif (
$res[
'lag'] !==
false ) {
4080 $res[
'pending'] =
$res[
'pending'] ?: $db->writesPending();
4099 if ( $b instanceof
Blob ) {
4110 callable $lineCallback =
null,
4111 callable $resultCallback =
null,
4113 callable $inputCallback =
null
4115 Wikimedia\suppressWarnings();
4116 $fp = fopen( $filename,
'r' );
4117 Wikimedia\restoreWarnings();
4119 if (
false === $fp ) {
4120 throw new RuntimeException(
"Could not open \"{$filename}\".\n" );
4124 $fname = __METHOD__ .
"( $filename )";
4129 $fp, $lineCallback, $resultCallback,
$fname, $inputCallback );
4130 }
catch ( Exception
$e ) {
4141 $this->schemaVars =
$vars;
4146 callable $lineCallback =
null,
4147 callable $resultCallback =
null,
4149 callable $inputCallback =
null
4151 $delimiterReset =
new ScopedCallback(
4159 while ( !feof( $fp ) ) {
4160 if ( $lineCallback ) {
4161 call_user_func( $lineCallback );
4164 $line = trim( fgets( $fp ) );
4166 if (
$line ==
'' ) {
4182 if ( $done || feof( $fp ) ) {
4185 if ( $inputCallback ) {
4186 $callbackResult = call_user_func( $inputCallback, $cmd );
4188 if ( is_string( $callbackResult ) || !$callbackResult ) {
4189 $cmd = $callbackResult;
4196 if ( $resultCallback ) {
4197 call_user_func( $resultCallback,
$res, $this );
4200 if (
false ===
$res ) {
4203 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
4210 ScopedCallback::consume( $delimiterReset );
4222 if ( $this->delimiter ) {
4224 $newLine = preg_replace(
4225 '/' . preg_quote( $this->delimiter,
'/' ) .
'$/',
'', $newLine );
4226 if ( $newLine != $prev ) {
4256 return preg_replace_callback(
4258 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
4259 \'\{\$ (\w+) }\' | # 3. addQuotes
4260 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
4261 /\*\$ (\w+) \*/ # 5. leave unencoded
4263 function ( $m ) use (
$vars ) {
4266 if ( isset( $m[1] ) && $m[1] !==
'' ) {
4267 if ( $m[1] ===
'i' ) {
4272 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3],
$vars ) ) {
4274 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4],
$vars ) ) {
4276 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5],
$vars ) ) {
4277 return $vars[$m[5]];
4293 if ( $this->schemaVars ) {
4316 return !isset( $this->namedLocksHeld[$lockName] );
4319 public function lock( $lockName, $method, $timeout = 5 ) {
4320 $this->namedLocksHeld[$lockName] = 1;
4325 public function unlock( $lockName, $method ) {
4326 unset( $this->namedLocksHeld[$lockName] );
4337 "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
4341 if ( !$this->
lock( $lockKey,
$fname, $timeout ) ) {
4345 $unlocker =
new ScopedCallback(
function () use ( $lockKey,
$fname ) {
4351 function () use ( $lockKey,
$fname ) {
4374 final public function lockTables( array $read, array $write, $method ) {
4376 throw new DBUnexpectedError( $this,
"Transaction writes or callbacks still pending." );
4425 public function dropTable( $tableName, $fName = __METHOD__ ) {
4426 if ( !$this->
tableExists( $tableName, $fName ) ) {
4429 $sql =
"DROP TABLE " . $this->
tableName( $tableName ) .
" CASCADE";
4431 return $this->
query( $sql, $fName );
4439 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() )
4445 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() ) {
4449 return ConvertibleTimestamp::convert( $format, $expiry );
4464 $reason = $this->
getLBInfo(
'readOnlyReason' );
4466 return is_string( $reason ) ? $reason :
false;
4470 $this->tableAliases = $aliases;
4474 $this->indexAliases = $aliases;
4489 if ( !$this->conn ) {
4492 'DB connection was already closed or the connection dropped.'
4512 $this->connLogger->warning(
4513 "Cloning " . static::class .
" is not recommended; forking connection",
4514 [
'exception' =>
new RuntimeException() ]
4519 $this->opened =
false;
4520 $this->conn =
false;
4521 $this->trxEndCallbacks = [];
4523 $this->
open( $this->server, $this->user, $this->password, $this->dbName );
4524 $this->lastPing = microtime(
true );
4534 throw new RuntimeException(
'Database serialization may cause problems, since ' .
4535 'the connection is not restored on wakeup.' );
4542 if ( $this->
trxLevel && $this->trxDoneWrites ) {
4543 trigger_error(
"Uncommitted DB writes (transaction from {$this->trxFname})." );
4547 if ( $danglingWriters ) {
4548 $fnames = implode(
', ', $danglingWriters );
4549 trigger_error(
"DB transaction writes or callbacks still pending ($fnames)." );
4552 if ( $this->conn ) {
4555 Wikimedia\suppressWarnings();
4557 Wikimedia\restoreWarnings();
4558 $this->conn =
false;
4559 $this->opened =
false;
4564class_alias( Database::class,
'DatabaseBase' );
4565class_alias( Database::class,
'Database' );
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
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
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database etc For and for historical it also represents a few features of articles that don t involve their such as access rights See also title txt Article Encapsulates access to the page table of the database The object represents a an and maintains state such as flags
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database key
the array() calling protocol came about after MediaWiki 1.4rc1.
static configuration should be added through ResourceLoaderGetConfigVars instead & $vars
namespace being checked & $result
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
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
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). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. '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
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
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
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
returning false will NOT prevent logging $e
Advanced database interface for IDatabase handles that include maintenance methods.
fieldInfo( $table, $field)
mysql_fetch_field() wrapper Returns false if the field doesn't exist
if(is_array($mode)) switch( $mode) $input