use Wikimedia;
use BagOStuff;
use HashBagOStuff;
+use LogicException;
use InvalidArgumentException;
use Exception;
use RuntimeException;
/** @var string Whether lock granularity is on the level of the entire database */
const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
+ /** @var int New Database instance will not be connected yet when returned */
+ const NEW_UNCONNECTED = 0;
+ /** @var int New Database instance will already be connected when returned */
+ const NEW_CONNECTED = 1;
+
/** @var string SQL query */
protected $lastQuery = '';
/** @var float|bool UNIX timestamp of last write query */
protected $lastWriteTime = false;
/** @var string|bool */
protected $phpError = false;
- /** @var string */
+ /** @var string Server that this instance is currently connected to */
protected $server;
- /** @var string */
+ /** @var string User that this instance is currently connected under the name of */
protected $user;
- /** @var string */
+ /** @var string Password used to establish the current connection */
protected $password;
- /** @var string */
+ /** @var string Database that this instance is currently connected to */
protected $dbName;
/** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
protected $tableAliases = [];
protected $cliMode;
/** @var string Agent name for query profiling */
protected $agent;
-
+ /** @var array Parameters used by initConnection() to establish a connection */
+ protected $connectionParams = [];
/** @var BagOStuff APC cache */
protected $srvCache;
/** @var LoggerInterface */
protected $nonNativeInsertSelectBatchSize = 10000;
/**
- * Constructor and database handle and attempt to connect to the DB server
- *
- * IDatabase classes should not be constructed directly in external
- * code. Database::factory() should be used instead.
- *
+ * @note: exceptions for missing libraries/drivers should be thrown in initConnection()
* @param array $params Parameters passed from Database::factory()
*/
- function __construct( array $params ) {
- $server = $params['host'];
- $user = $params['user'];
- $password = $params['password'];
- $dbName = $params['dbname'];
+ protected function __construct( array $params ) {
+ foreach ( [ 'host', 'user', 'password', 'dbname' ] as $name ) {
+ $this->connectionParams[$name] = $params[$name];
+ }
$this->schema = $params['schema'];
$this->tablePrefix = $params['tablePrefix'];
// Set initial dummy domain until open() sets the final DB/prefix
$this->currentDomain = DatabaseDomain::newUnspecified();
+ }
- if ( $user ) {
- $this->open( $server, $user, $password, $dbName );
- } elseif ( $this->requiresDatabaseUser() ) {
- throw new InvalidArgumentException( "No database user provided." );
+ /**
+ * Initialize the connection to the database over the wire (or to local files)
+ *
+ * @throws LogicException
+ * @throws InvalidArgumentException
+ * @throws DBConnectionError
+ * @since 1.31
+ */
+ final public function initConnection() {
+ if ( $this->isOpen() ) {
+ throw new LogicException( __METHOD__ . ': already connected.' );
}
-
+ // Establish the connection
+ $this->doInitConnection();
// Set the domain object after open() sets the relevant fields
if ( $this->dbName != '' ) {
// Domains with server scope but a table prefix are not used by IDatabase classes
}
}
+ /**
+ * 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']
+ );
+ } else {
+ throw new InvalidArgumentException( "No database user provided." );
+ }
+ }
+
/**
* Construct a Database subclass instance given a database type and parameters
*
* - 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.
+ * @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
* @since 1.18
*/
- final public static function factory( $dbType, $p = [] ) {
+ final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
$class = self::getClass( $dbType, isset( $p['driver'] ) ? $p['driver'] : null );
if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
};
}
+ /** @var Database $conn */
$conn = new $class( $p );
+ if ( $connect == self::NEW_CONNECTED ) {
+ $conn->initConnection();
+ }
} else {
$conn = null;
}
}
/**
- * The DBMS-dependent part of query()
+ * Run a query and return a DBMS-dependent wrapper (that has all IResultWrapper methods)
+ *
+ * This might return things, such as mysqli_result, that do not formally implement
+ * IResultWrapper, but nonetheless implement all of its methods correctly
*
* @param string $sql SQL query.
- * @return ResultWrapper|bool Result object to feed to fetchObject,
- * fetchRow, ...; or false on failure
+ * @return IResultWrapper|bool Iterator to feed to fetchObject/fetchRow; false on failure
*/
abstract protected function doQuery( $sql );
$this->tableAliases = $aliases;
}
- /**
- * @return bool Whether a DB user is required to access the DB
- * @since 1.28
- */
- protected function requiresDatabaseUser() {
- return true;
- }
-
/**
* Get the underlying binding connection handle
*
* This catches broken callers than catch and ignore disconnection exceptions.
* Unlike checking isOpen(), this is safe to call inside of open().
*
- * @return resource|object
+ * @return mixed
* @throws DBUnexpectedError
* @since 1.26
*/
class DatabaseMysqli extends DatabaseMysqlBase {
/**
* @param string $sql
- * @return resource
+ * @return mysqli_result
*/
protected function doQuery( $sql ) {
$conn = $this->getBindingHandle();
return (string)$this->conn;
}
}
+
+ /**
+ * @return mysqli
+ */
+ protected function getBindingHandle() {
+ return parent::getBindingHandle();
+ }
}
class_alias( DatabaseMysqli::class, 'DatabaseMysqli' );
*/
function __construct( array $p ) {
if ( isset( $p['dbFilePath'] ) ) {
- parent::__construct( $p );
- // Standalone .sqlite file mode.
- // Super doesn't open when $user is false, but we can work with $dbName,
- // which is derived from the file path in this case.
- $this->openFile( $p['dbFilePath'] );
- $lockDomain = md5( $p['dbFilePath'] );
- } elseif ( !isset( $p['dbDirectory'] ) ) {
- throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
- } else {
+ $this->dbPath = $p['dbFilePath'];
+ $lockDomain = md5( $this->dbPath );
+ } elseif ( isset( $p['dbDirectory'] ) ) {
$this->dbDir = $p['dbDirectory'];
- $this->dbName = $p['dbname'];
- $lockDomain = $this->dbName;
- // Stock wiki mode using standard file names per DB.
- parent::__construct( $p );
- // Super doesn't open when $user is false, but we can work with $dbName
- if ( $p['dbname'] && !$this->isOpen() ) {
- $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] );
- }
+ $lockDomain = $p['dbname'];
+ } else {
+ throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
}
$this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
'domain' => $lockDomain,
'lockDirectory' => "{$this->dbDir}/locks"
] );
+
+ parent::__construct( $p );
}
protected static function getAttributes() {
return $db;
}
+ protected function doInitConnection() {
+ if ( $this->dbPath !== null ) {
+ // Standalone .sqlite file mode.
+ $this->openFile( $this->dbPath );
+ } elseif ( $this->dbDir !== null ) {
+ // Stock wiki mode using standard file names per DB
+ if ( strlen( $this->connectionParams['dbname'] ) ) {
+ $this->open(
+ $this->connectionParams['host'],
+ $this->connectionParams['user'],
+ $this->connectionParams['password'],
+ $this->connectionParams['dbname']
+ );
+ } else {
+ // Caller will manually call open() later?
+ $this->connLogger->debug( __METHOD__ . ': no database opened.' );
+ }
+ } else {
+ throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
+ }
+ }
+
/**
* @return string
*/
* NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
*
* @param string $server
- * @param string $user
+ * @param string $user Unused
* @param string $pass
* @param string $dbName
*
}
$this->openFile( $fileName );
+ if ( $this->conn ) {
+ $this->dbName = $dbName;
+ }
+
return (bool)$this->conn;
}
throw new DBConnectionError( $this, $err );
}
- $this->opened = !!$this->conn;
+ $this->opened = is_object( $this->conn );
if ( $this->opened ) {
# Set error codes only, don't raise exceptions
$this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
* @return bool|ResultWrapper
*/
protected function doQuery( $sql ) {
- $res = $this->conn->query( $sql );
+ $res = $this->getBindingHandle()->query( $sql );
if ( $res === false ) {
return false;
}
*/
function insertId() {
// PDO::lastInsertId yields a string :(
- return intval( $this->conn->lastInsertId() );
+ return intval( $this->getBindingHandle()->lastInsertId() );
}
/**
* @return string Version information from the database
*/
function getServerVersion() {
- $ver = $this->conn->getAttribute( PDO::ATTR_SERVER_VERSION );
+ $ver = $this->getBindingHandle()->getAttribute( PDO::ATTR_SERVER_VERSION );
return $ver;
}
);
return "x'" . bin2hex( (string)$s ) . "'";
} else {
- return $this->conn->quote( (string)$s );
+ return $this->getBindingHandle()->quote( (string)$s );
}
}
}
}
- protected function requiresDatabaseUser() {
- return false; // just a file
- }
-
/**
* @return string
*/
public function __toString() {
- return 'SQLite ' . (string)$this->conn->getAttribute( PDO::ATTR_SERVER_VERSION );
+ return is_object( $this->conn )
+ ? 'SQLite ' . (string)$this->conn->getAttribute( PDO::ATTR_SERVER_VERSION )
+ : '(not connected)';
+ }
+
+ /**
+ * @return PDO
+ */
+ protected function getBindingHandle() {
+ return parent::getBindingHandle();
}
}
<?php
-use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\DatabaseMysqli;
use Wikimedia\Rdbms\LBFactorySingle;
use Wikimedia\Rdbms\TransactionProfiler;
use Wikimedia\TestingAccessWrapper;
+use Wikimedia\Rdbms\DatabaseSqlite;
+use Wikimedia\Rdbms\DatabasePostgres;
+use Wikimedia\Rdbms\DatabaseMssql;
class DatabaseTest extends PHPUnit\Framework\TestCase {
$this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
}
+ /**
+ * @dataProvider provideAddQuotes
+ * @covers Wikimedia\Rdbms\Database::factory
+ */
+ public function testFactory() {
+ $m = Database::NEW_UNCONNECTED; // no-connect mode
+ $p = [ 'host' => 'localhost', 'user' => 'me', 'password' => 'myself', 'dbname' => 'i' ];
+
+ $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'mysqli', $p, $m ) );
+ $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySqli', $p, $m ) );
+ $this->assertInstanceOf( DatabaseMysqli::class, Database::factory( 'MySQLi', $p, $m ) );
+ $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'postgres', $p, $m ) );
+ $this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'Postgres', $p, $m ) );
+
+ $x = $p + [ 'port' => 10000, 'UseWindowsAuth' => false ];
+ $this->assertInstanceOf( DatabaseMssql::class, Database::factory( 'mssql', $x, $m ) );
+
+ $x = $p + [ 'dbFilePath' => 'some/file.sqlite' ];
+ $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) );
+ $x = $p + [ 'dbDirectory' => 'some/file' ];
+ $this->assertInstanceOf( DatabaseSqlite::class, Database::factory( 'sqlite', $x, $m ) );
+ }
+
public static function provideAddQuotes() {
return [
[ null, 'NULL' ],