/** @var array Map of (name => 1) for locks obtained via lock() */
private $mNamedLocksHeld = [];
+ /** @var array Map of (table name => 1) for TEMPORARY tables */
+ protected $mSessionTempTables = [];
/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
private $lazyMasterHandle;
protected $trxProfiler;
/**
- * Constructor.
- *
- * FIXME: It is possible to construct a Database object with no associated
- * connection object, by specifying no parameters to __construct(). This
- * feature is deprecated and should be removed.
+ * Constructor and database handle and attempt to connect to the DB server
*
* IDatabase classes should not be constructed directly in external
- * code. DatabaseBase::factory() should be used instead.
+ * code. Database::factory() should be used instead.
*
- * @param array $params Parameters passed from DatabaseBase::factory()
+ * @param array $params Parameters passed from Database::factory()
*/
function __construct( array $params ) {
$server = $params['host'];
$user = $params['user'];
$password = $params['password'];
$dbName = $params['dbname'];
- $flags = $params['flags'];
$this->mSchema = $params['schema'];
$this->mTablePrefix = $params['tablePrefix'];
- $this->cliMode = isset( $params['cliMode'] )
- ? $params['cliMode']
- : ( PHP_SAPI === 'cli' );
- $this->agent = isset( $params['agent'] )
- ? str_replace( '/', '-', $params['agent'] ) // escape for comment
- : '';
+ $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->mFlags = $flags;
+ $this->mFlags = $params['flags'];
if ( $this->mFlags & DBO_DEFAULT ) {
if ( $this->cliMode ) {
$this->mFlags &= ~DBO_TRX;
? $params['srvCache']
: new HashBagOStuff();
- $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
- $this->trxProfiler = isset( $params['trxProfiler'] )
- ? $params['trxProfiler']
- : new TransactionProfiler();
- $this->connLogger = isset( $params['connLogger'] )
- ? $params['connLogger']
- : new \Psr\Log\NullLogger();
- $this->queryLogger = isset( $params['queryLogger'] )
- ? $params['queryLogger']
- : new \Psr\Log\NullLogger();
+ $this->profiler = $params['profiler'];
+ $this->trxProfiler = $params['trxProfiler'];
+ $this->connLogger = $params['connLogger'];
+ $this->queryLogger = $params['queryLogger'];
+
+ // Set initial dummy domain until open() sets the final DB/prefix
+ $this->currentDomain = DatabaseDomain::newUnspecified();
if ( $user ) {
$this->open( $server, $user, $password, $dbName );
- }
-
- $this->currentDomain = ( $this->mDBname != '' )
- ? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
- : DatabaseDomain::newUnspecified();
- }
-
- /**
- * Given a DB type, construct the name of the appropriate child class of
- * IDatabase. This is designed to replace all of the manual stuff like:
- * $class = 'Database' . ucfirst( strtolower( $dbType ) );
- * as well as validate against the canonical list of DB types we have
- *
- * This factory function is mostly useful for when you need to connect to a
- * database other than the MediaWiki default (such as for external auth,
- * an extension, et cetera). Do not use this to connect to the MediaWiki
- * database. Example uses in core:
- * @see LoadBalancer::reallyOpenConnection()
- * @see ForeignDBRepo::getMasterDB()
- * @see WebInstallerDBConnect::execute()
- *
- * @since 1.18
- *
- * @param string $dbType A possible DB type
- * @param array $p An array of options to pass to the constructor.
- * Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
- * @return IDatabase|null If the database driver or extension cannot be found
+ } elseif ( $this->requiresDatabaseUser() ) {
+ throw new InvalidArgumentException( "No database user provided." );
+ }
+
+ // Set the domain object after open() sets the relevant fields
+ if ( $this->mDBname != '' ) {
+ // Domains with server scope but a table prefix are not used by IDatabase classes
+ $this->currentDomain = new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix );
+ }
+ }
+
+ /**
+ * Construct a Database subclass instance given a database type and parameters
+ *
+ * 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:
+ * - 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
+ * - dbname : The name of the database to use where queries do not specify one.
+ * The database must exist or an error might be thrown. Setting this to the empty string
+ * will avoid any such errors and make the handle have no implicit database scope. This is
+ * useful for queries like SHOW STATUS, CREATE DATABASE, or DROP DATABASE. Note that a
+ * "database" in Postgres is rougly equivalent to an entire MySQL server. This the domain
+ * in which user names and such are defined, e.g. users are database-specific in Postgres.
+ * - schema : The database schema to use (if supported). A "schema" in Postgres is roughly
+ * 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,
+ * 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 the old
+ * 'mysql' driver and the newer 'mysqli' driver.
+ * - variables: Optional map of session variables to set after connecting. This can be
+ * used to adjust lock timeouts or encoding modes and the like.
+ * - connLogger: Optional PSR-3 logger interface instance.
+ * - queryLogger: Optional PSR-3 logger interface instance.
+ * - profiler: Optional class name or object with profileIn()/profileOut() methods.
+ * These will be called in query(), using a simplified version of the SQL that also
+ * includes the agent as a SQL comment.
+ * - trxProfiler: Optional TransactionProfiler instance.
+ * - errorLogger: Optional callback that takes an Exception and logs it.
+ * - 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.
+ * @return Database|null If the database driver or extension cannot be found
* @throws InvalidArgumentException If the database driver or extension cannot be found
+ * @since 1.18
*/
final public static function factory( $dbType, $p = [] ) {
- $canonicalDBTypes = [
+ static $canonicalDBTypes = [
'mysql' => [ 'mysqli', 'mysql' ],
'postgres' => [],
'sqlite' => [],
$p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
$p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
$p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
- $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
-
- $conn = new $class( $p );
- if ( isset( $p['connLogger'] ) ) {
- $conn->connLogger = $p['connLogger'];
+ $p['cliMode'] = isset( $p['cliMode'] ) ? $p['cliMode'] : ( PHP_SAPI === 'cli' );
+ $p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
+ if ( !isset( $p['connLogger'] ) ) {
+ $p['connLogger'] = new \Psr\Log\NullLogger();
}
- if ( isset( $p['queryLogger'] ) ) {
- $conn->queryLogger = $p['queryLogger'];
+ if ( !isset( $p['queryLogger'] ) ) {
+ $p['queryLogger'] = new \Psr\Log\NullLogger();
}
- if ( isset( $p['errorLogger'] ) ) {
- $conn->errorLogger = $p['errorLogger'];
- } else {
- $conn->errorLogger = function ( Exception $e ) {
+ $p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
+ if ( !isset( $p['trxProfiler'] ) ) {
+ $p['trxProfiler'] = new TransactionProfiler();
+ }
+ if ( !isset( $p['errorLogger'] ) ) {
+ $p['errorLogger'] = function ( Exception $e ) {
trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
};
}
+
+ $conn = new $class( $p );
} else {
$conn = null;
}
}
/**
- * Create a log context to pass to PSR logging functions.
+ * Create a log context to pass to PSR-3 logger functions.
*
* @param array $extras Additional data to add to context
* @return array
return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
}
+ /**
+ * @param string $sql A SQL query
+ * @return bool Whether $sql is SQL for creating/dropping a new TEMPORARY table
+ */
+ protected function registerTempTableOperation( $sql ) {
+ if ( preg_match(
+ '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+ $sql,
+ $matches
+ ) ) {
+ $this->mSessionTempTables[$matches[1]] = 1;
+
+ return true;
+ } elseif ( preg_match(
+ '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+ $sql,
+ $matches
+ ) ) {
+ unset( $this->mSessionTempTables[$matches[1]] );
+
+ return true;
+ } elseif ( preg_match(
+ '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
+ $sql,
+ $matches
+ ) ) {
+ return isset( $this->mSessionTempTables[$matches[1]] );
+ }
+
+ return false;
+ }
+
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
$priorWritesPending = $this->writesOrCallbacksPending();
$this->mLastQuery = $sql;
- $isWrite = $this->isWriteQuery( $sql );
+ $isWrite = $this->isWriteQuery( $sql ) && !$this->registerTempTableOperation( $sql );
if ( $isWrite ) {
$reason = $this->getReadOnlyReason();
if ( $reason !== false ) {
$lastError = $this->lastError();
$lastErrno = $this->lastErrno();
# Update state tracking to reflect transaction loss due to disconnection
- $this->handleTransactionLoss();
+ $this->handleSessionLoss();
if ( $this->reconnect() ) {
$msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
$this->connLogger->warning( $msg );
$tempIgnore = false; // not recoverable
}
# Update state tracking to reflect transaction loss
- $this->handleTransactionLoss();
+ $this->handleSessionLoss();
}
$this->reportQueryError(
$this->trxProfiler->recordQueryCompletion(
$queryProf, $startTime, $isWrite, $this->affectedRows()
);
- MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
+ $this->queryLogger->debug( $sql, [
+ 'method' => $fname,
+ 'master' => $isMaster,
+ 'runtime' => $queryRuntime,
+ ] );
return $ret;
}
return true;
}
- private function handleTransactionLoss() {
+ private function handleSessionLoss() {
$this->mTrxLevel = 0;
$this->mTrxIdleCallbacks = []; // bug 65263
$this->mTrxPreCommitCallbacks = []; // bug 65263
+ $this->mSessionTempTables = [];
+ $this->mNamedLocksHeld = [];
try {
// Handle callbacks in mTrxEndCallbacks
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
}
if ( isset( $options['HAVING'] ) ) {
$having = is_array( $options['HAVING'] )
- ? $this->makeList( $options['HAVING'], LIST_AND )
+ ? $this->makeList( $options['HAVING'], self::LIST_AND )
: $options['HAVING'];
$sql .= ' HAVING ' . $having;
}
if ( !empty( $conds ) ) {
if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
+ $conds = $this->makeList( $conds, self::LIST_AND );
}
$sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex WHERE $conds $preLimitTail";
} else {
}
public function tableExists( $table, $fname = __METHOD__ ) {
+ $tableRaw = $this->tableName( $table, 'raw' );
+ if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
+ return true; // already known to exist
+ }
+
$table = $this->tableName( $table );
$old = $this->ignoreErrors( true );
$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
$table = $this->tableName( $table );
$opts = $this->makeUpdateOptions( $options );
- $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
+ $sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
if ( $conds !== [] && $conds !== '*' ) {
- $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+ $sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
}
return $this->query( $sql, $fname );
}
- public function makeList( $a, $mode = LIST_COMMA ) {
+ public function makeList( $a, $mode = self::LIST_COMMA ) {
if ( !is_array( $a ) ) {
throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
}
foreach ( $a as $field => $value ) {
if ( !$first ) {
- if ( $mode == LIST_AND ) {
+ if ( $mode == self::LIST_AND ) {
$list .= ' AND ';
- } elseif ( $mode == LIST_OR ) {
+ } elseif ( $mode == self::LIST_OR ) {
$list .= ' OR ';
} else {
$list .= ',';
$first = false;
}
- if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
+ if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
$list .= "($value)";
- } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
+ } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
$list .= "$value";
- } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
+ } elseif (
+ ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
+ ) {
// Remove null from array to be handled separately if found
$includeNull = false;
foreach ( array_keys( $value, null, true ) as $nullKey ) {
unset( $value[$nullKey] );
}
if ( count( $value ) == 0 && !$includeNull ) {
- throw new InvalidArgumentException( __METHOD__ . ": empty input for field $field" );
+ throw new InvalidArgumentException(
+ __METHOD__ . ": empty input for field $field" );
} elseif ( count( $value ) == 0 ) {
// only check if $field is null
$list .= "$field IS NULL";
}
}
} elseif ( $value === null ) {
- if ( $mode == LIST_AND || $mode == LIST_OR ) {
+ if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
$list .= "$field IS ";
- } elseif ( $mode == LIST_SET ) {
+ } elseif ( $mode == self::LIST_SET ) {
$list .= "$field = ";
}
$list .= 'NULL';
} else {
- if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
+ if (
+ $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
+ ) {
$list .= "$field = ";
}
- $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
+ $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
}
}
if ( count( $sub ) ) {
$conds[] = $this->makeList(
[ $baseKey => $base, $subKey => array_keys( $sub ) ],
- LIST_AND );
+ self::LIST_AND );
}
}
if ( $conds ) {
- return $this->makeList( $conds, LIST_OR );
+ return $this->makeList( $conds, self::LIST_OR );
} else {
// Nothing to search for...
return false;
$tableClause .= ' ' . $ignore;
}
}
- $on = $this->makeList( (array)$conds, LIST_AND );
+ $on = $this->makeList( (array)$conds, self::LIST_AND );
if ( $on != '' ) {
$tableClause .= ' ON (' . $on . ')';
}
}
if ( $s === null ) {
return 'NULL';
+ } elseif ( is_bool( $s ) ) {
+ return (int)$s;
} else {
# This will also quote numeric values. This should be harmless,
# and protects against weird problems that occur when they really
foreach ( $index as $column ) {
$rowKey[$column] = $row[$column];
}
- $clauses[] = $this->makeList( $rowKey, LIST_AND );
+ $clauses[] = $this->makeList( $rowKey, self::LIST_AND );
}
}
- $where = [ $this->makeList( $clauses, LIST_OR ) ];
+ $where = [ $this->makeList( $clauses, self::LIST_OR ) ];
} else {
$where = false;
}
$joinTable = $this->tableName( $joinTable );
$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
if ( $conds != '*' ) {
- $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
+ $sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
}
$sql .= ')';
if ( $conds != '*' ) {
if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
+ $conds = $this->makeList( $conds, self::LIST_AND );
}
$sql .= ' WHERE ' . $conds;
}
if ( $conds != '*' ) {
if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
+ $conds = $this->makeList( $conds, self::LIST_AND );
}
$sql .= " WHERE $conds";
}
public function conditional( $cond, $trueVal, $falseVal ) {
if ( is_array( $cond ) ) {
- $cond = $this->makeList( $cond, LIST_AND );
+ $cond = $this->makeList( $cond, self::LIST_AND );
}
return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
$this->tableAliases = $aliases;
}
+ /**
+ * @return bool Whether a DB user is required to access the DB
+ * @since 1.28
+ */
+ protected function requiresDatabaseUser() {
+ return true;
+ }
+
/**
* @since 1.19
* @return string
return (string)$this->mConn;
}
+ /**
+ * Make sure that copies do not share the same client binding handle
+ * @throws DBConnectionError
+ */
+ public function __clone() {
+ $this->connLogger->warning(
+ "Cloning " . get_class( $this ) . " is not recomended; forking connection:\n" .
+ ( new RuntimeException() )->getTraceAsString()
+ );
+
+ if ( $this->isOpen() ) {
+ // Open a new connection resource without messing with the old one
+ $this->mOpened = false;
+ $this->mConn = false;
+ $this->mTrxLevel = 0; // no trx anymore
+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ $this->lastPing = microtime( true );
+ }
+ }
+
/**
* Called by serialize. Throw an exception when DB connection is serialized.
* This causes problems on some database engines because the connection is
}
/**
- * Run a few simple sanity checks
+ * Run a few simple sanity checks and close dangling connections
*/
public function __destruct() {
if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
$fnames = implode( ', ', $danglingWriters );
trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
}
+
+ if ( $this->mConn ) {
+ // Avoid connection leaks for sanity
+ $this->closeConnection();
+ $this->mConn = false;
+ $this->mOpened = false;
+ }
}
}