rdbms: Database::selectDB() update the domain and handle failure better
authorAaron Schulz <aschulz@wikimedia.org>
Tue, 14 Aug 2018 23:44:41 +0000 (16:44 -0700)
committerAaron Schulz <aschulz@wikimedia.org>
Wed, 10 Oct 2018 19:03:30 +0000 (12:03 -0700)
LoadBalancer uses Database::getDomainId() for deciding which keys to use
in the foreign connection handle arrays. This method should reflect any
changes made to the DB selection.

If the query fails, then do not change domain field. This is the sort of
approach that LoadBalancer is expects in openForeignConnection(). Also,
throw an exception when selectDB() fails.

The db/schema/prefix fields of Database no longer exist in favor of just
using the newer currentDomain field.

Also:
* Add IDatabase::selectDomain() method and made selectDB() wrap it.
* Extract the DB name from sqlite files if not explicitly provided.
* Fix inconsistent open() return values from Database subclasses.
* Make a relationSchemaQualifier() method to handle the concern of
  omitting schema names in queries. The means that getDomainId() can
  still return the right value, rather than confusingly omitt the schema.
* Make RevisionStore::checkDatabaseWikiId() account for the domain schema.
  Unlike d2a4d614fce09c, this does not incorrectly assume the storage is
  always for the current wiki domain. Also, LBFactorySingle sets the local
  domain so it is defined even in install.php.
* Make RevisionStoreDbTestBase actually set the LoadBalancer local domain.
* Make RevisionTest::testLoadFromTitle() account for the domain schema.

Bug: T193565
Change-Id: I6e51cd54c6da78830b38906b8c46789c79498ab5

20 files changed:
includes/Revision/RevisionStore.php
includes/Storage/NameTableStoreFactory.php
includes/db/DatabaseOracle.php
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMssql.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabaseMysqli.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/Storage/NameTableStoreFactoryTest.php
tests/phpunit/includes/db/DatabaseTestHelper.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php

index bef566d..6d3b72c 100644 (file)
@@ -2145,8 +2145,8 @@ class RevisionStore
                        return;
                }
 
