use Psr\Log\NullLogger;
use Wikimedia\ScopedCallback;
use Wikimedia\Timestamp\ConvertibleTimestamp;
-use Wikimedia;
+use Wikimedia\AtEase\AtEase;
use BagOStuff;
use HashBagOStuff;
use LogicException;
protected $cliMode;
/** @var string Agent name for query profiling */
protected $agent;
- /** @var int Bitfield of class DBO_* constants */
+ /** @var int Bit field of class DBO_* constants */
protected $flags;
/** @var array LoadBalancer tracking information */
protected $lbInfo = [];
/** @var int Writes to this temporary table effect lastDoneWrites() */
private static $TEMP_PSEUDO_PERMANENT = 2;
- /** Number of times to re-try an operation in case of deadlock */
+ /** @var int Number of times to re-try an operation in case of deadlock */
private static $DEADLOCK_TRIES = 4;
- /** Minimum time to wait before retry, in microseconds */
+ /** @var int Minimum time to wait before retry, in microseconds */
private static $DEADLOCK_DELAY_MIN = 500000;
- /** Maximum time to wait before retry */
+ /** @var int Maximum time to wait before retry */
private static $DEADLOCK_DELAY_MAX = 1500000;
- /** How long before it is worth doing a dummy query to test the connection */
+ /** @var int How long before it is worth doing a dummy query to test the connection */
private static $PING_TTL = 1.0;
+ /** @var string Dummy SQL query */
private static $PING_QUERY = 'SELECT 1 AS ping';
+ /** @var float Guess of how many seconds it takes to replicate a small insert */
private static $TINY_WRITE_SEC = 0.010;
+ /** @var float Consider a write slow if it took more than this many seconds */
private static $SLOW_WRITE_SEC = 0.500;
+ /** @var float Assume an insert of this many rows or less should be fast to replicate */
private static $SMALL_WRITE_ROWS = 100;
+ /** @var string[] List of DBO_* flags that can be changed after connection */
+ protected static $MUTABLE_FLAGS = [
+ 'DBO_DEBUG',
+ 'DBO_NOBUFFER',
+ 'DBO_TRX',
+ 'DBO_DDLMODE',
+ ];
+ /** @var int Bit field of all DBO_* flags that can be changed after connection */
+ protected static $DBO_MUTABLE = (
+ self::DBO_DEBUG | self::DBO_NOBUFFER | self::DBO_TRX | self::DBO_DDLMODE
+ );
+
/**
* @note exceptions for missing libraries/drivers should be thrown in initConnection()
* @param array $params Parameters passed from Database::factory()
/**
* Actually connect to the database over the wire (or to local files)
*
- * @throws InvalidArgumentException
* @throws DBConnectionError
* @since 1.31
*/
protected function doInitConnection() {
- if ( strlen( $this->connectionParams['user'] ) ) {
- $this->open(
- $this->connectionParams['host'],
- $this->connectionParams['user'],
- $this->connectionParams['password'],
- $this->connectionParams['dbname'],
- $this->connectionParams['schema'],
- $this->connectionParams['tablePrefix']
- );
- } else {
- throw new InvalidArgumentException( "No database user provided" );
- }
+ $this->open(
+ $this->connectionParams['host'],
+ $this->connectionParams['user'],
+ $this->connectionParams['password'],
+ $this->connectionParams['dbname'],
+ $this->connectionParams['schema'],
+ $this->connectionParams['tablePrefix']
+ );
}
/**
* Open a new connection to the database (closing any existing one)
*
- * @param string $server Database server host
- * @param string $user Database user name
- * @param string $password Database user password
- * @param string $dbName Database name
+ * @param string|null $server Database server host
+ * @param string|null $user Database user name
+ * @param string|null $password Database user password
+ * @param string|null $dbName Database name
* @param string|null $schema Database schema name
* @param string $tablePrefix Table prefix
* @throws DBConnectionError
*
* This also connects to the database immediately upon object construction
*
- * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
- * @param array $p Parameter map with keys:
+ * @param string $type A possible DB type (sqlite, mysql, postgres,...)
+ * @param array $params Parameter map with keys:
* - host : The hostname of the DB server
* - user : The name of the database user the client operates under
* - password : The password for the database user
* equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas.
* - tablePrefix : Optional table prefix that is implicitly added on to all table names
* recognized in queries. This can be used in place of schemas for handle site farms.
- * - flags : Optional bitfield of DBO_* constants that define connection, protocol,
+ * - flags : Optional bit field of DBO_* constants that define connection, protocol,
* buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT
* flag in place UNLESS this this database simply acts as a key/value store.
* - driver: Optional name of a specific DB client driver. For MySQL, there is only the
* @throws InvalidArgumentException If the database driver or extension cannot be found
* @since 1.18
*/
- final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
- $class = self::getClass( $dbType, $p['driver'] ?? null );
+ final public static function factory( $type, $params = [], $connect = self::NEW_CONNECTED ) {
+ $class = self::getClass( $type, $params['driver'] ?? null );
if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
- // Resolve some defaults for b/c
- $p['host'] = $p['host'] ?? false;
- $p['user'] = $p['user'] ?? false;
- $p['password'] = $p['password'] ?? false;
- $p['dbname'] = $p['dbname'] ?? false;
- $p['flags'] = $p['flags'] ?? 0;
- $p['variables'] = $p['variables'] ?? [];
- $p['tablePrefix'] = $p['tablePrefix'] ?? '';
- $p['schema'] = $p['schema'] ?? null;
- $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
- $p['agent'] = $p['agent'] ?? '';
- if ( !isset( $p['connLogger'] ) ) {
- $p['connLogger'] = new NullLogger();
- }
- if ( !isset( $p['queryLogger'] ) ) {
- $p['queryLogger'] = new NullLogger();
- }
- $p['profiler'] = $p['profiler'] ?? null;
- if ( !isset( $p['trxProfiler'] ) ) {
- $p['trxProfiler'] = new TransactionProfiler();
- }
- if ( !isset( $p['errorLogger'] ) ) {
- $p['errorLogger'] = function ( Exception $e ) {
+ $params += [
+ 'host' => null,
+ 'user' => null,
+ 'password' => null,
+ 'dbname' => null,
+ 'schema' => null,
+ 'tablePrefix' => '',
+ 'flags' => 0,
+ 'variables' => [],
+ 'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
+ 'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname()
+ ];
+
+ $normalizedParams = [
+ // Configuration
+ 'host' => strlen( $params['host'] ) ? $params['host'] : null,
+ 'user' => strlen( $params['user'] ) ? $params['user'] : null,
+ 'password' => is_string( $params['password'] ) ? $params['password'] : null,
+ 'dbname' => strlen( $params['dbname'] ) ? $params['dbname'] : null,
+ 'schema' => strlen( $params['schema'] ) ? $params['schema'] : null,
+ 'tablePrefix' => (string)$params['tablePrefix'],
+ 'flags' => (int)$params['flags'],
+ 'variables' => $params['variables'],
+ 'cliMode' => (bool)$params['cliMode'],
+ 'agent' => (string)$params['agent'],
+ // Objects and callbacks
+ 'profiler' => $params['profiler'] ?? null,
+ 'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
+ 'connLogger' => $params['connLogger'] ?? new NullLogger(),
+ 'queryLogger' => $params['queryLogger'] ?? new NullLogger(),
+ 'errorLogger' => $params['errorLogger'] ?? function ( Exception $e ) {
trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
- };
- }
- if ( !isset( $p['deprecationLogger'] ) ) {
- $p['deprecationLogger'] = function ( $msg ) {
+ },
+ 'deprecationLogger' => $params['deprecationLogger'] ?? function ( $msg ) {
trigger_error( $msg, E_USER_DEPRECATED );
- };
- }
+ }
+ ] + $params;
/** @var Database $conn */
- $conn = new $class( $p );
- if ( $connect == self::NEW_CONNECTED ) {
+ $conn = new $class( $normalizedParams );
+ if ( $connect === self::NEW_CONNECTED ) {
$conn->initConnection();
}
} else {
return null;
}
- public function setLBInfo( $name, $value = null ) {
- if ( is_null( $value ) ) {
- $this->lbInfo = $name;
+ public function setLBInfo( $nameOrArray, $value = null ) {
+ if ( is_array( $nameOrArray ) ) {
+ $this->lbInfo = $nameOrArray;
+ } elseif ( is_string( $nameOrArray ) ) {
+ if ( $value !== null ) {
+ $this->lbInfo[$nameOrArray] = $value;
+ } else {
+ unset( $this->lbInfo[$nameOrArray] );
+ }
} else {
- $this->lbInfo[$name] = $value;
+ throw new InvalidArgumentException( "Got non-string key" );
}
}
return $this->lazyMasterHandle;
}
- public function implicitGroupby() {
- return true;
- }
-
public function implicitOrderby() {
return true;
}
return $this->lastQuery;
}
- public function doneWrites() {
- return (bool)$this->lastWriteTime;
- }
-
public function lastDoneWrites() {
return $this->lastWriteTime ?: false;
}
}
public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
- if ( ( $flag & self::DBO_IGNORE ) ) {
- throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
+ if ( $flag & ~static::$DBO_MUTABLE ) {
+ throw new DBUnexpectedError(
+ $this,
+ "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
+ );
}
if ( $remember === self::REMEMBER_PRIOR ) {
array_push( $this->priorFlags, $this->flags );
}
+
$this->flags |= $flag;
}
public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
- if ( ( $flag & self::DBO_IGNORE ) ) {
- throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
+ if ( $flag & ~static::$DBO_MUTABLE ) {
+ throw new DBUnexpectedError(
+ $this,
+ "Got $flag (allowed: " . implode( ', ', static::$MUTABLE_FLAGS ) . ')'
+ );
}
if ( $remember === self::REMEMBER_PRIOR ) {
array_push( $this->priorFlags, $this->flags );
}
+
$this->flags &= ~$flag;
}
}
public function getFlag( $flag ) {
- return (bool)( $this->flags & $flag );
- }
-
- /**
- * @param string $name Class field name
- * @return mixed
- * @deprecated Since 1.28
- */
- public function getProperty( $name ) {
- return $this->$name;
+ return ( ( $this->flags & $flag ) === $flag );
}
public function getDomainID() {
return $this->currentDomain->getId();
}
- final public function getWikiID() {
- return $this->getDomainID();
- }
-
/**
* Get information about an index into an object
* @param string $table Table name
$closed = true; // already closed; nothing to do
}
- $this->conn = false;
+ $this->conn = null;
// Throw any unexpected errors after having disconnected
if ( $exception instanceof Exception ) {
*
* @param string $sql Original SQL query
* @param string $fname Name of the calling function
- * @param int $flags Bitfield of class QUERY_* constants
+ * @param int $flags Bit field of class QUERY_* constants
* @return array An n-tuple of:
* - mixed|bool: An object, resource, or true on success; false on failure
* - string: The result of calling lastError()
* @param string $commentedSql SQL query with debugging/trace comment
* @param bool $isPermWrite Whether the query is a (non-temporary table) write
* @param string $fname Name of the calling function
- * @param int $flags Bitfield of class QUERY_* constants
+ * @param int $flags Bit field of class QUERY_* constants
* @return array An n-tuple of:
* - mixed|bool: An object, resource, or true on success; false on failure
* - string: The result of calling lastError()
if ( $ignore ) {
$this->queryLogger->debug( "SQL ERROR (ignored): $error" );
} else {
- $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
-
- throw $exception;
+ throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
}
}
* @return DBError
*/
private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
- $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
$this->queryLogger->error(
"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
$this->getLogContext( [
'method' => __METHOD__,
'errno' => $errno,
'error' => $error,
- 'sql1line' => $sql1line,
+ 'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ),
'fname' => $fname,
'trace' => ( new RuntimeException() )->getTraceAsString()
] )
);
- $this->queryLogger->debug( "SQL ERROR: " . $error . "" );
+
if ( $this->wasQueryTimeout( $error, $errno ) ) {
$e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
} elseif ( $this->wasConnectionError( $errno ) ) {
return $e;
}
+ /**
+ * @param string $error
+ * @return DBConnectionError
+ */
+ final protected function newExceptionAfterConnectError( $error ) {
+ // Connection was not fully initialized and is not safe for use
+ $this->conn = null;
+
+ $this->connLogger->error(
+ "Error connecting to {db_server} as user {db_user}: {error}",
+ $this->getLogContext( [
+ 'error' => $error,
+ 'trace' => ( new RuntimeException() )->getTraceAsString()
+ ] )
+ );
+
+ return new DBConnectionError( $this, $error );
+ }
+
public function freeResult( $res ) {
}
*/
protected function replaceLostConnection( $fname ) {
$this->closeConnection();
- $this->conn = false;
+ $this->conn = null;
$this->handleSessionLossPreconnect();
$this->server,
$this->user,
$this->password,
- $this->getDBname(),
- $this->dbSchema(),
+ $this->currentDomain->getDatabase(),
+ $this->currentDomain->getSchema(),
$this->tablePrefix()
);
$this->lastPing = microtime( true );
$fname = false,
callable $inputCallback = null
) {
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$fp = fopen( $filename, 'r' );
- Wikimedia\restoreWarnings();
+ AtEase::restoreWarnings();
if ( $fp === false ) {
throw new RuntimeException( "Could not open \"{$filename}\"" );
*/
protected function getReadOnlyReason() {
$reason = $this->getLBInfo( 'readOnlyReason' );
+ if ( is_string( $reason ) ) {
+ return $reason;
+ } elseif ( $this->getLBInfo( 'replica' ) ) {
+ return "Server is configured in the role of a read-only replica database.";
+ }
- return is_string( $reason ) ? $reason : false;
+ return false;
}
public function setTableAliases( array $aliases ) {
if ( $this->isOpen() ) {
// Open a new connection resource without messing with the old one
- $this->conn = false;
+ $this->conn = null;
$this->trxEndCallbacks = []; // don't copy
$this->trxSectionCancelCallbacks = []; // don't copy
$this->handleSessionLossPreconnect(); // no trx or locks anymore
$this->server,
$this->user,
$this->password,
- $this->getDBname(),
- $this->dbSchema(),
+ $this->currentDomain->getDatabase(),
+ $this->currentDomain->getSchema(),
$this->tablePrefix()
);
$this->lastPing = microtime( true );
if ( $this->conn ) {
// Avoid connection leaks for sanity. Normally, resources close at script completion.
// The connection might already be closed in zend/hhvm by now, so suppress warnings.
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$this->closeConnection();
- Wikimedia\restoreWarnings();
- $this->conn = false;
+ AtEase::restoreWarnings();
+ $this->conn = null;
}
}
}