* @since 1.28
*/
abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
- /** @var string Server that this instance is currently connected to */
- protected $server;
- /** @var string User that this instance is currently connected under the name of */
- protected $user;
- /** @var string Password used to establish the current connection */
- protected $password;
- /** @var array[] Map of (table => (dbname, schema, prefix) map) */
- protected $tableAliases = [];
- /** @var string[] Map of (index alias => index) */
- protected $indexAliases = [];
- /** @var bool Whether this PHP instance is for a CLI script */
- protected $cliMode;
- /** @var string Agent name for query profiling */
- protected $agent;
- /** @var int Bit field of class DBO_* constants */
- protected $flags;
- /** @var array LoadBalancer tracking information */
- protected $lbInfo = [];
- /** @var array|bool Variables use for schema element placeholders */
- protected $schemaVars = false;
- /** @var array Parameters used by initConnection() to establish a connection */
- protected $connectionParams = [];
- /** @var array SQL variables values to use for all new connections */
- protected $connectionVariables = [];
- /** @var string Current SQL query delimiter */
- protected $delimiter = ';';
- /** @var string|bool|null Stashed value of html_errors INI setting */
- protected $htmlErrors;
- /** @var int Row batch size to use for emulated INSERT SELECT queries */
- protected $nonNativeInsertSelectBatchSize = 10000;
-
/** @var BagOStuff APC cache */
protected $srvCache;
/** @var LoggerInterface */
protected $profiler;
/** @var TransactionProfiler */
protected $trxProfiler;
+
/** @var DatabaseDomain */
protected $currentDomain;
+
/** @var object|resource|null Database connection */
protected $conn;
/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
private $lazyMasterHandle;
+ /** @var string Server that this instance is currently connected to */
+ protected $server;
+ /** @var string User that this instance is currently connected under the name of */
+ protected $user;
+ /** @var string Password used to establish the current connection */
+ protected $password;
+ /** @var bool Whether this PHP instance is for a CLI script */
+ protected $cliMode;
+ /** @var string Agent name for query profiling */
+ protected $agent;
+ /** @var array Parameters used by initConnection() to establish a connection */
+ protected $connectionParams;
+ /** @var string[]|int[]|float[] SQL variables values to use for all new connections */
+ protected $connectionVariables;
+ /** @var int Row batch size to use for emulated INSERT SELECT queries */
+ protected $nonNativeInsertSelectBatchSize;
+
+ /** @var int Current bit field of class DBO_* constants */
+ protected $flags;
+ /** @var array Current LoadBalancer tracking information */
+ protected $lbInfo = [];
+ /** @var string Current SQL query delimiter */
+ protected $delimiter = ';';
+ /** @var array[] Current map of (table => (dbname, schema, prefix) map) */
+ protected $tableAliases = [];
+ /** @var string[] Current map of (index alias => index) */
+ protected $indexAliases = [];
+ /** @var array|null Current variables use for schema element placeholders */
+ protected $schemaVars;
+
+ /** @var string|bool|null Stashed value of html_errors INI setting */
+ private $htmlErrors;
+ /** @var int[] Prior flags member variable values */
+ private $priorFlags = [];
+
/** @var array Map of (name => 1) for locks obtained via lock() */
protected $sessionNamedLocks = [];
/** @var array Map of (table name => 1) for TEMPORARY tables */
protected $sessionTempTables = [];
/** @var string ID of the active transaction or the empty string otherwise */
- protected $trxShortId = '';
+ private $trxShortId = '';
/** @var int Transaction status */
- protected $trxStatus = self::STATUS_TRX_NONE;
+ private $trxStatus = self::STATUS_TRX_NONE;
/** @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR */
- protected $trxStatusCause;
+ private $trxStatusCause;
/** @var array|null Error details of the last statement-only rollback */
private $trxStatusIgnoredCause;
/** @var float|null UNIX timestamp at the time of BEGIN for the last transaction */
/** @var bool Whether to suppress triggering of transaction end callbacks */
private $trxEndCallbacksSuppressed = false;
- /** @var int[] Prior flags member variable values */
- private $priorFlags = [];
-
/** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */
protected $affectedRowCount;
/** @var float Query rount trip time estimate */
private $lastRoundTripEstimate = 0.0;
+ /** @var int|null Integer ID of the managing LBFactory instance or null if none */
+ private $ownerId;
+
/** @var string Lock granularity is on the level of the entire database */
const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
/** @var string The SCHEMA keyword refers to a grouping of tables in a database */
* @note exceptions for missing libraries/drivers should be thrown in initConnection()
* @param array $params Parameters passed from Database::factory()
*/
- protected function __construct( array $params ) {
+ public function __construct( array $params ) {
+ $this->connectionParams = [];
foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
$this->connectionParams[$name] = $params[$name];
}
-
+ $this->connectionVariables = $params['variables'] ?? [];
$this->cliMode = $params['cliMode'];
- // Agent name is added to SQL queries in a comment, so make sure it can't break out
- $this->agent = str_replace( '/', '-', $params['agent'] );
-
+ $this->agent = $params['agent'];
$this->flags = $params['flags'];
if ( $this->flags & self::DBO_DEFAULT ) {
if ( $this->cliMode ) {
$this->flags |= self::DBO_TRX;
}
}
- // Disregard deprecated DBO_IGNORE flag (T189999)
- $this->flags &= ~self::DBO_IGNORE;
-
- $this->connectionVariables = $params['variables'];
+ $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'] ?? 10000;
$this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
-
$this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
$this->trxProfiler = $params['trxProfiler'];
$this->connLogger = $params['connLogger'];
$this->errorLogger = $params['errorLogger'];
$this->deprecationLogger = $params['deprecationLogger'];
- if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
- $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
- }
-
// Set initial dummy domain until open() sets the final DB/prefix
$this->currentDomain = new DatabaseDomain(
$params['dbname'] != '' ? $params['dbname'] : null,
$params['schema'] != '' ? $params['schema'] : null,
$params['tablePrefix']
);
+
+ $this->ownerId = $params['ownerId'] ?? null;
}
/**
* - cliMode: Whether to consider the execution context that of a CLI script.
* - agent: Optional name used to identify the end-user in query profiling/logging.
* - srvCache: Optional BagOStuff instance to an APC-style cache.
- * - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT emulation.
+ * - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT.
+ * - ownerId: Optional integer ID of a LoadBalancer instance that manages this instance.
* @param int $connect One of the class constants (NEW_CONNECTED, NEW_UNCONNECTED) [optional]
* @return Database|null If the database driver or extension cannot be found
* @throws InvalidArgumentException If the database driver or extension cannot be found
'flags' => 0,
'variables' => [],
'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
- 'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname()
+ 'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname(),
+ 'ownerId' => null
];
$normalizedParams = [
'cliMode' => (bool)$params['cliMode'],
'agent' => (string)$params['agent'],
// Objects and callbacks
+ 'srvCache' => $params['srvCache'] ?? new HashBagOStuff(),
'profiler' => $params['profiler'] ?? null,
'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
'connLogger' => $params['connLogger'] ?? new NullLogger(),
// we auto-detect the first available driver. For types without built-in support,
// an class named "Database<Type>" us used, eg. DatabaseFoo for type 'foo'.
static $builtinTypes = [
- 'mssql' => DatabaseMssql::class,
'mysql' => [ 'mysqli' => DatabaseMysqli::class ],
'sqlite' => DatabaseSqlite::class,
'postgres' => DatabasePostgres::class,
}
/**
- * @return array Map of (Database::ATTR_* constant => value
+ * @return array Map of (Database::ATTR_* constant => value)
* @since 1.31
*/
protected static function getAttributes() {
return $this->getServerVersion();
}
+ /**
+ * Backwards-compatibility no-op method for disabling query buffering
+ *
+ * @param null|bool $buffer Whether to buffer queries (ignored)
+ * @return bool Whether buffering was already enabled (always true)
+ * @deprecated Since 1.34 Use query batching; this no longer does anything
+ */
public function bufferResults( $buffer = null ) {
- $res = !$this->getFlag( self::DBO_NOBUFFER );
- if ( $buffer !== null ) {
- $buffer
- ? $this->clearFlag( self::DBO_NOBUFFER )
- : $this->setFlag( self::DBO_NOBUFFER );
- }
-
- return $res;
+ return true;
}
final public function trxLevel() {
);
}
- final public function close() {
- $exception = null; // error to throw after disconnecting
+ final public function close( $fname = __METHOD__, $owner = null ) {
+ $error = null; // error to throw after disconnecting
$wasOpen = (bool)$this->conn;
// This should mostly do nothing if the connection is already closed
if ( $this->trxAtomicLevels ) {
// Cannot let incomplete atomic sections be committed
$levels = $this->flatAtomicSectionList();
- $exception = new DBUnexpectedError(
- $this,
- __METHOD__ . ": atomic sections $levels are still open"
- );
+ $error = "$fname: atomic sections $levels are still open";
} elseif ( $this->trxAutomatic ) {
// Only the connection manager can commit non-empty DBO_TRX transactions
// (empty ones we can silently roll back)
if ( $this->writesOrCallbacksPending() ) {
- $exception = new DBUnexpectedError(
- $this,
- __METHOD__ .
- ": mass commit/rollback of peer transaction required (DBO_TRX set)"
- );
+ $error = "$fname: " .
+ "expected mass rollback of all peer transactions (DBO_TRX set)";
}
} else {
// Manual transactions should have been committed or rolled
// back, even if empty.
- $exception = new DBUnexpectedError(
- $this,
- __METHOD__ . ": transaction is still open (from {$this->trxFname})"
- );
+ $error = "$fname: transaction is still open (from {$this->trxFname})";
}
- if ( $this->trxEndCallbacksSuppressed ) {
- $exception = $exception ?: new DBUnexpectedError(
- $this,
- __METHOD__ . ': callbacks are suppressed; cannot properly commit'
- );
+ if ( $this->trxEndCallbacksSuppressed && $error === null ) {
+ $error = "$fname: callbacks are suppressed; cannot properly commit";
}
// Rollback the changes and run any callbacks as needed
$this->conn = null;
- // Throw any unexpected errors after having disconnected
- if ( $exception instanceof Exception ) {
- throw $exception;
+ // Log or throw any unexpected errors after having disconnected
+ if ( $error !== null ) {
+ // T217819, T231443: if this is probably just LoadBalancer trying to recover from
+ // errors and shutdown, then log any problems and move on since the request has to
+ // end one way or another. Throwing errors is not very useful at some point.
+ if ( $this->ownerId !== null && $owner === $this->ownerId ) {
+ $this->queryLogger->error( $error );
+ } else {
+ throw new DBUnexpectedError( $this, $error );
+ }
}
// Note that various subclasses call close() at the start of open(), which itself is
* @throws DBReadOnlyError
*/
protected function assertIsWritableMaster() {
- if ( $this->getLBInfo( 'replica' ) === true ) {
+ if ( $this->getLBInfo( 'replica' ) ) {
throw new DBReadOnlyRoleError(
$this,
'Write operations are not allowed on replica database connections'
// Send the query to the server and fetch any corresponding errors
list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
if ( $ret === false ) {
- $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
+ $ignoreErrors = $this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS );
// Throw an error unless both the ignore flag was set and a rollback is not needed
$this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
}
// Do not treat temporary table writes as "meaningful writes" since they are only
// visible to one session and are not permanent. Profile them as reads. Integration
// tests can override this behavior via $flags.
- $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
+ $pseudoPermanent = $this->fieldHasBit( $flags, self::QUERY_PSEUDO_PERMANENT );
list( $tmpType, $tmpNew, $tmpDel ) = $this->getTempWrites( $sql, $pseudoPermanent );
$isPermWrite = ( $tmpType !== self::$TEMP_NORMAL );
// DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
- if ( $isPermWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
+ if ( $isPermWrite && $this->fieldHasBit( $flags, self::QUERY_REPLICA_ROLE ) ) {
throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
}
} else {
}
// Add trace comment to the begin of the sql string, right after the operator.
- // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
- $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
+ // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
+ $encAgent = str_replace( '/', '-', $this->agent );
+ $commentedSql = preg_replace( '/\s|$/', " /* $fname $encAgent */ ", $sql, 1 );
// Send the query to the server and fetch any corresponding errors.
// This also doubles as a "ping" to see if the connection was dropped.
$this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
// Check if the query failed due to a recoverable connection loss
- $allowRetry = !$this->hasFlags( $flags, self::QUERY_NO_RETRY );
+ $allowRetry = !$this->fieldHasBit( $flags, self::QUERY_NO_RETRY );
if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) {
// Silently resend the query to the server since it is safe and possible
list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
}
}
- $prefix = !is_null( $this->getLBInfo( 'master' ) ) ? 'query-m: ' : 'query: ';
+ $prefix = $this->getLBInfo( 'master' ) ? 'query-m: ' : 'query: ';
$generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix );
$startTime = microtime( true );
// Avoid the overhead of logging calls unless debug mode is enabled
if ( $this->getFlag( self::DBO_DEBUG ) ) {
$this->queryLogger->debug(
- "{method} [{runtime}s] {db_host}: $sql",
+ "{method} [{runtime}s] {db_host}: {sql}",
[
'method' => $fname,
'db_host' => $this->getServer(),
+ 'sql' => $sql,
'domain' => $this->getDomainID(),
'runtime' => round( $queryRuntime, 3 )
]
$this->trxAtomicCounter = 0;
$this->trxIdleCallbacks = []; // T67263; transaction already lost
$this->trxPreCommitCallbacks = []; // T67263; transaction already lost
+ // Clear additional subclass fields
+ $this->doHandleSessionLossPreconnect();
// @note: leave trxRecurringCallbacks in place
if ( $this->trxDoneWrites ) {
$this->trxProfiler->transactionWritingOut(
}
}
+ /**
+ * Reset any additional subclass trx* and session* fields
+ */
+ protected function doHandleSessionLossPreconnect() {
+ // no-op
+ }
+
/**
* Clean things up after session (and thus transaction) loss after reconnect
*/
* Returns an optional USE INDEX clause to go after the table, and a
* string to go at the end of the query.
*
+ * @see Database::select()
+ *
* @param array $options Associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
* @return array
- * @see Database::select()
*/
- protected function makeSelectOptions( $options ) {
+ protected function makeSelectOptions( array $options ) {
$preLimitTail = $postLimitTail = '';
$startOpts = '';
$this->selectOptionsIncludeLocking( $options ) &&
$this->selectFieldsOrOptionsAggregate( $vars, $options )
) {
- // Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate
+ // Some DB types (e.g. postgres) disallow FOR UPDATE with aggregate
// functions. Discourage use of such queries to encourage compatibility.
call_user_func(
$this->deprecationLogger,
if ( in_array( $entry[2], $sectionIds, true ) ) {
$callback = $entry[0];
$this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
- // @phan-suppress-next-line PhanInfiniteRecursion No recursion at all here, phan is confused
+ // @phan-suppress-next-line PhanInfiniteRecursion, PhanUndeclaredInvokeInCallable
return $callback( self::TRIGGER_ROLLBACK, $this );
};
// This "on resolution" callback no longer belongs to a section.
try {
++$count;
list( $phpCallback ) = $callback;
+ // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
$phpCallback( $this );
} catch ( Exception $ex ) {
( $this->errorLogger )( $ex );
foreach ( $callbacks as $entry ) {
if ( $sectionIds === null || in_array( $entry[2], $sectionIds, true ) ) {
try {
+ // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
$entry[0]( $trigger, $this );
} catch ( Exception $ex ) {
( $this->errorLogger )( $ex );
if ( $this->trxLevel() ) {
if ( $this->trxAtomicLevels ) {
$levels = $this->flatAtomicSectionList();
- $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open";
+ $msg = "$fname: got explicit BEGIN while atomic section(s) $levels are open";
throw new DBUnexpectedError( $this, $msg );
} elseif ( !$this->trxAutomatic ) {
- $msg = "$fname: Explicit transaction already active (from {$this->trxFname})";
+ $msg = "$fname: explicit transaction already active (from {$this->trxFname})";
throw new DBUnexpectedError( $this, $msg );
} else {
- $msg = "$fname: Implicit transaction already active (from {$this->trxFname})";
+ $msg = "$fname: implicit transaction already active (from {$this->trxFname})";
throw new DBUnexpectedError( $this, $msg );
}
} elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
- $msg = "$fname: Implicit transaction expected (DBO_TRX set)";
+ $msg = "$fname: implicit transaction expected (DBO_TRX set)";
throw new DBUnexpectedError( $this, $msg );
}
$levels = $this->flatAtomicSectionList();
throw new DBUnexpectedError(
$this,
- "$fname: Got COMMIT while atomic sections $levels are still open"
+ "$fname: got COMMIT while atomic sections $levels are still open"
);
}
} elseif ( !$this->trxAutomatic ) {
throw new DBUnexpectedError(
$this,
- "$fname: Flushing an explicit transaction, getting out of sync"
+ "$fname: flushing an explicit transaction, getting out of sync"
);
}
} elseif ( !$this->trxLevel() ) {
$this->queryLogger->error(
- "$fname: No transaction to commit, something got out of sync" );
+ "$fname: no transaction to commit, something got out of sync" );
return; // nothing to do
} elseif ( $this->trxAutomatic ) {
throw new DBUnexpectedError(
$this,
- "$fname: Expected mass commit of all peer transactions (DBO_TRX set)"
+ "$fname: expected mass commit of all peer transactions (DBO_TRX set)"
);
}
}
// This will reconnect if possible or return false if not
- $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
- $ok = ( $this->query( self::$PING_QUERY, __METHOD__, true ) !== false );
- $this->restoreFlags( self::RESTORE_PRIOR );
-
+ $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS;
+ $ok = ( $this->query( self::$PING_QUERY, __METHOD__, $flags ) !== false );
if ( $ok ) {
$rtt = $this->lastRoundTripEstimate;
}
}
public function setSchemaVars( $vars ) {
- $this->schemaVars = $vars;
+ $this->schemaVars = is_array( $vars ) ? $vars : null;
}
public function sourceStream(
* @return array
*/
protected function getSchemaVars() {
- if ( $this->schemaVars ) {
- return $this->schemaVars;
- } else {
- return $this->getDefaultSchemaVars();
- }
+ return $this->schemaVars ?? $this->getDefaultSchemaVars();
}
/**
* @param int $field
* @param int $flags
* @return bool
+ * @since 1.34
*/
- protected function hasFlags( $field, $flags ) {
+ final protected function fieldHasBit( $field, $flags ) {
return ( ( $field & $flags ) === $flags );
}