-               // XXX: we really want the default database ID...
-               $storeWiki = $storeWiki ?: wfWikiID();
+               $storeWiki = $storeWiki ?: $this->loadBalancer->getLocalDomainID();
+               // @FIXME: when would getDomainID() be false here?
                $dbWiki = $dbWiki ?: wfWikiID();
 
                if ( $dbWiki === $storeWiki ) {
index 02ea9a7..ec364f9 100644 (file)
@@ -94,9 +94,10 @@ class NameTableStoreFactory {
                if ( !isset( $infos[$tableName] ) ) {
                        throw new \InvalidArgumentException( "Invalid table name \$tableName" );
                }
-               if ( $wiki === wfWikiID() ) {
+               if ( $wiki === $this->lbFactory->getLocalDomainID() ) {
                        $wiki = false;
                }
+
                if ( isset( $this->stores[$tableName][$wiki] ) ) {
                        return $this->stores[$tableName][$wiki];
                }
index 876b9bb..343c7a6 100644 (file)
 
 use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DatabaseDomain;
 use Wikimedia\Rdbms\Blob;
 use Wikimedia\Rdbms\ResultWrapper;
 use Wikimedia\Rdbms\DBConnectionError;
 use Wikimedia\Rdbms\DBUnexpectedError;
+use Wikimedia\Rdbms\DBExpectedError;
 
 /**
  * @ingroup Database
@@ -81,8 +83,9 @@ class DatabaseOracle extends Database {
                return false;
        }
 
-       function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                global $wgDBOracleDRCP;
+
                if ( !function_exists( 'oci_connect' ) ) {
                        throw new DBConnectionError(
                                $this,
@@ -94,20 +97,15 @@ class DatabaseOracle extends Database {
                $this->close();
                $this->user = $user;
                $this->password = $password;
-               // changed internal variables functions
-               // mServer now holds the TNS endpoint
-               // mDBname is schema name if different from username
                if ( !$server ) {
-                       // backward compatibillity (server used to be null and TNS was supplied in dbname)
+                       // Backward compatibility (server used to be null and TNS was supplied in dbname)
                        $this->server = $dbName;
-                       $this->dbName = $user;
+                       $realDatabase = $user;
                } else {
+                       // $server now holds the TNS endpoint
                        $this->server = $server;
-                       if ( !$dbName ) {
-                               $this->dbName = $user;
-                       } else {
-                               $this->dbName = $dbName;
-                       }
+                       // $dbName is schema name if different from username
+                       $realDatabase = $dbName ?: $user;
                }
 
                if ( !strlen( $user ) ) { # e.g. the class is being loaded
@@ -148,9 +146,15 @@ class DatabaseOracle extends Database {
                }
                Wikimedia\restoreWarnings();
 
-               if ( $this->user != $this->dbName ) {
+               if ( $this->user != $realDatabase ) {
                        // change current schema in session
-                       $this->selectDB( $this->dbName );
+                       $this->selectDB( $realDatabase );
+               } else {
+                       $this->currentDomain = new DatabaseDomain(
+                               $realDatabase,
+                               null,
+                               $tablePrefix
+                       );
                }
 
                if ( !$this->conn ) {
@@ -654,8 +658,8 @@ class DatabaseOracle extends Database {
                                        atc.table_name
                                ) || '_' ||
                                atc.column_name || '_SEQ' = '{$this->tablePrefix}' || asq.sequence_name
-                               AND asq.sequence_owner = upper('{$this->dbName}')
-                               AND atc.owner = upper('{$this->dbName}')" );
+                               AND asq.sequence_owner = upper('{$this->getDBname()}')
+                               AND atc.owner = upper('{$this->getDBname()}')" );
 
                        while ( ( $row = $result->fetchRow() ) !== false ) {
                                $this->sequenceData[$row[1]] = [
@@ -735,7 +739,7 @@ class DatabaseOracle extends Database {
                        $listWhere = ' AND table_name LIKE \'' . strtoupper( $prefix ) . '%\'';
                }
 
-               $owner = strtoupper( $this->dbName );
+               $owner = strtoupper( $this->getDBname() );
                $result = $this->doQuery( "SELECT table_name FROM all_tables " .
                        "WHERE owner='$owner' AND table_name NOT LIKE '%!_IDX\$_' ESCAPE '!' $listWhere" );
 
@@ -813,7 +817,7 @@ class DatabaseOracle extends Database {
                $table = $this->tableName( $table );
                $table = strtoupper( $this->removeIdentifierQuotes( $table ) );
                $index = strtoupper( $index );
-               $owner = strtoupper( $this->dbName );
+               $owner = strtoupper( $this->getDBname() );
                $sql = "SELECT 1 FROM all_indexes WHERE owner='$owner' AND index_name='{$table}_{$index}'";
                $res = $this->doQuery( $sql );
                if ( $res ) {
@@ -835,7 +839,7 @@ class DatabaseOracle extends Database {
        function tableExists( $table, $fname = __METHOD__ ) {
                $table = $this->tableName( $table );
                $table = $this->addQuotes( strtoupper( $this->removeIdentifierQuotes( $table ) ) );
-               $owner = $this->addQuotes( strtoupper( $this->dbName ) );
+               $owner = $this->addQuotes( strtoupper( $this->getDBname() ) );
                $sql = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table";
                $res = $this->doQuery( $sql );
                if ( $res && $res->numRows() > 0 ) {
@@ -1031,23 +1035,33 @@ class DatabaseOracle extends Database {
                return true;
        }
 
-       function selectDB( $db ) {
-               $this->dbName = $db;
-               if ( $db == null || $db == $this->user ) {
+       protected function doSelectDomain( DatabaseDomain $domain ) {
+               if ( $domain->getSchema() !== null ) {
+                       // We use the *database* aspect of $domain for schema, not the domain schema
+                       throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+               }
+
+               $database = $domain->getDatabase();
+               if ( $database === null || $database === $this->user ) {
+                       // Backward compatibility
+                       $this->currentDomain = $domain;
+
                        return true;
                }
-               $sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper( $db );
+
+               // https://docs.oracle.com/javadb/10.8.3.0/ref/rrefsqlj32268.html
+               $encDatabase = $this->addIdentifierQuotes( strtoupper( $database ) );
+               $sql = "ALTER SESSION SET CURRENT_SCHEMA=$encDatabase";
                $stmt = oci_parse( $this->conn, $sql );
                Wikimedia\suppressWarnings();
                $success = oci_execute( $stmt );
                Wikimedia\restoreWarnings();
-               if ( !$success ) {
+               if ( $success ) {
+                       // Update that domain fields on success (no exception thrown)
+                       $this->currentDomain = $domain;
+               } else {
                        $e = oci_error( $stmt );
-                       if ( $e['code'] != '1435' ) {
-                               $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
-                       }
-
-                       return false;
+                       $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
                }
 
                return true;
@@ -1332,10 +1346,6 @@ class DatabaseOracle extends Database {
                return 'BITOR(' . $fieldLeft . ', ' . $fieldRight . ')';
        }
 
-       function getDBname() {
-               return $this->dbName;
-       }
-
        function getServer() {
                return $this->server;
        }
index ba251ba..f693dd5 100644 (file)
@@ -375,6 +375,11 @@ class DBConnRef implements IDatabase {
                throw new DBUnexpectedError( $this, "Database selection is disallowed to enable reuse." );
        }
 
+       public function selectDomain( $domain ) {
+               // Disallow things that might confuse the LoadBalancer tracking
+               throw new DBUnexpectedError( $this, "Database selection is disallowed to enable reuse." );
+       }
+
        public function getDBname() {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
index a091242..91cb881 100644 (file)
@@ -81,8 +81,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected $user;
        /** @var string Password used to establish the current connection */
        protected $password;
-       /** @var string Database that this instance is currently connected to */
-       protected $dbName;
        /** @var array[] Map of (table => (dbname, schema, prefix) map) */
        protected $tableAliases = [];
        /** @var string[] Map of (index alias => index) */
@@ -120,10 +118,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var bool Whether to suppress triggering of transaction end callbacks */
        protected $trxEndCallbacksSuppressed = false;
 
-       /** @var string */
-       protected $tablePrefix = '';
-       /** @var string */
-       protected $schema = '';
        /** @var int */
        protected $flags;
        /** @var array */
@@ -291,13 +285,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @param array $params Parameters passed from Database::factory()
         */
        protected function __construct( array $params ) {
-               foreach ( [ 'host', 'user', 'password', 'dbname' ] as $name ) {
+               foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
                        $this->connectionParams[$name] = $params[$name];
                }
 
-               $this->schema = $params['schema'];
-               $this->tablePrefix = $params['tablePrefix'];
-
                $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'] );
@@ -329,7 +320,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                // Set initial dummy domain until open() sets the final DB/prefix
-               $this->currentDomain = DatabaseDomain::newUnspecified();
+               $this->currentDomain = new DatabaseDomain(
+                       $params['dbname'] != '' ? $params['dbname'] : null,
+                       $params['schema'] != '' ? $params['schema'] : null,
+                       $params['tablePrefix']
+               );
        }
 
        /**
@@ -346,11 +341,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
                // 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
-                       $this->currentDomain = new DatabaseDomain( $this->dbName, null, $this->tablePrefix );
-               }
        }
 
        /**
@@ -366,7 +356,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->connectionParams['host'],
                                $this->connectionParams['user'],
                                $this->connectionParams['password'],
-                               $this->connectionParams['dbname']
+                               $this->connectionParams['dbname'],
+                               $this->connectionParams['schema'],
+                               $this->connectionParams['tablePrefix']
                        );
                } else {
                        throw new InvalidArgumentException( "No database user provided." );
@@ -380,10 +372,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @param string $user Database user name
         * @param string $password Database user password
         * @param string $dbName Database name
+        * @param string|null $schema Database schema name
+        * @param string $tablePrefix Table prefix
         * @return bool
         * @throws DBConnectionError
         */
-       abstract protected function open( $server, $user, $password, $dbName );
+       abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
 
        /**
         * Construct a Database subclass instance given a database type and parameters
@@ -441,7 +435,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $p['flags'] = $p['flags'] ?? 0;
                        $p['variables'] = $p['variables'] ?? [];
                        $p['tablePrefix'] = $p['tablePrefix'] ?? '';
-                       $p['schema'] = $p['schema'] ?? '';
+                       $p['schema'] = $p['schema'] ?? null;
                        $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
                        $p['agent'] = $p['agent'] ?? '';
                        if ( !isset( $p['connLogger'] ) ) {
@@ -599,24 +593,37 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function tablePrefix( $prefix = null ) {
-               $old = $this->tablePrefix;
+               $old = $this->currentDomain->getTablePrefix();
                if ( $prefix !== null ) {
-                       $this->tablePrefix = $prefix;
-                       $this->currentDomain = ( $this->dbName != '' )
-                               ? new DatabaseDomain( $this->dbName, null, $this->tablePrefix )
-                               : DatabaseDomain::newUnspecified();
+                       $this->currentDomain = new DatabaseDomain(
+                               $this->currentDomain->getDatabase(),
+                               $this->currentDomain->getSchema(),
+                               $prefix
+                       );
                }
 
                return $old;
        }
 
        public function dbSchema( $schema = null ) {
-               $old = $this->schema;
+               $old = $this->currentDomain->getSchema();
                if ( $schema !== null ) {
-                       $this->schema = $schema;
+                       $this->currentDomain = new DatabaseDomain(
+                               $this->currentDomain->getDatabase(),
+                               // DatabaseDomain uses null for unspecified schemas
+                               strlen( $schema ) ? $schema : null,
+                               $this->currentDomain->getTablePrefix()
+                       );
                }
 
-               return $old;
+               return (string)$old;
+       }
+
+       /**
+        * @return string Schema to use to qualify relations in queries
+        */
+       protected function relationSchemaQualifier() {
+               return $this->dbSchema();
        }
 
        public function getLBInfo( $name = null ) {
@@ -900,7 +907,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return array_merge(
                        [
                                'db_server' => $this->server,
-                               'db_name' => $this->dbName,
+                               'db_name' => $this->getDBname(),
                                'db_user' => $this->user,
                        ],
                        $extras
@@ -2287,17 +2294,26 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return false;
        }
 
-       public function selectDB( $db ) {
-               # Stub. Shouldn't cause serious problems if it's not overridden, but
-               # if your database engine supports a concept similar to MySQL's
-               # databases you may as well.
-               $this->dbName = $db;
+       final public function selectDB( $db ) {
+               $this->selectDomain( new DatabaseDomain(
+                       $db,
+                       $this->currentDomain->getSchema(),
+                       $this->currentDomain->getTablePrefix()
+               ) );
 
                return true;
        }
 
+       final public function selectDomain( $domain ) {
+               $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
+       }
+
+       protected function doSelectDomain( DatabaseDomain $domain ) {
+               $this->currentDomain = $domain;
+       }
+
        public function getDBname() {
-               return $this->dbName;
+               return $this->currentDomain->getDatabase();
        }
 
        public function getServer() {
@@ -2382,14 +2398,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $database = $this->tableAliases[$table]['dbname'];
                                $schema = is_string( $this->tableAliases[$table]['schema'] )
                                        ? $this->tableAliases[$table]['schema']
-                                       : $this->schema;
+                                       : $this->relationSchemaQualifier();
                                $prefix = is_string( $this->tableAliases[$table]['prefix'] )
                                        ? $this->tableAliases[$table]['prefix']
-                                       : $this->tablePrefix;
+                                       : $this->tablePrefix();
                        } else {
                                $database = '';
-                               $schema = $this->schema; # Default schema
-                               $prefix = $this->tablePrefix; # Default prefix
+                               $schema = $this->relationSchemaQualifier(); # Default schema
+                               $prefix = $this->tablePrefix(); # Default prefix
                        }
                }
 
@@ -4109,7 +4125,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->opened = false;
                $this->conn = false;
                try {
-                       $this->open( $this->server, $this->user, $this->password, $this->dbName );
+                       $this->open(
+                               $this->server,
+                               $this->user,
+                               $this->password,
+                               $this->getDBname(),
+                               $this->dbSchema(),
+                               $this->tablePrefix()
+                       );
                        $this->lastPing = microtime( true );
                        $ok = true;
 
@@ -4643,7 +4666,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->conn = false;
                        $this->trxEndCallbacks = []; // don't copy
                        $this->handleSessionLoss(); // no trx or locks anymore
-                       $this->open( $this->server, $this->user, $this->password, $this->dbName );
+                       $this->open(
+                               $this->server,
+                               $this->user,
+                               $this->password,
+                               $this->getDBname(),
+                               $this->dbSchema(),
+                               $this->tablePrefix()
+                       );
                        $this->lastPing = microtime( true );
                }
        }
index 1246e44..61367f5 100644 (file)
@@ -77,7 +77,7 @@ class DatabaseMssql extends Database {
                parent::__construct( $params );
        }
 
-       protected function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                # Test for driver support, to avoid suppressed fatal error
                if ( !function_exists( 'sqlsrv_connect' ) ) {
                        throw new DBConnectionError(
@@ -96,11 +96,10 @@ class DatabaseMssql extends Database {
                $this->server = $server;
                $this->user = $user;
                $this->password = $password;
-               $this->dbName = $dbName;
 
                $connectionInfo = [];
 
-               if ( $dbName ) {
+               if ( $dbName != '' ) {
                        $connectionInfo['Database'] = $dbName;
                }
 
@@ -120,6 +119,11 @@ class DatabaseMssql extends Database {
                }
 
                $this->opened = true;
+               $this->currentDomain = new DatabaseDomain(
+                       ( $dbName != '' ) ? $dbName : null,
+                       null,
+                       $tablePrefix
+               );
 
                return (bool)$this->conn;
        }
@@ -1006,7 +1010,7 @@ class DatabaseMssql extends Database {
                }
 
                if ( $schema === false ) {
-                       $schema = $this->schema;
+                       $schema = $this->dbSchema();
                }
 
                $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES
@@ -1167,18 +1171,13 @@ class DatabaseMssql extends Database {
                        $s );
        }
 
-       /**
-        * @param string $db
-        * @return bool
-        */
-       public function selectDB( $db ) {
-               try {
-                       $this->dbName = $db;
-                       $this->query( "USE $db" );
-                       return true;
-               } catch ( Exception $e ) {
-                       return false;
-               }
+       protected function doSelectDomain( DatabaseDomain $domain ) {
+               $encDatabase = $this->addIdentifierQuotes( $domain->getDatabase() );
+               $this->query( "USE $encDatabase" );
+               // Update that domain fields on success (no exception thrown)
+               $this->currentDomain = $domain;
+
+               return true;
        }
 
        /**
@@ -1306,8 +1305,8 @@ class DatabaseMssql extends Database {
        private function populateColumnCaches() {
                $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
                        [
-                               'TABLE_CATALOG' => $this->dbName,
-                               'TABLE_SCHEMA' => $this->schema,
+                               'TABLE_CATALOG' => $this->getDBname(),
+                               'TABLE_SCHEMA' => $this->dbSchema(),
                                'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
                        ] );
 
index 4bd607c..cc9e98f 100644 (file)
@@ -120,18 +120,17 @@ abstract class DatabaseMysqlBase extends Database {
                return 'mysql';
        }
 
-       protected function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                # Close/unset connection handle
                $this->close();
 
                $this->server = $server;
                $this->user = $user;
                $this->password = $password;
-               $this->dbName = $dbName;
 
                $this->installErrorHandler();
                try {
-                       $this->conn = $this->mysqlConnect( $this->server );
+                       $this->conn = $this->mysqlConnect( $this->server, $dbName );
                } catch ( Exception $ex ) {
                        $this->restoreErrorHandler();
                        throw $ex;
@@ -156,20 +155,9 @@ abstract class DatabaseMysqlBase extends Database {
                }
 
                if ( strlen( $dbName ) ) {
-                       Wikimedia\suppressWarnings();
-                       $success = $this->selectDB( $dbName );
-                       Wikimedia\restoreWarnings();
-                       if ( !$success ) {
-                               $error = $this->lastError();
-                               $this->queryLogger->error(
-                                       "Error selecting database {db_name} on server {db_server}: {error}",
-                                       $this->getLogContext( [
-                                               'method' => __METHOD__,
-                                               'error' => $error,
-                                       ] )
-                               );
-                               throw new DBConnectionError( $this, "Error selecting database $dbName: $error" );
-                       }
+                       $this->selectDomain( new DatabaseDomain( $dbName, null, $tablePrefix ) );
+               } else {
+                       $this->currentDomain = new DatabaseDomain( null, null, $tablePrefix );
                }
 
                // Tell the server what we're communicating with
@@ -240,10 +228,11 @@ abstract class DatabaseMysqlBase extends Database {
         * Open a connection to a MySQL server
         *
         * @param string $realServer
+        * @param string|null $dbName
         * @return mixed Raw connection
         * @throws DBConnectionError
         */
-       abstract protected function mysqlConnect( $realServer );
+       abstract protected function mysqlConnect( $realServer, $dbName );
 
        /**
         * Set the character set of the MySQL link
@@ -1513,7 +1502,7 @@ abstract class DatabaseMysqlBase extends Database {
         */
        public function listViews( $prefix = null, $fname = __METHOD__ ) {
                // The name of the column containing the name of the VIEW
-               $propertyName = 'Tables_in_' . $this->dbName;
+               $propertyName = 'Tables_in_' . $this->getDBname();
 
                // Query for the VIEWS
                $res = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
index 6d9dabd..ad9b0a4 100644 (file)
@@ -53,10 +53,11 @@ class DatabaseMysqli extends DatabaseMysqlBase {
 
        /**
         * @param string $realServer
+        * @param string|null $dbName
         * @return bool|mysqli
         * @throws DBConnectionError
         */
-       protected function mysqlConnect( $realServer ) {
+       protected function mysqlConnect( $realServer, $dbName ) {
                # Avoid suppressed fatal error, which is very hard to track down
                if ( !function_exists( 'mysqli_init' ) ) {
                        throw new DBConnectionError( $this, "MySQLi functions missing,"
@@ -111,9 +112,15 @@ class DatabaseMysqli extends DatabaseMysqlBase {
                }
                $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, 3 );
 
-               if ( $mysqli->real_connect( $realServer, $this->user,
-                       $this->password, $this->dbName, $port, $socket, $connFlags )
-               ) {
+               if ( $mysqli->real_connect(
+                       $realServer,
+                       $this->user,
+                       $this->password,
+                       $dbName,
+                       $port,
+                       $socket,
+                       $connFlags
+               ) ) {
                        return $mysqli;
                }
 
@@ -177,16 +184,22 @@ class DatabaseMysqli extends DatabaseMysqlBase {
                return $conn->affected_rows;
        }
 
-       /**
-        * @param string $db
-        * @return bool
-        */
-       function selectDB( $db ) {
+       function doSelectDomain( DatabaseDomain $domain ) {
                $conn = $this->getBindingHandle();
 
-               $this->dbName = $db;
+               if ( $domain->getSchema() !== null ) {
+                       throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+               }
+
+               $database = $domain->getDatabase();
+               if ( !$conn->select_db( $database ) ) {
+                       throw new DBExpectedError( $this, "Could not select database '$database'." );
+               }
+
+               // Update that domain fields on success (no exception thrown)
+               $this->currentDomain = $domain;
 
-               return $conn->select_db( $db );
+               return true;
        }
 
        /**
index 691a4b7..e1cd764 100644 (file)
@@ -86,7 +86,7 @@ class DatabasePostgres extends Database {
                return false;
        }
 
-       protected function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                # Test for Postgres support, to avoid suppressed fatal error
                if ( !function_exists( 'pg_connect' ) ) {
                        throw new DBConnectionError(
@@ -100,7 +100,6 @@ class DatabasePostgres extends Database {
                $this->server = $server;
                $this->user = $user;
                $this->password = $password;
-               $this->dbName = $dbName;
 
                $connectVars = [
                        // pg_connect() user $user as the default database. Since a database is *required*,
@@ -157,30 +156,42 @@ class DatabasePostgres extends Database {
                $this->query( "SET standard_conforming_strings = on", __METHOD__ );
                $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
 
-               $this->determineCoreSchema( $this->schema );
-               // The schema to be used is now in the search path; no need for explicit qualification
-               $this->schema = '';
+               $this->determineCoreSchema( $schema );
+               $this->currentDomain = new DatabaseDomain( $dbName, $schema, $tablePrefix );
 
-               return $this->conn;
+               return (bool)$this->conn;
+       }
+
+       protected function relationSchemaQualifier() {
+               if ( $this->coreSchema === $this->currentDomain->getSchema() ) {
+                       // The schema to be used is now in the search path; no need for explicit qualification
+                       return '';
+               }
+
+               return parent::relationSchemaQualifier();
        }
 
        public function databasesAreIndependent() {
                return true;
        }
 
-       /**
-        * Postgres doesn't support selectDB in the same way MySQL does. So if the
-        * DB name doesn't match the open connection, open a new one
-        * @param string $db
-        * @return bool
-        * @throws DBUnexpectedError
-        */
-       public function selectDB( $db ) {
-               if ( $this->dbName !== $db ) {
-                       return (bool)$this->open( $this->server, $this->user, $this->password, $db );
+       public function doSelectDomain( DatabaseDomain $domain ) {
+               if ( $this->getDBname() !== $domain->getDatabase() ) {
+                       // Postgres doesn't support selectDB in the same way MySQL does.
+                       // So if the DB name doesn't match the open connection, open a new one
+                       $this->open(
+                               $this->server,
+                               $this->user,
+                               $this->password,
+                               $domain->getDatabase(),
+                               $domain->getSchema(),
+                               $domain->getTablePrefix()
+                       );
                } else {
-                       return true;
+                       $this->currentDomain = $domain;
                }
+
+               return true;
        }
 
        /**
@@ -1320,10 +1331,6 @@ SQL;
                return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
        }
 
-       public function getDBname() {
-               return $this->dbName;
-       }
-
        public function getServer() {
                return $this->server;
        }
index 0e6240f..487e122 100644 (file)
@@ -71,6 +71,10 @@ class DatabaseSqlite extends Database {
                if ( isset( $p['dbFilePath'] ) ) {
                        $this->dbPath = $p['dbFilePath'];
                        $lockDomain = md5( $this->dbPath );
+                       // Use "X" for things like X.sqlite and ":memory:" for RAM-only DBs
+                       if ( !isset( $p['dbname'] ) || !strlen( $p['dbname'] ) ) {
+                               $p['dbname'] = preg_replace( '/\.sqlite\d?$/', '', basename( $this->dbPath ) );
+                       }
                } elseif ( isset( $p['dbDirectory'] ) ) {
                        $this->dbDir = $p['dbDirectory'];
                        $lockDomain = $p['dbname'];
@@ -109,7 +113,7 @@ class DatabaseSqlite extends Database {
         */
        public static function newStandaloneInstance( $filename, array $p = [] ) {
                $p['dbFilePath'] = $filename;
-               $p['schema'] = false;
+               $p['schema'] = null;
                $p['tablePrefix'] = '';
                /** @var DatabaseSqlite $db */
                $db = Database::factory( 'sqlite', $p );
@@ -120,7 +124,11 @@ class DatabaseSqlite extends Database {
        protected function doInitConnection() {
                if ( $this->dbPath !== null ) {
                        // Standalone .sqlite file mode.
-                       $this->openFile( $this->dbPath, $this->connectionParams['dbname'] );
+                       $this->openFile(
+                               $this->dbPath,
+                               $this->connectionParams['dbname'],
+                               $this->connectionParams['tablePrefix']
+                       );
                } elseif ( $this->dbDir !== null ) {
                        // Stock wiki mode using standard file names per DB
                        if ( strlen( $this->connectionParams['dbname'] ) ) {
@@ -128,7 +136,9 @@ class DatabaseSqlite extends Database {
                                        $this->connectionParams['host'],
                                        $this->connectionParams['user'],
                                        $this->connectionParams['password'],
-                                       $this->connectionParams['dbname']
+                                       $this->connectionParams['dbname'],
+                                       $this->connectionParams['schema'],
+                                       $this->connectionParams['tablePrefix']
                                );
                        } else {
                                // Caller will manually call open() later?
@@ -155,7 +165,7 @@ class DatabaseSqlite extends Database {
                return false;
        }
 
-       protected function open( $server, $user, $pass, $dbName ) {
+       protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
                $this->close();
                $fileName = self::generateFileName( $this->dbDir, $dbName );
                if ( !is_readable( $fileName ) ) {
@@ -163,7 +173,7 @@ class DatabaseSqlite extends Database {
                        throw new DBConnectionError( $this, "SQLite database not accessible" );
                }
                // Only $dbName is used, the other parameters are irrelevant for SQLite databases
-               $this->openFile( $fileName, $dbName );
+               $this->openFile( $fileName, $dbName, $tablePrefix );
 
                return (bool)$this->conn;
        }
@@ -173,10 +183,11 @@ class DatabaseSqlite extends Database {
         *
         * @param string $fileName
         * @param string $dbName
+        * @param string $tablePrefix
         * @throws DBConnectionError
         * @return PDO|bool SQL connection or false if failed
         */
-       protected function openFile( $fileName, $dbName ) {
+       protected function openFile( $fileName, $dbName, $tablePrefix ) {
                $err = false;
 
                $this->dbPath = $fileName;
@@ -198,7 +209,7 @@ class DatabaseSqlite extends Database {
 
                $this->opened = is_object( $this->conn );
                if ( $this->opened ) {
-                       $this->dbName = $dbName;
+                       $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
                        # Set error codes only, don't raise exceptions
                        $this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
                        # Enforce LIKE to be case sensitive, just like MySQL
index 0608253..1973322 100644 (file)
@@ -174,14 +174,15 @@ interface IDatabase {
        /**
         * Get/set the table prefix.
         * @param string|null $prefix The table prefix to set, or omitted to leave it unchanged.
-        * @return string The previous table prefix.
+        * @return string The previous table prefix
+        * @throws DBUnexpectedError
         */
        public function tablePrefix( $prefix = null );
 
        /**
         * Get/set the db schema.
         * @param string|null $schema The database schema to set, or omitted to leave it unchanged.
-        * @return string The previous db schema.
+        * @return string The previous db schema
         */
        public function dbSchema( $schema = null );
 
@@ -358,6 +359,10 @@ interface IDatabase {
        public function getFlag( $flag );
 
        /**
+        * Return the currently selected domain ID
+        *
+        * Null components (database/schema) might change once a connection is established
+        *
         * @return string
         */
        public function getDomainID();
@@ -1115,14 +1120,27 @@ interface IDatabase {
         * Change the current database
         *
         * @param string $db
-        * @return bool Success or failure
+        * @return bool True unless an exception was thrown
         * @throws DBConnectionError If databasesAreIndependent() is true and an error occurs
+        * @throws DBError
+        * @deprecated Since 1.32
         */
        public function selectDB( $db );
 
+       /**
+        * Set the current domain (database, schema, and table prefix)
+        *
+        * This will throw an error for some database types if the database unspecified
+        *
+        * @param string|DatabaseDomain $domain
+        * @since 1.32
+        * @throws DBConnectionError
+        */
+       public function selectDomain( $domain );
+
        /**
         * Get the current DB name
-        * @return string
+        * @return string|null
         */
        public function getDBname();
 
index b4c7c8f..d84ba65 100644 (file)
@@ -974,12 +974,11 @@ class LoadBalancer implements ILoadBalancer {
         * @param int $i Server index
         * @param string $domain Domain ID to open
         * @param int $flags Class CONN_* constant bitfield
-        * @return Database
+        * @return Database|bool Returns false on connection error
+        * @throws DBError When database selection fails
         */
        private function openForeignConnection( $i, $domain, $flags = 0 ) {
                $domainInstance = DatabaseDomain::newFromId( $domain );
-               $dbName = $domainInstance->getDatabase();
-               $prefix = $domainInstance->getTablePrefix();
                $autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
 
                if ( $autoCommit ) {
@@ -990,6 +989,7 @@ class LoadBalancer implements ILoadBalancer {
                        $connInUseKey = self::KEY_FOREIGN_INUSE;
                }
 
+               /** @var Database $conn */
                if ( isset( $this->conns[$connInUseKey][$i][$domain] ) ) {
                        // Reuse an in-use connection for the same domain
                        $conn = $this->conns[$connInUseKey][$i][$domain];
@@ -1004,19 +1004,18 @@ class LoadBalancer implements ILoadBalancer {
                        // Reuse a free connection from another domain
                        $conn = reset( $this->conns[$connFreeKey][$i] );
                        $oldDomain = key( $this->conns[$connFreeKey][$i] );
-                       if ( strlen( $dbName ) && !$conn->selectDB( $dbName ) ) {
-                               $this->lastError = "Error selecting database '$dbName' on server " .
-                                       $conn->getServer() . " from client host {$this->hostname}";
-                               $this->errorConnection = $conn;
-                               $conn = false;
+                       if ( $domainInstance->getDatabase() !== null ) {
+                               $conn->selectDomain( $domainInstance );
                        } else {
-                               $conn->tablePrefix( $prefix );
-                               unset( $this->conns[$connFreeKey][$i][$oldDomain] );
-                               // Note that if $domain is an empty string, getDomainID() might not match it
-                               $this->conns[$connInUseKey][$i][$conn->getDomainId()] = $conn;
-                               $this->connLogger->debug( __METHOD__ .
-                                       ": reusing free connection from $oldDomain for $domain" );
+                               // Stay on the current database, but update the schema/prefix
+                               $conn->dbSchema( $domainInstance->getSchema() );
+                               $conn->tablePrefix( $domainInstance->getTablePrefix() );
                        }
+                       unset( $this->conns[$connFreeKey][$i][$oldDomain] );
+                       // Note that if $domain is an empty string, getDomainID() might not match it
+                       $this->conns[$connInUseKey][$i][$conn->getDomainId()] = $conn;
+                       $this->connLogger->debug( __METHOD__ .
+                               ": reusing free connection from $oldDomain for $domain" );
                } else {
                        if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) {
                                throw new InvalidArgumentException( "No server with index '$i'." );
index 5c0af11..2161b66 100644 (file)
@@ -77,7 +77,7 @@ class LoadBalancerSingle extends LoadBalancer {
                ) );
        }
 
-       protected function reallyOpenConnection( array $server, DatabaseDomain $domainOverride ) {
+       protected function reallyOpenConnection( array $server, DatabaseDomain $domain ) {
                return $this->db;
        }
 }
index 355d2ce..9e6d054 100644 (file)
@@ -25,6 +25,7 @@ use TestUserRegistry;
 use Title;
 use WANObjectCache;
 use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DatabaseDomain;
 use Wikimedia\Rdbms\DatabaseSqlite;
 use Wikimedia\Rdbms\FakeResultWrapper;
 use Wikimedia\Rdbms\LoadBalancer;
@@ -132,9 +133,13 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
         */
        private function getLoadBalancerMock( array $server ) {
+               $domain = new DatabaseDomain( $server['dbname'], null, $server['tablePrefix'] );
+
                $lb = $this->getMockBuilder( LoadBalancer::class )
                        ->setMethods( [ 'reallyOpenConnection' ] )
-                       ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] )
+                       ->setConstructorArgs( [
+                               [ 'servers' => [ $server ], 'localDomain' => $domain ]
+                       ] )
                        ->getMock();
 
                $lb->method( 'reallyOpenConnection' )->willReturnCallback(
index 1ae27ff..1dbb298 100644 (file)
@@ -622,10 +622,11 @@ class RevisionTest extends MediaWikiTestCase {
                        'rev_content_model' => 'GOATMODEL',
                ];
 
+               $domain = MediaWikiServices::getInstance()->getDBLoadBalancer()->getLocalDomainID();
                $db = $this->getMock( IDatabase::class );
                $db->expects( $this->any() )
                        ->method( 'getDomainId' )
-                       ->will( $this->returnValue( wfWikiID() ) );
+                       ->will( $this->returnValue( $domain ) );
                $db->expects( $this->once() )
                        ->method( 'selectRow' )
                        ->with(
index f377993..ef7f2f5 100644 (file)
@@ -18,9 +18,15 @@ class NameTableStoreFactoryTest extends MediaWikiTestCase {
        /**
         * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
         */
-       private function getMockLoadBalancer() {
-               return $this->getMockBuilder( ILoadBalancer::class )
+       private function getMockLoadBalancer( $localDomain ) {
+               $mock = $this->getMockBuilder( ILoadBalancer::class )
                        ->disableOriginalConstructor()->getMock();
+
+               $mock->expects( $this->any() )
+                       ->method( 'getLocalDomainID' )
+                       ->willReturn( $localDomain );
+
+               return $mock;
        }
 
        /**
@@ -30,11 +36,16 @@ class NameTableStoreFactoryTest extends MediaWikiTestCase {
                $mock = $this->getMockBuilder( ILBFactory::class )
                        ->disableOriginalConstructor()->getMock();
 
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $localDomain = $lbFactory->getLocalDomainID();
+
+               $mock->expects( $this->any() )->method( 'getLocalDomainID' )->willReturn( $localDomain );
+
                $mock->expects( $this->once() )
                        ->method( 'getMainLB' )
                        ->with( $this->equalTo( $expectedWiki ) )
-                       ->willReturnCallback( function ( $domain ) use ( $expectedWiki ) {
-                               return $this->getMockLoadBalancer();
+                       ->willReturnCallback( function ( $domain ) use ( $localDomain ) {
+                               return $this->getMockLoadBalancer( $localDomain );
                        } );
 
                return $mock;
@@ -68,12 +79,9 @@ class NameTableStoreFactoryTest extends MediaWikiTestCase {
        /** @dataProvider provideTestGet */
        public function testGet( $tableName, $wiki, $expectedWiki ) {
                $services = MediaWikiServices::getInstance();
-               $db = wfGetDB( DB_MASTER );
-               if ( $wiki === false ) {
-                       $wiki2 = $db->getWikiID();
-               } else {
-                       $wiki2 = $wiki;
-               }
+               $wiki2 = ( $wiki === false )
+                       ? $services->getDBLoadBalancerFactory()->getLocalDomainID()
+                       : $wiki;
                $names = new NameTableStoreFactory(
                        $this->getMockLoadBalancerFactory( $expectedWiki ),
                        $services->getMainWANObjectCache(),
index 0795609..936bee0 100644 (file)
@@ -57,7 +57,7 @@ class DatabaseTestHelper extends Database {
                        wfWarn( $msg );
                };
                $this->currentDomain = DatabaseDomain::newUnspecified();
-               $this->open( 'localhost', 'testuser', 'password', 'testdb' );
+               $this->open( 'localhost', 'testuser', 'password', 'testdb', null, '' );
        }
 
        /**
@@ -155,7 +155,7 @@ class DatabaseTestHelper extends Database {
                return 'test';
        }
 
-       function open( $server, $user, $password, $dbName ) {
+       function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                $this->conn = (object)[ 'test' ];
 
                return true;
index 1616139..e84998c 100644 (file)
@@ -608,7 +608,15 @@ class LBFactoryTest extends MediaWikiTestCase {
                        $this->assertFalse( $db->isOpen() );
                } else {
                        \Wikimedia\suppressWarnings();
-                       $this->assertFalse( $db->selectDB( 'garbage-db' ) );
+                       try {
+                               $this->assertFalse( $db->selectDB( 'garbage-db' ) );
+                               $this->fail( "No error thrown." );
+                       } catch ( \Wikimedia\Rdbms\DBExpectedError $e ) {
+                               $this->assertEquals(
+                                       "Could not select database 'garbage-db'.",
+                                       $e->getMessage()
+                               );
+                       }
                        \Wikimedia\restoreWarnings();
                }
        }
index a86a1c9..b7dbe0b 100644 (file)
@@ -100,7 +100,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
        private function getMockForViews() {
                $db = $this->getMockBuilder( DatabaseMysqli::class )
                        ->disableOriginalConstructor()
-                       ->setMethods( [ 'fetchRow', 'query' ] )
+                       ->setMethods( [ 'fetchRow', 'query', 'getDBname' ] )
                        ->getMock();
 
                $db->method( 'query' )
@@ -110,6 +110,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
                                (object)[ 'Tables_in_' => 'view2' ],
                                (object)[ 'Tables_in_' => 'myview' ]
                        ] ) );
+               $db->method( 'getDBname' )->willReturn( '' );
 
                return $db;
        }
@@ -677,7 +678,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
        public function testIndexAliases() {
                $db = $this->getMockBuilder( DatabaseMysqli::class )
                        ->disableOriginalConstructor()
-                       ->setMethods( [ 'mysqlRealEscapeString' ] )
+                       ->setMethods( [ 'mysqlRealEscapeString', 'dbSchema', 'tablePrefix' ] )
                        ->getMock();
                $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
                        function ( $s ) {
@@ -710,7 +711,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
        public function testTableAliases() {
                $db = $this->getMockBuilder( DatabaseMysqli::class )
                        ->disableOriginalConstructor()
-                       ->setMethods( [ 'mysqlRealEscapeString' ] )
+                       ->setMethods( [ 'mysqlRealEscapeString', 'dbSchema', 'tablePrefix' ] )
                        ->getMock();
                $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
                        function ( $s ) {
index abde37a..762812c 100644 (file)
@@ -218,9 +218,10 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
         * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
         */
        public function testTransactionIdle_TRX() {
-               $db = $this->getMockDB( [ 'isOpen', 'ping' ] );
+               $db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
                $db->method( 'isOpen' )->willReturn( true );
                $db->method( 'ping' )->willReturn( true );
+               $db->method( 'getDBname' )->willReturn( '' );
                $db->setFlag( DBO_TRX );
 
                $lbFactory = LBFactorySingle::newFromConnection( $db );
@@ -311,9 +312,10 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
         * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
         */
        public function testTransactionPreCommitOrIdle_TRX() {
-               $db = $this->getMockDB( [ 'isOpen', 'ping' ] );
+               $db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
                $db->method( 'isOpen' )->willReturn( true );
                $db->method( 'ping' )->willReturn( true );
+               $db->method( 'getDBname' )->willReturn( 'unittest' );
                $db->setFlag( DBO_TRX );
 
                $lbFactory = LBFactorySingle::newFromConnection( $db );
@@ -484,8 +486,9 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
         * @covers Wikimedia\Rdbms\Database::lockIsFree
         */
        public function testGetScopedLock() {
-               $db = $this->getMockDB( [ 'isOpen' ] );
+               $db = $this->getMockDB( [ 'isOpen', 'getDBname' ] );
                $db->method( 'isOpen' )->willReturn( true );
+               $db->method( 'getDBname' )->willReturn( 'unittest' );
 
                $this->assertEquals( 0, $db->trxLevel() );
                $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
@@ -625,21 +628,57 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
         * @covers Wikimedia\Rdbms\Database::tablePrefix
         * @covers Wikimedia\Rdbms\Database::dbSchema
         */
-       public function testMutators() {
+       public function testSchemaAndPrefixMutators() {
                $old = $this->db->tablePrefix();
+               $oldDomain = $this->db->getDomainId();
                $this->assertInternalType( 'string', $old, 'Prefix is string' );
-               $this->assertEquals( $old, $this->db->tablePrefix(), "Prefix unchanged" );
-               $this->assertEquals( $old, $this->db->tablePrefix( 'xxx' ) );
-               $this->assertEquals( 'xxx', $this->db->tablePrefix(), "Prefix set" );
+               $this->assertSame( $old, $this->db->tablePrefix(), "Prefix unchanged" );
+               $this->assertSame( $old, $this->db->tablePrefix( 'xxx' ) );
+               $this->assertSame( 'xxx', $this->db->tablePrefix(), "Prefix set" );
                $this->db->tablePrefix( $old );
                $this->assertNotEquals( 'xxx', $this->db->tablePrefix() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
 
                $old = $this->db->dbSchema();
+               $oldDomain = $this->db->getDomainId();
                $this->assertInternalType( 'string', $old, 'Schema is string' );
-               $this->assertEquals( $old, $this->db->dbSchema(), "Schema unchanged" );
-               $this->assertEquals( $old, $this->db->dbSchema( 'xxx' ) );
-               $this->assertEquals( 'xxx', $this->db->dbSchema(), "Schema set" );
+               $this->assertSame( $old, $this->db->dbSchema(), "Schema unchanged" );
+               $this->assertSame( $old, $this->db->dbSchema( 'xxx' ) );
+               $this->assertSame( 'xxx', $this->db->dbSchema(), "Schema set" );
                $this->db->dbSchema( $old );
                $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::selectDomain
+        */
+       public function testSelectDomain() {
+               $oldDomain = $this->db->getDomainId();
+               $oldDatabase = $this->db->getDBname();
+               $oldSchema = $this->db->dbSchema();
+               $oldPrefix = $this->db->tablePrefix();
+
+               $this->db->selectDomain( 'testselectdb-xxx' );
+               $this->assertSame( 'testselectdb', $this->db->getDBname() );
+               $this->assertSame( '', $this->db->dbSchema() );
+               $this->assertSame( 'xxx', $this->db->tablePrefix() );
+
+               $this->db->selectDomain( $oldDomain );
+               $this->assertSame( $oldDatabase, $this->db->getDBname() );
+               $this->assertSame( $oldSchema, $this->db->dbSchema() );
+               $this->assertSame( $oldPrefix, $this->db->tablePrefix() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
+
+               $this->db->selectDomain( 'testselectdb-schema-xxx' );
+               $this->assertSame( 'testselectdb', $this->db->getDBname() );
+               $this->assertSame( 'schema', $this->db->dbSchema() );
+               $this->assertSame( 'xxx', $this->db->tablePrefix() );
+
+               $this->db->selectDomain( $oldDomain );
+               $this->assertSame( $oldDatabase, $this->db->getDBname() );
+               $this->assertSame( $oldSchema, $this->db->dbSchema() );
+               $this->assertSame( $oldPrefix, $this->db->tablePrefix() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
        }
 }