'Wikimedia\\Rdbms\\DBQueryError' => __DIR__ . '/includes/libs/rdbms/exception/DBQueryError.php',
'Wikimedia\\Rdbms\\DBQueryTimeoutError' => __DIR__ . '/includes/libs/rdbms/exception/DBQueryTimeoutError.php',
'Wikimedia\\Rdbms\\DBReadOnlyError' => __DIR__ . '/includes/libs/rdbms/exception/DBReadOnlyError.php',
+ 'Wikimedia\\Rdbms\\DBReadOnlyRoleError' => __DIR__ . '/includes/libs/rdbms/exception/DBReadOnlyRoleError.php',
'Wikimedia\\Rdbms\\DBReplicationWaitError' => __DIR__ . '/includes/libs/rdbms/exception/DBReplicationWaitError.php',
'Wikimedia\\Rdbms\\DBTransactionError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionError.php',
'Wikimedia\\Rdbms\\DBTransactionSizeError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionSizeError.php',
* Helper class to handle automatically marking connections as reusable (via RAII pattern)
* as well handling deferring the actual network connection until the handle is used
*
- * @note: proxy methods are defined explicitly to avoid interface errors
* @ingroup Database
* @since 1.22
*/
private $conn;
/** @var array|null N-tuple of (server index, group, DatabaseDomain|string) */
private $params;
+ /** @var int One of DB_MASTER/DB_REPLICA */
+ private $role;
const FLD_INDEX = 0;
const FLD_GROUP = 1;
/**
* @param ILoadBalancer $lb Connection manager for $conn
- * @param Database|array $conn Database handle or (server index, query groups, domain, flags)
+ * @param Database|array $conn Database or (server index, query groups, domain, flags)
+ * @param int $role The type of connection asked for; one of DB_MASTER/DB_REPLICA
+ * @internal This method should not be called outside of LoadBalancer
*/
- public function __construct( ILoadBalancer $lb, $conn ) {
+ public function __construct( ILoadBalancer $lb, $conn, $role ) {
$this->lb = $lb;
+ $this->role = $role;
if ( $conn instanceof Database ) {
$this->conn = $conn; // live handle
} elseif ( is_array( $conn ) && count( $conn ) >= 4 && $conn[self::FLD_DOMAIN] !== false ) {
return $this->conn->$name( ...$arguments );
}
+ /**
+ * @return int DB_MASTER when this *requires* the master DB, otherwise DB_REPLICA
+ * @since 1.33
+ */
+ public function getReferenceRole() {
+ return $this->role;
+ }
+
public function getServerInfo() {
return $this->__call( __FUNCTION__, func_get_args() );
}
}
public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
- return $this->__call( __FUNCTION__, func_get_args() );
+ if ( $this->role !== ILoadBalancer::DB_MASTER ) {
+ $flags |= IDatabase::QUERY_REPLICA_ROLE;
+ }
+
+ return $this->__call( __FUNCTION__, [ $sql, $fname, $flags ] );
}
public function freeResult( $res ) {
public function lockForUpdate(
$table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
}
public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
}
public function nextSequenceValue( $seqName ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function upsert(
$table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__
) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function deleteJoin(
$delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function delete( $table, $conds, $fname = __METHOD__ ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
$destTable, $srcTable, $varMap, $conds,
$fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
}
public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
+ // DB_REPLICA role: caller might want to refresh cache after a REPEATABLE-READ snapshot
return $this->__call( __FUNCTION__, func_get_args() );
}
public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
+ // DB_REPLICA role: caller might want to refresh cache after a REPEATABLE-READ snapshot
return $this->__call( __FUNCTION__, func_get_args() );
}
public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
- return $this->__call( __FUNCTION__, func_get_args() );
+ return $this->onTransactionCommitOrIdle( $callback, $fname );
}
public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
+ // DB_REPLICA role: caller might want to refresh cache after a cache mutex is released
return $this->__call( __FUNCTION__, func_get_args() );
}
public function startAtomic(
$fname = __METHOD__, $cancelable = IDatabase::ATOMIC_NOT_CANCELABLE
) {
+ // Don't call assertRoleAllowsWrites(); caller might want a REPEATABLE-READ snapshot
return $this->__call( __FUNCTION__, func_get_args() );
}
public function endAtomic( $fname = __METHOD__ ) {
+ // Don't call assertRoleAllowsWrites(); caller might want a REPEATABLE-READ snapshot
return $this->__call( __FUNCTION__, func_get_args() );
}
public function cancelAtomic( $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null ) {
+ // Don't call assertRoleAllowsWrites(); caller might want a REPEATABLE-READ snapshot
return $this->__call( __FUNCTION__, func_get_args() );
}
public function doAtomicSection(
$fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
) {
+ // Don't call assertRoleAllowsWrites(); caller might want a REPEATABLE-READ snapshot
return $this->__call( __FUNCTION__, func_get_args() );
}
}
public function lockIsFree( $lockName, $method ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function lock( $lockName, $method, $timeout = 5 ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function unlock( $lockName, $method ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
return $this->__call( __FUNCTION__, func_get_args() );
}
+ /**
+ * Error out if the role is not DB_MASTER
+ *
+ * Note that the underlying connection may or may not itself be read-only.
+ * It could even be to a writable master (both server-side and to the application).
+ * This error is meant for the case when a DB_REPLICA handle was requested but a
+ * a write was attempted on that handle regardless.
+ *
+ * In configurations where the master DB has some generic read load or is the only server,
+ * DB_MASTER/DB_REPLICA will sometimes (or always) use the same connection to the master DB.
+ * This does not effect the role of DBConnRef instances.
+ * @throws DBReadOnlyRoleError
+ */
+ protected function assertRoleAllowsWrites() {
+ // DB_MASTER is "prima facie" writable
+ if ( $this->role !== ILoadBalancer::DB_MASTER ) {
+ throw new DBReadOnlyRoleError( $this->conn, "Cannot write with role DB_REPLICA" );
+ }
+ }
+
/**
* Clean up the connection when out of scope
*/
*/
protected function assertIsWritableMaster() {
if ( $this->getLBInfo( 'replica' ) === true ) {
- throw new DBUnexpectedError(
+ throw new DBReadOnlyRoleError(
$this,
'Write operations are not allowed on replica database connections.'
);
$flags = (int)$flags; // b/c; this field used to be a bool
$ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
- $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
$priorTransaction = $this->trxLevel;
$priorWritesPending = $this->writesOrCallbacksPending();
$this->assertIsWritableMaster();
# Do not treat temporary table writes as "meaningful writes" that need committing.
# Profile them as reads. Integration tests can override this behavior via $flags.
+ $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
$tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
$isEffectiveWrite = ( $tableType !== self::TEMP_NORMAL );
+ # DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
+ if ( $isEffectiveWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
+ throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
+ }
} else {
$isEffectiveWrite = false;
}
* permanent as far as write tracking is concerned. This is useful for testing.
*/
const QUERY_PSEUDO_PERMANENT = 2;
+ /** @var int Enforce that a query does not make effective writes */
+ const QUERY_REPLICA_ROLE = 4;
/** @var bool Parameter to unionQueries() for UNION ALL */
const UNION_ALL = true;
$fname = false,
callable $inputCallback = null
) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
$fname = __METHOD__,
callable $inputCallback = null
) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function dropTable( $tableName, $fName = __METHOD__ ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function deadlockLoop() {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function duplicateTableStructure(
$oldName, $newName, $temporary = false, $fname = __METHOD__
) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
}
public function lockTables( array $read, array $write, $method ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
public function unlockTables( $method ) {
+ $this->assertRoleAllowsWrites();
+
return $this->__call( __FUNCTION__, func_get_args() );
}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+namespace Wikimedia\Rdbms;
+
+/**
+ * Exception class for attempted DB write access to a DBConnRef with the DB_REPLICA role
+ *
+ * @ingroup Database
+ */
+class DBReadOnlyRoleError extends DBUnexpectedError {
+}
*
* @see ILoadBalancer::getConnection() for parameter information
*
- * @param int $db Server index or DB_MASTER/DB_REPLICA
+ * @param int $i Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
* @param int $flags Bitfield of CONN_* class constants (e.g. CONN_TRX_AUTOCOMMIT)
* @return MaintainableDBConnRef
*/
- public function getMaintenanceConnectionRef( $db, $groups = [], $domain = false, $flags = 0 );
+ public function getMaintenanceConnectionRef( $i, $groups = [], $domain = false, $flags = 0 );
/**
* Open a connection to the server given by the specified index
}
}
- public function getConnectionRef( $db, $groups = [], $domain = false, $flags = 0 ) {
+ public function getConnectionRef( $i, $groups = [], $domain = false, $flags = 0 ) {
$domain = $this->resolveDomainID( $domain );
+ $role = $this->getRoleFromIndex( $i );
- return new DBConnRef( $this, $this->getConnection( $db, $groups, $domain, $flags ) );
+ return new DBConnRef( $this, $this->getConnection( $i, $groups, $domain, $flags ), $role );
}
- public function getLazyConnectionRef( $db, $groups = [], $domain = false, $flags = 0 ) {
+ public function getLazyConnectionRef( $i, $groups = [], $domain = false, $flags = 0 ) {
$domain = $this->resolveDomainID( $domain );
+ $role = $this->getRoleFromIndex( $i );
- return new DBConnRef( $this, [ $db, $groups, $domain, $flags ] );
+ return new DBConnRef( $this, [ $i, $groups, $domain, $flags ], $role );
}
- public function getMaintenanceConnectionRef( $db, $groups = [], $domain = false, $flags = 0 ) {
+ public function getMaintenanceConnectionRef( $i, $groups = [], $domain = false, $flags = 0 ) {
$domain = $this->resolveDomainID( $domain );
+ $role = $this->getRoleFromIndex( $i );
return new MaintainableDBConnRef(
- $this, $this->getConnection( $db, $groups, $domain, $flags ) );
+ $this, $this->getConnection( $i, $groups, $domain, $flags ), $role );
+ }
+
+ /**
+ * @param int $i Server index or DB_MASTER/DB_REPLICA
+ * @return int One of DB_MASTER/DB_REPLICA
+ */
+ private function getRoleFromIndex( $i ) {
+ return ( $i === self::DB_MASTER || $i === $this->getWriterIndex() )
+ ? self::DB_MASTER
+ : self::DB_REPLICA;
}
public function openConnection( $i, $domain = false, $flags = 0 ) {
* @covers \Wikimedia\Rdbms\LoadBalancer
*/
class LoadBalancerTest extends MediaWikiTestCase {
- private function makeServerConfig() {
+ private function makeServerConfig( $flags = DBO_DEFAULT ) {
global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
return [
'type' => $wgDBtype,
'dbDirectory' => $wgSQLiteDataDir,
'load' => 0,
- 'flags' => DBO_TRX // REPEATABLE-READ for consistency
+ 'flags' => $flags
];
}
$called = false;
$lb = new LoadBalancer( [
- 'servers' => [ $this->makeServerConfig() ],
+ // Simulate web request with DBO_TRX
+ 'servers' => [ $this->makeServerConfig( DBO_TRX ) ],
'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
'chronologyCallback' => function () use ( &$called ) {
$this->assertSame( 'my_test_wiki', $lb->resolveDomainID( 'my_test_wiki' ) );
$this->assertSame( $ld->getId(), $lb->resolveDomainID( false ) );
$this->assertSame( $ld->getId(), $lb->resolveDomainID( $ld ) );
-
$this->assertFalse( $called );
+
$dbw = $lb->getConnection( DB_MASTER );
$this->assertTrue( $called );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
}
public function testWithReplica() {
- global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
-
- $servers = [
- [ // master
- 'host' => $wgDBserver,
- 'dbname' => $wgDBname,
- 'tablePrefix' => $this->dbPrefix(),
- 'user' => $wgDBuser,
- 'password' => $wgDBpassword,
- 'type' => $wgDBtype,
- 'dbDirectory' => $wgSQLiteDataDir,
- 'load' => 0,
- 'flags' => DBO_TRX // REPEATABLE-READ for consistency
- ],
- [ // emulated replica
- 'host' => $wgDBserver,
- 'dbname' => $wgDBname,
- 'tablePrefix' => $this->dbPrefix(),
- 'user' => $wgDBuser,
- 'password' => $wgDBpassword,
- 'type' => $wgDBtype,
- 'dbDirectory' => $wgSQLiteDataDir,
- 'load' => 100,
- 'flags' => DBO_TRX // REPEATABLE-READ for consistency
- ]
- ];
+ global $wgDBserver;
- $lb = new LoadBalancer( [
- 'servers' => $servers,
- 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
- 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
- 'loadMonitorClass' => LoadMonitorNull::class
- ] );
+ // Simulate web request with DBO_TRX
+ $lb = $this->newMultiServerLocalLoadBalancer( DBO_TRX );
$dbw = $lb->getConnection( DB_MASTER );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
$lb->closeAll();
}
+ private function newSingleServerLocalLoadBalancer() {
+ global $wgDBname;
+
+ return new LoadBalancer( [
+ 'servers' => [ $this->makeServerConfig() ],
+ 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
+ ] );
+ }
+
+ private function newMultiServerLocalLoadBalancer( $flags = DBO_DEFAULT ) {
+ global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
+
+ $servers = [
+ [ // master
+ 'host' => $wgDBserver,
+ 'dbname' => $wgDBname,
+ 'tablePrefix' => $this->dbPrefix(),
+ 'user' => $wgDBuser,
+ 'password' => $wgDBpassword,
+ 'type' => $wgDBtype,
+ 'dbDirectory' => $wgSQLiteDataDir,
+ 'load' => 0,
+ 'flags' => $flags
+ ],
+ [ // emulated replica
+ 'host' => $wgDBserver,
+ 'dbname' => $wgDBname,
+ 'tablePrefix' => $this->dbPrefix(),
+ 'user' => $wgDBuser,
+ 'password' => $wgDBpassword,
+ 'type' => $wgDBtype,
+ 'dbDirectory' => $wgSQLiteDataDir,
+ 'load' => 100,
+ 'flags' => $flags
+ ]
+ ];
+
+ return new LoadBalancer( [
+ 'servers' => $servers,
+ 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
+ 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
+ 'loadMonitorClass' => LoadMonitorNull::class
+ ] );
+ }
+
private function assertWriteForbidden( Database $db ) {
try {
$db->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__ );
$i = $lb->getWriterIndex();
$this->assertEquals( null, $lb->getAnyOpenConnection( $i ) );
+
$conn1 = $lb->getConnection( $i );
$this->assertNotEquals( null, $conn1 );
$this->assertEquals( $conn1, $lb->getAnyOpenConnection( $i ) );
+ $this->assertFalse( $conn1->getFlag( DBO_TRX ) );
+
$conn2 = $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT );
$this->assertNotEquals( null, $conn2 );
+ $this->assertFalse( $conn2->getFlag( DBO_TRX ) );
+
if ( $lb->getServerAttributes( $i )[Database::ATTR_DB_LEVEL_LOCKING] ) {
$this->assertEquals( null,
$lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
'type' => $wgDBtype,
'dbDirectory' => $wgSQLiteDataDir,
'load' => 0,
- 'flags' => DBO_TRX // REPEATABLE-READ for consistency
+ 'flags' => DBO_TRX // simulate a web request with DBO_TRX
],
];
$conn1->close();
$conn2->close();
}
+
+ public function testDBConnRefReadsMasterAndReplicaRoles() {
+ $lb = $this->newSingleServerLocalLoadBalancer();
+
+ $rConn = $lb->getConnectionRef( DB_REPLICA );
+ $wConn = $lb->getConnectionRef( DB_MASTER );
+ $wConn2 = $lb->getConnectionRef( 0 );
+
+ $v = [ 'value' => '1', '1' ];
+ $sql = 'SELECT MAX(1) AS value';
+ foreach ( [ $rConn, $wConn, $wConn2 ] as $conn ) {
+ $conn->clearFlag( $conn::DBO_TRX );
+
+ $res = $conn->query( $sql, __METHOD__ );
+ $this->assertEquals( $v, $conn->fetchRow( $res ) );
+
+ $res = $conn->query( $sql, __METHOD__, $conn::QUERY_REPLICA_ROLE );
+ $this->assertEquals( $v, $conn->fetchRow( $res ) );
+ }
+
+ $wConn->getScopedLockAndFlush( 'key', __METHOD__, 1 );
+ $wConn2->getScopedLockAndFlush( 'key2', __METHOD__, 1 );
+ }
+
+ /**
+ * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
+ */
+ public function testDBConnRefWritesReplicaRole() {
+ $lb = $this->newSingleServerLocalLoadBalancer();
+
+ $rConn = $lb->getConnectionRef( DB_REPLICA );
+
+ $rConn->query( 'DELETE FROM sometesttable WHERE 1=0' );
+ }
+
+ /**
+ * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
+ */
+ public function testDBConnRefWritesReplicaRoleIndex() {
+ $lb = $this->newMultiServerLocalLoadBalancer();
+
+ $rConn = $lb->getConnectionRef( 1 );
+
+ $rConn->query( 'DELETE FROM sometesttable WHERE 1=0' );
+ }
+
+ /**
+ * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
+ */
+ public function testDBConnRefWritesReplicaRoleInsert() {
+ $lb = $this->newMultiServerLocalLoadBalancer();
+
+ $rConn = $lb->getConnectionRef( DB_REPLICA );
+
+ $rConn->insert( 'test', [ 't' => 1 ], __METHOD__ );
+ }
}
*/
private function getDBConnRef( ILoadBalancer $lb = null ) {
$lb = $lb ?: $this->getLoadBalancerMock();
- return new DBConnRef( $lb, $this->getDatabaseMock() );
+ return new DBConnRef( $lb, $this->getDatabaseMock(), DB_MASTER );
}
public function testConstruct() {
$lb = $this->getLoadBalancerMock();
- $ref = new DBConnRef( $lb, $this->getDatabaseMock() );
+ $ref = new DBConnRef( $lb, $this->getDatabaseMock(), DB_MASTER );
$this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
}
$ref = new DBConnRef(
$lb,
- [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ]
+ [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ],
+ DB_MASTER
);
$this->assertInstanceOf( ResultWrapper::class, $ref->select( 'whatever', '*' ) );
+ $this->assertEquals( DB_MASTER, $ref->getReferenceRole() );
+
+ $ref2 = new DBConnRef(
+ $lb,
+ [ DB_MASTER, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT ],
+ DB_REPLICA
+ );
+ $this->assertEquals( DB_REPLICA, $ref2->getReferenceRole() );
}
public function testDestruct() {
$this->setExpectedException( InvalidArgumentException::class, '' );
$lb = $this->getLoadBalancerMock();
- new DBConnRef( $lb, 17 ); // bad constructor argument
+ new DBConnRef( $lb, 17, DB_REPLICA ); // bad constructor argument
}
/**
$lb->expects( $this->never() )
->method( 'getConnection' );
- $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ] );
+ $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
$this->assertSame( 'dummy', $ref->getDomainID() );
}
$this->assertInternalType( 'string', $ref->__toString() );
$lb = $this->getLoadBalancerMock();
- $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'test', 0 ] );
+ $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'test', 0 ], DB_MASTER );
$this->assertInternalType( 'string', $ref->__toString() );
}
*/
public function testClose() {
$lb = $this->getLoadBalancerMock();
- $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ] );
+ $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_MASTER );
$ref->close();
}
+
+ /**
+ * @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole
+ */
+ public function testGetReferenceRole() {
+ $lb = $this->getLoadBalancerMock();
+ $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
+ $this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
+
+ $ref = new DBConnRef( $lb, [ DB_MASTER, [], 'dummy', 0 ], DB_MASTER );
+ $this->assertSame( DB_MASTER, $ref->getReferenceRole() );
+
+ $ref = new DBConnRef( $lb, [ 1, [], 'dummy', 0 ], DB_REPLICA );
+ $this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
+
+ $ref = new DBConnRef( $lb, [ 0, [], 'dummy', 0 ], DB_MASTER );
+ $this->assertSame( DB_MASTER, $ref->getReferenceRole() );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole
+ * @expectedException Wikimedia\Rdbms\DBReadOnlyRoleError
+ * @dataProvider provideRoleExceptions
+ */
+ public function testRoleExceptions( $method, $args ) {
+ $lb = $this->getLoadBalancerMock();
+ $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
+ $ref->$method( ...$args );
+ }
+
+ function provideRoleExceptions() {
+ return [
+ [ 'insert', [ 'table', [ 'a' => 1 ] ] ],
+ [ 'update', [ 'table', [ 'a' => 1 ], [ 'a' => 2 ] ] ],
+ [ 'delete', [ 'table', [ 'a' => 1 ] ] ],
+ [ 'replace', [ 'table', [ 'a' ], [ 'a' => 1 ] ] ],
+ [ 'upsert', [ 'table', [ 'a' => 1 ], [ 'a' ], [ 'a = a + 1' ] ] ],
+ [ 'lock', [ 'k', 'method' ] ],
+ [ 'unlock', [ 'k', 'method' ] ],
+ [ 'getScopedLockAndFlush', [ 'k', 'method', 1 ] ]
+ ];
+ }
}