2010-12-14 16:26:35 +00:00
|
|
|
<?php
|
|
|
|
|
|
2018-03-08 04:10:13 +00:00
|
|
|
use Wikimedia\Rdbms\Database;
|
2018-08-15 08:20:30 +00:00
|
|
|
use Wikimedia\Rdbms\DatabaseDomain;
|
2018-03-08 04:10:13 +00:00
|
|
|
use Wikimedia\Rdbms\DatabaseMysqli;
|
2020-01-10 00:00:51 +00:00
|
|
|
use Wikimedia\Rdbms\DatabasePostgres;
|
|
|
|
|
use Wikimedia\Rdbms\DatabaseSqlite;
|
Permit temporary table writes on replica DB connections
In I8e17644d1b447416adee18e42cf0122b52a80b22, MediaWiki's DBAL was adjusted to
reject any write query on read-only DB replica connections. This poses a problem
for extensions that use temporary tables in their queries, as such queries now
have to be executed on the source DB rather than a replica to work around this
fact. An example of such an extension is Semantic MediaWiki, whose QueryEngine
uses temporary tables extensively in serving reads. The current situation, where
all writes, including non-persistent ones, must be executed on a source DB
connection, causes scalability issues since it's no longer possible to
distribute these queries between multiple replicas.
An old code comment in the DBAL cited MySQL bug 33669 as a potential blocker to
permitting temporary table operations on read-only connections. However, that
bug was closed a decade ago, and Fandom's Semantic MediaWiki cluster has been
permitting such operations on its MySQL 5.7 replica nodes (running with
--read-only) for several years now, without observing any adverse side-effect.
This patch accordingly relaxes the restrictions placed by the MediaWiki DBAL on
temporary table operations to enable executing them even on read-only replica DB
connections. Several unit tests were added to verify the conditions under which
a given write query may be allowed to execute on a connection.
Bug: T259362
Change-Id: I90a1427a15d0aee07e7b24ba4248b7ef4475c227
2020-07-31 15:37:26 +00:00
|
|
|
use Wikimedia\Rdbms\DBReadOnlyRoleError;
|
2020-01-10 00:00:51 +00:00
|
|
|
use Wikimedia\Rdbms\DBUnexpectedError;
|
|
|
|
|
use Wikimedia\Rdbms\IDatabase;
|
Permit temporary table writes on replica DB connections
In I8e17644d1b447416adee18e42cf0122b52a80b22, MediaWiki's DBAL was adjusted to
reject any write query on read-only DB replica connections. This poses a problem
for extensions that use temporary tables in their queries, as such queries now
have to be executed on the source DB rather than a replica to work around this
fact. An example of such an extension is Semantic MediaWiki, whose QueryEngine
uses temporary tables extensively in serving reads. The current situation, where
all writes, including non-persistent ones, must be executed on a source DB
connection, causes scalability issues since it's no longer possible to
distribute these queries between multiple replicas.
An old code comment in the DBAL cited MySQL bug 33669 as a potential blocker to
permitting temporary table operations on read-only connections. However, that
bug was closed a decade ago, and Fandom's Semantic MediaWiki cluster has been
permitting such operations on its MySQL 5.7 replica nodes (running with
--read-only) for several years now, without observing any adverse side-effect.
This patch accordingly relaxes the restrictions placed by the MediaWiki DBAL on
temporary table operations to enable executing them even on read-only replica DB
connections. Several unit tests were added to verify the conditions under which
a given write query may be allowed to execute on a connection.
Bug: T259362
Change-Id: I90a1427a15d0aee07e7b24ba4248b7ef4475c227
2020-07-31 15:37:26 +00:00
|
|
|
use Wikimedia\Rdbms\IResultWrapper;
|
2017-07-26 09:04:34 +00:00
|
|
|
use Wikimedia\Rdbms\LBFactorySingle;
|
2017-07-20 20:17:11 +00:00
|
|
|
use Wikimedia\Rdbms\TransactionProfiler;
|
|
|
|
|
use Wikimedia\TestingAccessWrapper;
|
2017-02-10 18:09:05 +00:00
|
|
|
|
2018-02-17 12:29:13 +00:00
|
|
|
class DatabaseTest extends PHPUnit\Framework\TestCase {
|
2010-12-14 16:26:35 +00:00
|
|
|
|
2017-12-29 23:22:37 +00:00
|
|
|
use MediaWikiCoversValidator;
|
|
|
|
|
|
2019-10-06 14:12:39 +00:00
|
|
|
/** @var DatabaseTestHelper */
|
|
|
|
|
private $db;
|
|
|
|
|
|
2019-10-20 18:11:08 +00:00
|
|
|
protected function setUp() : void {
|
2017-07-20 20:17:11 +00:00
|
|
|
$this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
|
2012-01-11 20:19:55 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-28 20:56:34 +00:00
|
|
|
/**
|
|
|
|
|
* @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' ];
|
|
|
|
|
|
2020-07-22 22:54:19 +00:00
|
|
|
$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 ) );
|
2018-02-28 20:56:34 +00:00
|
|
|
$this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'postgres', $p, $m ) );
|
|
|
|
|
$this->assertInstanceOf( DatabasePostgres::class, Database::factory( 'Postgres', $p, $m ) );
|
|
|
|
|
|
|
|
|
|
$x = $p + [ 'port' => 10000, 'UseWindowsAuth' => false ];
|
|
|
|
|
|
|
|
|
|
$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 ) );
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-20 20:17:11 +00:00
|
|
|
public static function provideAddQuotes() {
|
|
|
|
|
return [
|
|
|
|
|
[ null, 'NULL' ],
|
2019-11-15 05:35:33 +00:00
|
|
|
[ 1234, "1234" ],
|
2017-07-20 20:17:11 +00:00
|
|
|
[ 1234.5678, "'1234.5678'" ],
|
|
|
|
|
[ 'string', "'string'" ],
|
|
|
|
|
[ 'string\'s cause trouble', "'string\'s cause trouble'" ],
|
|
|
|
|
];
|
2010-12-14 16:26:35 +00:00
|
|
|
}
|
2016-09-15 21:40:00 +00:00
|
|
|
|
2013-10-18 10:36:09 +00:00
|
|
|
/**
|
2017-07-20 20:17:11 +00:00
|
|
|
* @dataProvider provideAddQuotes
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::addQuotes
|
2013-10-18 10:36:09 +00:00
|
|
|
*/
|
2017-07-20 20:17:11 +00:00
|
|
|
public function testAddQuotes( $input, $expected ) {
|
|
|
|
|
$this->assertEquals( $expected, $this->db->addQuotes( $input ) );
|
2010-12-14 16:26:35 +00:00
|
|
|
}
|
|
|
|
|
|
2017-07-20 20:17:11 +00:00
|
|
|
public static function provideTableName() {
|
|
|
|
|
// Formatting is mostly ignored since addIdentifierQuotes is abstract.
|
|
|
|
|
// For testing of addIdentifierQuotes, see actual Database subclas tests.
|
|
|
|
|
return [
|
|
|
|
|
'local' => [
|
|
|
|
|
'tablename',
|
|
|
|
|
'tablename',
|
|
|
|
|
'quoted',
|
|
|
|
|
],
|
|
|
|
|
'local-raw' => [
|
|
|
|
|
'tablename',
|
|
|
|
|
'tablename',
|
|
|
|
|
'raw',
|
|
|
|
|
],
|
|
|
|
|
'shared' => [
|
|
|
|
|
'sharedb.tablename',
|
|
|
|
|
'tablename',
|
|
|
|
|
'quoted',
|
|
|
|
|
[ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
|
|
|
|
|
],
|
|
|
|
|
'shared-raw' => [
|
|
|
|
|
'sharedb.tablename',
|
|
|
|
|
'tablename',
|
|
|
|
|
'raw',
|
|
|
|
|
[ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
|
|
|
|
|
],
|
|
|
|
|
'shared-prefix' => [
|
|
|
|
|
'sharedb.sh_tablename',
|
|
|
|
|
'tablename',
|
|
|
|
|
'quoted',
|
|
|
|
|
[ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
|
|
|
|
|
],
|
|
|
|
|
'shared-prefix-raw' => [
|
|
|
|
|
'sharedb.sh_tablename',
|
|
|
|
|
'tablename',
|
|
|
|
|
'raw',
|
|
|
|
|
[ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
|
|
|
|
|
],
|
|
|
|
|
'foreign' => [
|
|
|
|
|
'databasename.tablename',
|
|
|
|
|
'databasename.tablename',
|
|
|
|
|
'quoted',
|
|
|
|
|
],
|
|
|
|
|
'foreign-raw' => [
|
|
|
|
|
'databasename.tablename',
|
|
|
|
|
'databasename.tablename',
|
|
|
|
|
'raw',
|
|
|
|
|
],
|
|
|
|
|
];
|
2010-12-14 16:26:35 +00:00
|
|
|
}
|
|
|
|
|
|
2017-07-20 20:17:11 +00:00
|
|
|
/**
|
|
|
|
|
* @dataProvider provideTableName
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::tableName
|
|
|
|
|
*/
|
|
|
|
|
public function testTableName( $expected, $table, $format, array $alias = null ) {
|
|
|
|
|
if ( $alias ) {
|
|
|
|
|
$this->db->setTableAliases( [ $table => $alias ] );
|
2013-05-26 14:26:41 +00:00
|
|
|
}
|
2012-07-18 08:18:49 +00:00
|
|
|
$this->assertEquals(
|
2017-07-20 20:17:11 +00:00
|
|
|
$expected,
|
|
|
|
|
$this->db->tableName( $table, $format ?: 'quoted' )
|
2012-07-18 08:18:49 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
Database: Support parenthesized JOINs
SQL supports parentheses for grouping in the FROM clause.[1] This is
useful when you want to left-join against a join of other tables.
For example, say you have tables 'a', 'b', and 'c'. You want all rows
from 'a', along with rows from 'b' + 'c' only where both of those
exist.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) JOIN c ON (b_c = c_id)
doesn't work, it'll only give you the rows where 'c' exists.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) LEFT JOIN c ON (b_c = c_id)
doesn't work either, it'll give you rows from 'b' without a
corresponding row in 'c'. What you need to do is
SELECT * FROM a LEFT JOIN (b JOIN c ON (b_c = c_id)) ON (a_b = b_id)
This patch implements this by extending the syntax for the $table
parameter to IDatabase::select(). When passing an array of tables, if a
value in the array is itself an array that is interpreted as a request
for a parenthesized join. To produce the example above, you'd do
something like
$db->select(
[ 'a', 'nest' => [ 'b', 'c' ] ],
'*',
[],
__METHOD__,
[],
[
'c' => [ 'JOIN', 'b_c = c_id ],
'nest' => [ 'LEFT JOIN', 'a_b = b_id' ],
]
);
[1]: In standards as far back as SQL-1992 (I couldn't find an earlier
version), and it seems to be supported by at least MySQL 5.6, MariaDB
10.1.28, PostgreSQL 9.3, PostgreSQL 10.0, Oracle 11g R2, SQLite 3.20.1,
and MSSQL 2014 (from local testing and sqlfiddle.com).
Change-Id: I1e0a77381e06d885650a94f53847fb82f01c2694
2017-10-13 17:51:43 +00:00
|
|
|
public function provideTableNamesWithIndexClauseOrJOIN() {
|
|
|
|
|
return [
|
|
|
|
|
'one-element array' => [
|
|
|
|
|
[ 'table' ], [], 'table '
|
|
|
|
|
],
|
|
|
|
|
'comma join' => [
|
|
|
|
|
[ 'table1', 'table2' ], [], 'table1,table2 '
|
|
|
|
|
],
|
|
|
|
|
'real join' => [
|
|
|
|
|
[ 'table1', 'table2' ],
|
|
|
|
|
[ 'table2' => [ 'LEFT JOIN', 't1_id = t2_id' ] ],
|
|
|
|
|
'table1 LEFT JOIN table2 ON ((t1_id = t2_id))'
|
|
|
|
|
],
|
|
|
|
|
'real join with multiple conditionals' => [
|
|
|
|
|
[ 'table1', 'table2' ],
|
|
|
|
|
[ 'table2' => [ 'LEFT JOIN', [ 't1_id = t2_id', 't2_x = \'X\'' ] ] ],
|
|
|
|
|
'table1 LEFT JOIN table2 ON ((t1_id = t2_id) AND (t2_x = \'X\'))'
|
|
|
|
|
],
|
|
|
|
|
'join with parenthesized group' => [
|
|
|
|
|
[ 'table1', 'n' => [ 'table2', 'table3' ] ],
|
|
|
|
|
[
|
|
|
|
|
'table3' => [ 'JOIN', 't2_id = t3_id' ],
|
|
|
|
|
'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
|
|
|
|
|
],
|
|
|
|
|
'table1 LEFT JOIN (table2 JOIN table3 ON ((t2_id = t3_id))) ON ((t1_id = t2_id))'
|
|
|
|
|
],
|
2017-11-29 20:42:27 +00:00
|
|
|
'join with degenerate parenthesized group' => [
|
|
|
|
|
[ 'table1', 'n' => [ 't2' => 'table2' ] ],
|
|
|
|
|
[
|
|
|
|
|
'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
|
|
|
|
|
],
|
|
|
|
|
'table1 LEFT JOIN table2 t2 ON ((t1_id = t2_id))'
|
|
|
|
|
],
|
Database: Support parenthesized JOINs
SQL supports parentheses for grouping in the FROM clause.[1] This is
useful when you want to left-join against a join of other tables.
For example, say you have tables 'a', 'b', and 'c'. You want all rows
from 'a', along with rows from 'b' + 'c' only where both of those
exist.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) JOIN c ON (b_c = c_id)
doesn't work, it'll only give you the rows where 'c' exists.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) LEFT JOIN c ON (b_c = c_id)
doesn't work either, it'll give you rows from 'b' without a
corresponding row in 'c'. What you need to do is
SELECT * FROM a LEFT JOIN (b JOIN c ON (b_c = c_id)) ON (a_b = b_id)
This patch implements this by extending the syntax for the $table
parameter to IDatabase::select(). When passing an array of tables, if a
value in the array is itself an array that is interpreted as a request
for a parenthesized join. To produce the example above, you'd do
something like
$db->select(
[ 'a', 'nest' => [ 'b', 'c' ] ],
'*',
[],
__METHOD__,
[],
[
'c' => [ 'JOIN', 'b_c = c_id ],
'nest' => [ 'LEFT JOIN', 'a_b = b_id' ],
]
);
[1]: In standards as far back as SQL-1992 (I couldn't find an earlier
version), and it seems to be supported by at least MySQL 5.6, MariaDB
10.1.28, PostgreSQL 9.3, PostgreSQL 10.0, Oracle 11g R2, SQLite 3.20.1,
and MSSQL 2014 (from local testing and sqlfiddle.com).
Change-Id: I1e0a77381e06d885650a94f53847fb82f01c2694
2017-10-13 17:51:43 +00:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider provideTableNamesWithIndexClauseOrJOIN
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
|
|
|
|
|
*/
|
|
|
|
|
public function testTableNamesWithIndexClauseOrJOIN( $tables, $join_conds, $expect ) {
|
|
|
|
|
$clause = TestingAccessWrapper::newFromObject( $this->db )
|
|
|
|
|
->tableNamesWithIndexClauseOrJOIN( $tables, [], [], $join_conds );
|
|
|
|
|
$this->assertSame( $expect, $clause );
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-20 20:17:11 +00:00
|
|
|
/**
|
2018-05-09 02:28:39 +00:00
|
|
|
* @covers Wikimedia\Rdbms\Database::onTransactionCommitOrIdle
|
2017-07-20 20:17:11 +00:00
|
|
|
* @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
|
|
|
|
|
*/
|
2015-12-08 01:26:15 +00:00
|
|
|
public function testTransactionIdle() {
|
|
|
|
|
$db = $this->db;
|
|
|
|
|
|
2018-03-19 05:26:11 +00:00
|
|
|
$db->clearFlag( DBO_TRX );
|
2016-07-04 18:02:42 +00:00
|
|
|
$called = false;
|
2015-12-08 01:26:15 +00:00
|
|
|
$flagSet = null;
|
2018-04-17 04:39:02 +00:00
|
|
|
$callback = function ( $trigger, IDatabase $db ) use ( &$flagSet, &$called ) {
|
2018-03-19 05:26:11 +00:00
|
|
|
$called = true;
|
|
|
|
|
$flagSet = $db->getFlag( DBO_TRX );
|
|
|
|
|
};
|
|
|
|
|
|
2018-05-09 02:28:39 +00:00
|
|
|
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
|
2016-07-04 18:02:42 +00:00
|
|
|
$this->assertTrue( $called, 'Callback reached' );
|
2018-03-19 05:26:11 +00:00
|
|
|
$this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
|
|
|
|
|
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' );
|
2015-12-08 01:26:15 +00:00
|
|
|
|
|
|
|
|
$flagSet = null;
|
2018-03-19 05:26:11 +00:00
|
|
|
$called = false;
|
|
|
|
|
$db->startAtomic( __METHOD__ );
|
2018-05-09 02:28:39 +00:00
|
|
|
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
|
2018-03-19 05:26:11 +00:00
|
|
|
$this->assertFalse( $called, 'Callback not reached during TRX' );
|
|
|
|
|
$db->endAtomic( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
$this->assertTrue( $called, 'Callback reached after COMMIT' );
|
2015-12-08 01:26:15 +00:00
|
|
|
$this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
|
|
|
|
|
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
|
|
|
|
|
|
|
|
|
|
$db->clearFlag( DBO_TRX );
|
2018-05-09 02:28:39 +00:00
|
|
|
$db->onTransactionCommitOrIdle(
|
2018-04-17 04:39:02 +00:00
|
|
|
function ( $trigger, IDatabase $db ) {
|
2016-09-15 21:40:00 +00:00
|
|
|
$db->setFlag( DBO_TRX );
|
|
|
|
|
},
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
2015-12-08 01:26:15 +00:00
|
|
|
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
|
|
|
|
|
}
|
2016-07-04 18:02:42 +00:00
|
|
|
|
2018-03-19 05:26:11 +00:00
|
|
|
/**
|
2018-05-09 02:28:39 +00:00
|
|
|
* @covers Wikimedia\Rdbms\Database::onTransactionCommitOrIdle
|
2018-03-19 05:26:11 +00:00
|
|
|
* @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
|
|
|
|
|
*/
|
|
|
|
|
public function testTransactionIdle_TRX() {
|
2018-08-14 23:44:41 +00:00
|
|
|
$db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
|
2018-03-19 05:26:11 +00:00
|
|
|
$db->method( 'isOpen' )->willReturn( true );
|
|
|
|
|
$db->method( 'ping' )->willReturn( true );
|
2018-08-14 23:44:41 +00:00
|
|
|
$db->method( 'getDBname' )->willReturn( '' );
|
2018-03-19 05:26:11 +00:00
|
|
|
$db->setFlag( DBO_TRX );
|
|
|
|
|
|
|
|
|
|
$lbFactory = LBFactorySingle::newFromConnection( $db );
|
|
|
|
|
// Ask for the connection so that LB sets internal state
|
|
|
|
|
// about this connection being the master connection
|
|
|
|
|
$lb = $lbFactory->getMainLB();
|
|
|
|
|
$conn = $lb->openConnection( $lb->getWriterIndex() );
|
|
|
|
|
$this->assertSame( $db, $conn, 'Same DB instance' );
|
|
|
|
|
$this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
|
|
|
|
|
|
|
|
|
|
$called = false;
|
|
|
|
|
$flagSet = null;
|
|
|
|
|
$callback = function () use ( $db, &$flagSet, &$called ) {
|
|
|
|
|
$called = true;
|
|
|
|
|
$flagSet = $db->getFlag( DBO_TRX );
|
|
|
|
|
};
|
|
|
|
|
|
2018-05-09 02:28:39 +00:00
|
|
|
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
|
2018-03-19 05:26:11 +00:00
|
|
|
$this->assertTrue( $called, 'Called when idle if DBO_TRX is set' );
|
|
|
|
|
$this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
|
|
|
|
|
$this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX still default' );
|
|
|
|
|
|
|
|
|
|
$called = false;
|
|
|
|
|
$lbFactory->beginMasterChanges( __METHOD__ );
|
2018-05-09 02:28:39 +00:00
|
|
|
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
|
2018-03-19 05:26:11 +00:00
|
|
|
$this->assertFalse( $called, 'Not called when lb-transaction is active' );
|
|
|
|
|
|
|
|
|
|
$lbFactory->commitMasterChanges( __METHOD__ );
|
|
|
|
|
$this->assertTrue( $called, 'Called when lb-transaction is committed' );
|
|
|
|
|
|
|
|
|
|
$called = false;
|
|
|
|
|
$lbFactory->beginMasterChanges( __METHOD__ );
|
2018-05-09 02:28:39 +00:00
|
|
|
$db->onTransactionCommitOrIdle( $callback, __METHOD__ );
|
2018-03-19 05:26:11 +00:00
|
|
|
$this->assertFalse( $called, 'Not called when lb-transaction is active' );
|
|
|
|
|
|
|
|
|
|
$lbFactory->rollbackMasterChanges( __METHOD__ );
|
|
|
|
|
$this->assertFalse( $called, 'Not called when lb-transaction is rolled back' );
|
|
|
|
|
|
|
|
|
|
$lbFactory->commitMasterChanges( __METHOD__ );
|
|
|
|
|
$this->assertFalse( $called, 'Not called in next round commit' );
|
2018-05-25 23:02:27 +00:00
|
|
|
|
|
|
|
|
$db->setFlag( DBO_TRX );
|
|
|
|
|
try {
|
|
|
|
|
$db->onTransactionCommitOrIdle( function () {
|
|
|
|
|
throw new RuntimeException( 'test' );
|
|
|
|
|
} );
|
|
|
|
|
$this->fail( "Exception not thrown" );
|
|
|
|
|
} catch ( RuntimeException $e ) {
|
|
|
|
|
$this->assertTrue( $db->getFlag( DBO_TRX ) );
|
|
|
|
|
}
|
2018-03-19 05:26:11 +00:00
|
|
|
}
|
|
|
|
|
|
2017-07-26 09:04:34 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
|
|
|
|
|
*/
|
|
|
|
|
public function testTransactionPreCommitOrIdle() {
|
|
|
|
|
$db = $this->getMockDB( [ 'isOpen' ] );
|
|
|
|
|
$db->method( 'isOpen' )->willReturn( true );
|
|
|
|
|
$db->clearFlag( DBO_TRX );
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX is not set' );
|
|
|
|
|
|
|
|
|
|
$called = false;
|
|
|
|
|
$db->onTransactionPreCommitOrIdle(
|
2018-04-17 04:39:02 +00:00
|
|
|
function ( IDatabase $db ) use ( &$called ) {
|
2017-07-26 09:04:34 +00:00
|
|
|
$called = true;
|
|
|
|
|
},
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
$this->assertTrue( $called, 'Called when idle' );
|
|
|
|
|
|
|
|
|
|
$db->begin( __METHOD__ );
|
|
|
|
|
$called = false;
|
|
|
|
|
$db->onTransactionPreCommitOrIdle(
|
2018-04-17 04:39:02 +00:00
|
|
|
function ( IDatabase $db ) use ( &$called ) {
|
2017-07-26 09:04:34 +00:00
|
|
|
$called = true;
|
|
|
|
|
},
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
$this->assertFalse( $called, 'Not called when transaction is active' );
|
|
|
|
|
$db->commit( __METHOD__ );
|
|
|
|
|
$this->assertTrue( $called, 'Called when transaction is committed' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
|
|
|
|
|
*/
|
|
|
|
|
public function testTransactionPreCommitOrIdle_TRX() {
|
2018-08-14 23:44:41 +00:00
|
|
|
$db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
|
2017-07-26 09:04:34 +00:00
|
|
|
$db->method( 'isOpen' )->willReturn( true );
|
2018-03-19 05:26:11 +00:00
|
|
|
$db->method( 'ping' )->willReturn( true );
|
2018-08-14 23:44:41 +00:00
|
|
|
$db->method( 'getDBname' )->willReturn( 'unittest' );
|
2017-07-26 09:04:34 +00:00
|
|
|
$db->setFlag( DBO_TRX );
|
|
|
|
|
|
|
|
|
|
$lbFactory = LBFactorySingle::newFromConnection( $db );
|
2018-03-19 05:26:11 +00:00
|
|
|
// Ask for the connection so that LB sets internal state
|
2017-07-26 09:04:34 +00:00
|
|
|
// about this connection being the master connection
|
|
|
|
|
$lb = $lbFactory->getMainLB();
|
|
|
|
|
$conn = $lb->openConnection( $lb->getWriterIndex() );
|
|
|
|
|
$this->assertSame( $db, $conn, 'Same DB instance' );
|
|
|
|
|
|
2018-03-24 07:02:24 +00:00
|
|
|
$this->assertFalse( $lb->hasMasterChanges() );
|
|
|
|
|
$this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
|
2017-07-26 09:04:34 +00:00
|
|
|
$called = false;
|
2018-04-17 04:39:02 +00:00
|
|
|
$callback = function ( IDatabase $db ) use ( &$called ) {
|
2018-03-19 23:20:15 +00:00
|
|
|
$called = true;
|
|
|
|
|
};
|
|
|
|
|
$db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
|
2018-03-19 05:26:11 +00:00
|
|
|
$this->assertTrue( $called, 'Called when idle if DBO_TRX is set' );
|
2018-03-24 07:02:24 +00:00
|
|
|
$called = false;
|
|
|
|
|
$lbFactory->commitMasterChanges();
|
|
|
|
|
$this->assertFalse( $called );
|
2017-07-26 09:04:34 +00:00
|
|
|
|
2018-03-19 05:26:11 +00:00
|
|
|
$called = false;
|
2017-07-26 09:04:34 +00:00
|
|
|
$lbFactory->beginMasterChanges( __METHOD__ );
|
2018-03-19 05:26:11 +00:00
|
|
|
$db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
|
2017-07-26 09:04:34 +00:00
|
|
|
$this->assertFalse( $called, 'Not called when lb-transaction is active' );
|
|
|
|
|
$lbFactory->commitMasterChanges( __METHOD__ );
|
|
|
|
|
$this->assertTrue( $called, 'Called when lb-transaction is committed' );
|
2018-03-19 23:20:15 +00:00
|
|
|
|
|
|
|
|
$called = false;
|
|
|
|
|
$lbFactory->beginMasterChanges( __METHOD__ );
|
|
|
|
|
$db->onTransactionPreCommitOrIdle( $callback, __METHOD__ );
|
|
|
|
|
$this->assertFalse( $called, 'Not called when lb-transaction is active' );
|
|
|
|
|
|
|
|
|
|
$lbFactory->rollbackMasterChanges( __METHOD__ );
|
|
|
|
|
$this->assertFalse( $called, 'Not called when lb-transaction is rolled back' );
|
|
|
|
|
|
|
|
|
|
$lbFactory->commitMasterChanges( __METHOD__ );
|
|
|
|
|
$this->assertFalse( $called, 'Not called in next round commit' );
|
2017-07-26 09:04:34 +00:00
|
|
|
}
|
|
|
|
|
|
2017-07-20 20:17:11 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::onTransactionResolution
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
|
|
|
|
|
*/
|
2016-07-04 18:02:42 +00:00
|
|
|
public function testTransactionResolution() {
|
|
|
|
|
$db = $this->db;
|
|
|
|
|
|
|
|
|
|
$db->clearFlag( DBO_TRX );
|
|
|
|
|
$db->begin( __METHOD__ );
|
|
|
|
|
$called = false;
|
2018-04-17 04:39:02 +00:00
|
|
|
$db->onTransactionResolution( function ( $trigger, IDatabase $db ) use ( &$called ) {
|
2016-07-04 18:02:42 +00:00
|
|
|
$called = true;
|
|
|
|
|
$db->setFlag( DBO_TRX );
|
|
|
|
|
} );
|
|
|
|
|
$db->commit( __METHOD__ );
|
|
|
|
|
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
|
|
|
|
|
$this->assertTrue( $called, 'Callback reached' );
|
|
|
|
|
|
|
|
|
|
$db->clearFlag( DBO_TRX );
|
|
|
|
|
$db->begin( __METHOD__ );
|
|
|
|
|
$called = false;
|
2018-04-17 04:39:02 +00:00
|
|
|
$db->onTransactionResolution( function ( $trigger, IDatabase $db ) use ( &$called ) {
|
2016-07-04 18:02:42 +00:00
|
|
|
$called = true;
|
|
|
|
|
$db->setFlag( DBO_TRX );
|
|
|
|
|
} );
|
2018-03-28 20:01:32 +00:00
|
|
|
$db->rollback( __METHOD__ );
|
2016-07-04 18:02:42 +00:00
|
|
|
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
|
|
|
|
|
$this->assertTrue( $called, 'Callback reached' );
|
|
|
|
|
}
|
2016-08-17 21:05:41 +00:00
|
|
|
|
2016-08-26 07:19:34 +00:00
|
|
|
/**
|
2017-07-20 20:17:11 +00:00
|
|
|
* @covers Wikimedia\Rdbms\Database::setTransactionListener
|
2016-08-26 07:19:34 +00:00
|
|
|
*/
|
|
|
|
|
public function testTransactionListener() {
|
|
|
|
|
$db = $this->db;
|
|
|
|
|
|
2016-09-15 21:40:00 +00:00
|
|
|
$db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
|
2016-08-26 07:19:34 +00:00
|
|
|
$called = true;
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
$called = false;
|
|
|
|
|
$db->begin( __METHOD__ );
|
|
|
|
|
$db->commit( __METHOD__ );
|
|
|
|
|
$this->assertTrue( $called, 'Callback reached' );
|
|
|
|
|
|
|
|
|
|
$called = false;
|
|
|
|
|
$db->begin( __METHOD__ );
|
|
|
|
|
$db->commit( __METHOD__ );
|
|
|
|
|
$this->assertTrue( $called, 'Callback still reached' );
|
|
|
|
|
|
|
|
|
|
$called = false;
|
|
|
|
|
$db->begin( __METHOD__ );
|
|
|
|
|
$db->rollback( __METHOD__ );
|
|
|
|
|
$this->assertTrue( $called, 'Callback reached' );
|
|
|
|
|
|
|
|
|
|
$db->setTransactionListener( 'ping', null );
|
|
|
|
|
$called = false;
|
|
|
|
|
$db->begin( __METHOD__ );
|
|
|
|
|
$db->commit( __METHOD__ );
|
|
|
|
|
$this->assertFalse( $called, 'Callback not reached' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-07-20 20:17:11 +00:00
|
|
|
* Use this mock instead of DatabaseTestHelper for cases where
|
|
|
|
|
* DatabaseTestHelper is too inflexibile due to mocking too much
|
|
|
|
|
* or being too restrictive about fname matching (e.g. for tests
|
|
|
|
|
* that assert behaviour when the name is a mismatch, we need to
|
|
|
|
|
* catch the error here instead of there).
|
|
|
|
|
*
|
|
|
|
|
* @return Database
|
|
|
|
|
*/
|
|
|
|
|
private function getMockDB( $methods = [] ) {
|
|
|
|
|
static $abstractMethods = [
|
2018-01-28 14:10:39 +00:00
|
|
|
'fetchAffectedRowCount',
|
2017-07-20 20:17:11 +00:00
|
|
|
'closeConnection',
|
|
|
|
|
'dataSeek',
|
|
|
|
|
'doQuery',
|
|
|
|
|
'fetchObject', 'fetchRow',
|
|
|
|
|
'fieldInfo', 'fieldName',
|
|
|
|
|
'getSoftwareLink', 'getServerVersion',
|
|
|
|
|
'getType',
|
|
|
|
|
'indexInfo',
|
|
|
|
|
'insertId',
|
|
|
|
|
'lastError', 'lastErrno',
|
|
|
|
|
'numFields', 'numRows',
|
|
|
|
|
'open',
|
|
|
|
|
'strencode',
|
2018-06-06 21:38:47 +00:00
|
|
|
'tableExists'
|
2017-07-20 20:17:11 +00:00
|
|
|
];
|
|
|
|
|
$db = $this->getMockBuilder( Database::class )
|
|
|
|
|
->disableOriginalConstructor()
|
|
|
|
|
->setMethods( array_values( array_unique( array_merge(
|
|
|
|
|
$abstractMethods,
|
|
|
|
|
$methods
|
|
|
|
|
) ) ) )
|
|
|
|
|
->getMock();
|
|
|
|
|
$wdb = TestingAccessWrapper::newFromObject( $db );
|
|
|
|
|
$wdb->trxProfiler = new TransactionProfiler();
|
|
|
|
|
$wdb->connLogger = new \Psr\Log\NullLogger();
|
|
|
|
|
$wdb->queryLogger = new \Psr\Log\NullLogger();
|
2019-10-11 16:57:11 +00:00
|
|
|
$wdb->replLogger = new \Psr\Log\NullLogger();
|
2018-08-15 08:20:30 +00:00
|
|
|
$wdb->currentDomain = DatabaseDomain::newUnspecified();
|
2017-07-20 20:17:11 +00:00
|
|
|
return $db;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::flushSnapshot
|
2016-08-26 07:19:34 +00:00
|
|
|
*/
|
2016-09-08 11:28:52 +00:00
|
|
|
public function testFlushSnapshot() {
|
2017-07-20 20:17:11 +00:00
|
|
|
$db = $this->getMockDB( [ 'isOpen' ] );
|
|
|
|
|
$db->method( 'isOpen' )->willReturn( true );
|
2016-08-26 07:19:34 +00:00
|
|
|
|
2016-09-08 11:28:52 +00:00
|
|
|
$db->flushSnapshot( __METHOD__ ); // ok
|
|
|
|
|
$db->flushSnapshot( __METHOD__ ); // ok
|
2016-08-26 07:19:34 +00:00
|
|
|
|
|
|
|
|
$db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
|
|
|
|
|
$db->query( 'SELECT 1', __METHOD__ );
|
|
|
|
|
$this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
|
2016-09-08 11:28:52 +00:00
|
|
|
$db->flushSnapshot( __METHOD__ ); // ok
|
2016-08-26 07:19:34 +00:00
|
|
|
$db->restoreFlags( $db::RESTORE_PRIOR );
|
|
|
|
|
|
|
|
|
|
$this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-14 08:27:14 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::getScopedLockAndFlush
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::lock
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::unlock
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::lockIsFree
|
|
|
|
|
*/
|
2016-08-17 21:05:41 +00:00
|
|
|
public function testGetScopedLock() {
|
2018-08-14 23:44:41 +00:00
|
|
|
$db = $this->getMockDB( [ 'isOpen', 'getDBname' ] );
|
2017-07-20 20:17:11 +00:00
|
|
|
$db->method( 'isOpen' )->willReturn( true );
|
2018-08-14 23:44:41 +00:00
|
|
|
$db->method( 'getDBname' )->willReturn( 'unittest' );
|
2016-08-17 21:05:41 +00:00
|
|
|
|
2019-09-17 14:31:49 +00:00
|
|
|
$this->assertSame( 0, $db->trxLevel() );
|
2020-01-09 23:23:19 +00:00
|
|
|
$this->assertTrue( $db->lockIsFree( 'x', __METHOD__ ) );
|
|
|
|
|
$this->assertTrue( $db->lock( 'x', __METHOD__ ) );
|
2019-09-17 14:19:26 +00:00
|
|
|
$this->assertFalse( $db->lockIsFree( 'x', __METHOD__ ) );
|
2020-01-09 23:23:19 +00:00
|
|
|
$this->assertTrue( $db->unlock( 'x', __METHOD__ ) );
|
|
|
|
|
$this->assertTrue( $db->lockIsFree( 'x', __METHOD__ ) );
|
2019-09-17 14:31:49 +00:00
|
|
|
$this->assertSame( 0, $db->trxLevel() );
|
2018-02-14 08:27:14 +00:00
|
|
|
|
|
|
|
|
$db->setFlag( DBO_TRX );
|
2020-01-09 23:23:19 +00:00
|
|
|
$this->assertTrue( $db->lockIsFree( 'x', __METHOD__ ) );
|
|
|
|
|
$this->assertTrue( $db->lock( 'x', __METHOD__ ) );
|
2019-09-17 14:19:26 +00:00
|
|
|
$this->assertFalse( $db->lockIsFree( 'x', __METHOD__ ) );
|
2020-01-09 23:23:19 +00:00
|
|
|
$this->assertTrue( $db->unlock( 'x', __METHOD__ ) );
|
|
|
|
|
$this->assertTrue( $db->lockIsFree( 'x', __METHOD__ ) );
|
2018-02-14 08:27:14 +00:00
|
|
|
$db->clearFlag( DBO_TRX );
|
|
|
|
|
|
2018-03-28 20:01:32 +00:00
|
|
|
// Pending writes with DBO_TRX
|
2019-09-17 14:31:49 +00:00
|
|
|
$this->assertSame( 0, $db->trxLevel() );
|
2018-03-28 20:01:32 +00:00
|
|
|
$this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
|
2016-08-17 21:05:41 +00:00
|
|
|
$db->setFlag( DBO_TRX );
|
2018-03-28 20:01:32 +00:00
|
|
|
$db->query( "DELETE FROM test WHERE t = 1" ); // trigger DBO_TRX transaction before lock
|
2016-08-17 21:05:41 +00:00
|
|
|
try {
|
2018-03-28 20:01:32 +00:00
|
|
|
$lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
|
|
|
|
|
$this->fail( "Exception not reached" );
|
|
|
|
|
} catch ( DBUnexpectedError $e ) {
|
2020-05-30 10:36:42 +00:00
|
|
|
$this->assertSame( 1, $db->trxLevel(), "Transaction not committed." );
|
2018-03-28 20:01:32 +00:00
|
|
|
$this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ), 'Lock not acquired' );
|
2016-08-17 21:05:41 +00:00
|
|
|
}
|
|
|
|
|
$db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
|
2018-03-28 20:01:32 +00:00
|
|
|
// Pending writes without DBO_TRX
|
|
|
|
|
$db->clearFlag( DBO_TRX );
|
2019-09-17 14:31:49 +00:00
|
|
|
$this->assertSame( 0, $db->trxLevel() );
|
2018-03-28 20:01:32 +00:00
|
|
|
$this->assertTrue( $db->lockIsFree( 'meow2', __METHOD__ ) );
|
|
|
|
|
$db->begin( __METHOD__ );
|
|
|
|
|
$db->query( "DELETE FROM test WHERE t = 1" ); // trigger DBO_TRX transaction before lock
|
2016-08-17 21:05:41 +00:00
|
|
|
try {
|
2018-03-28 20:01:32 +00:00
|
|
|
$lock = $db->getScopedLockAndFlush( 'meow2', __METHOD__, 1 );
|
|
|
|
|
$this->fail( "Exception not reached" );
|
|
|
|
|
} catch ( DBUnexpectedError $e ) {
|
2020-05-30 10:36:42 +00:00
|
|
|
$this->assertSame( 1, $db->trxLevel(), "Transaction not committed." );
|
2018-03-28 20:01:32 +00:00
|
|
|
$this->assertTrue( $db->lockIsFree( 'meow2', __METHOD__ ), 'Lock not acquired' );
|
2016-08-17 21:05:41 +00:00
|
|
|
}
|
2018-03-28 20:01:32 +00:00
|
|
|
$db->rollback( __METHOD__ );
|
|
|
|
|
// No pending writes, with DBO_TRX
|
|
|
|
|
$db->setFlag( DBO_TRX );
|
2019-09-17 14:31:49 +00:00
|
|
|
$this->assertSame( 0, $db->trxLevel() );
|
2018-03-28 20:01:32 +00:00
|
|
|
$this->assertTrue( $db->lockIsFree( 'wuff', __METHOD__ ) );
|
|
|
|
|
$db->query( "SELECT 1", __METHOD__ );
|
2020-05-30 10:36:42 +00:00
|
|
|
$this->assertSame( 1, $db->trxLevel() );
|
2018-03-28 20:01:32 +00:00
|
|
|
$lock = $db->getScopedLockAndFlush( 'wuff', __METHOD__, 1 );
|
2019-09-17 14:31:49 +00:00
|
|
|
$this->assertSame( 0, $db->trxLevel() );
|
2018-03-28 20:01:32 +00:00
|
|
|
$this->assertFalse( $db->lockIsFree( 'wuff', __METHOD__ ), 'Lock already acquired' );
|
2016-08-17 21:05:41 +00:00
|
|
|
$db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
|
2018-03-28 20:01:32 +00:00
|
|
|
// No pending writes, without DBO_TRX
|
|
|
|
|
$db->clearFlag( DBO_TRX );
|
2019-09-17 14:31:49 +00:00
|
|
|
$this->assertSame( 0, $db->trxLevel() );
|
2018-03-28 20:01:32 +00:00
|
|
|
$this->assertTrue( $db->lockIsFree( 'wuff2', __METHOD__ ) );
|
2016-08-17 21:05:41 +00:00
|
|
|
$db->begin( __METHOD__ );
|
2018-03-28 20:01:32 +00:00
|
|
|
try {
|
|
|
|
|
$lock = $db->getScopedLockAndFlush( 'wuff2', __METHOD__, 1 );
|
|
|
|
|
$this->fail( "Exception not reached" );
|
|
|
|
|
} catch ( DBUnexpectedError $e ) {
|
2020-05-30 10:36:42 +00:00
|
|
|
$this->assertSame( 1, $db->trxLevel(), "Transaction not committed." );
|
2018-03-28 20:01:32 +00:00
|
|
|
$this->assertFalse( $db->lockIsFree( 'wuff2', __METHOD__ ), 'Lock not acquired' );
|
|
|
|
|
}
|
|
|
|
|
$db->rollback( __METHOD__ );
|
2016-08-17 21:05:41 +00:00
|
|
|
}
|
2016-08-22 05:35:12 +00:00
|
|
|
|
|
|
|
|
/**
|
2017-07-20 20:17:11 +00:00
|
|
|
* @covers Wikimedia\Rdbms\Database::getFlag
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::setFlag
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::restoreFlags
|
2016-08-22 05:35:12 +00:00
|
|
|
*/
|
|
|
|
|
public function testFlagSetting() {
|
|
|
|
|
$db = $this->db;
|
|
|
|
|
$origTrx = $db->getFlag( DBO_TRX );
|
2019-07-11 02:35:46 +00:00
|
|
|
$origNoBuffer = $db->getFlag( DBO_NOBUFFER );
|
2016-08-22 05:35:12 +00:00
|
|
|
|
2016-09-20 23:24:16 +00:00
|
|
|
$origTrx
|
|
|
|
|
? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
|
|
|
|
|
: $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
|
2016-08-22 05:35:12 +00:00
|
|
|
$this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
|
|
|
|
|
|
2019-07-11 02:35:46 +00:00
|
|
|
$origNoBuffer
|
|
|
|
|
? $db->clearFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR )
|
|
|
|
|
: $db->setFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR );
|
|
|
|
|
$this->assertEquals( !$origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
|
2016-08-22 05:35:12 +00:00
|
|
|
|
2016-09-20 23:24:16 +00:00
|
|
|
$db->restoreFlags( $db::RESTORE_INITIAL );
|
|
|
|
|
$this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
|
2019-07-11 02:35:46 +00:00
|
|
|
$this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
|
2016-09-20 23:24:16 +00:00
|
|
|
|
|
|
|
|
$origTrx
|
|
|
|
|
? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
|
|
|
|
|
: $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
|
2019-07-11 02:35:46 +00:00
|
|
|
$origNoBuffer
|
|
|
|
|
? $db->clearFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR )
|
|
|
|
|
: $db->setFlag( DBO_NOBUFFER, $db::REMEMBER_PRIOR );
|
2016-08-22 05:35:12 +00:00
|
|
|
|
|
|
|
|
$db->restoreFlags();
|
2019-07-11 02:35:46 +00:00
|
|
|
$this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
|
2016-08-22 05:35:12 +00:00
|
|
|
$this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
|
|
|
|
|
|
|
|
|
|
$db->restoreFlags();
|
2019-07-11 02:35:46 +00:00
|
|
|
$this->assertEquals( $origNoBuffer, $db->getFlag( DBO_NOBUFFER ) );
|
2016-08-22 05:35:12 +00:00
|
|
|
$this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
|
|
|
|
|
}
|
2016-09-16 18:32:23 +00:00
|
|
|
|
2019-07-11 02:35:46 +00:00
|
|
|
public function provideImmutableDBOFlags() {
|
|
|
|
|
return [
|
|
|
|
|
[ Database::DBO_IGNORE ],
|
|
|
|
|
[ Database::DBO_DEFAULT ],
|
|
|
|
|
[ Database::DBO_PERSISTENT ]
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-08 04:10:13 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::setFlag
|
2019-07-11 02:35:46 +00:00
|
|
|
* @dataProvider provideImmutableDBOFlags
|
|
|
|
|
* @param int $flag
|
2018-03-08 04:10:13 +00:00
|
|
|
*/
|
2019-07-11 02:35:46 +00:00
|
|
|
public function testDBOCannotSet( $flag ) {
|
2018-03-08 04:10:13 +00:00
|
|
|
$db = $this->getMockBuilder( DatabaseMysqli::class )
|
|
|
|
|
->disableOriginalConstructor()
|
|
|
|
|
->setMethods( null )
|
|
|
|
|
->getMock();
|
|
|
|
|
|
2019-10-11 22:22:26 +00:00
|
|
|
$this->expectException( DBUnexpectedError::class );
|
2019-07-11 02:35:46 +00:00
|
|
|
$db->setFlag( $flag );
|
2018-03-08 04:10:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::clearFlag
|
2019-07-11 02:35:46 +00:00
|
|
|
* @dataProvider provideImmutableDBOFlags
|
|
|
|
|
* @param int $flag
|
2018-03-08 04:10:13 +00:00
|
|
|
*/
|
2019-07-11 02:35:46 +00:00
|
|
|
public function testDBOCannotClear( $flag ) {
|
2018-03-08 04:10:13 +00:00
|
|
|
$db = $this->getMockBuilder( DatabaseMysqli::class )
|
|
|
|
|
->disableOriginalConstructor()
|
|
|
|
|
->setMethods( null )
|
|
|
|
|
->getMock();
|
|
|
|
|
|
2019-10-11 22:22:26 +00:00
|
|
|
$this->expectException( DBUnexpectedError::class );
|
2019-07-11 02:35:46 +00:00
|
|
|
$db->clearFlag( $flag );
|
2018-03-08 04:10:13 +00:00
|
|
|
}
|
|
|
|
|
|
2016-09-16 18:32:23 +00:00
|
|
|
/**
|
2017-07-20 20:17:11 +00:00
|
|
|
* @covers Wikimedia\Rdbms\Database::tablePrefix
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::dbSchema
|
2016-09-16 18:32:23 +00:00
|
|
|
*/
|
2018-08-14 23:44:41 +00:00
|
|
|
public function testSchemaAndPrefixMutators() {
|
2019-03-28 23:04:59 +00:00
|
|
|
$ud = DatabaseDomain::newUnspecified();
|
|
|
|
|
|
|
|
|
|
$this->assertEquals( $ud->getId(), $this->db->getDomainID() );
|
|
|
|
|
|
2016-09-16 18:32:23 +00:00
|
|
|
$old = $this->db->tablePrefix();
|
2020-05-26 13:14:46 +00:00
|
|
|
$oldDomain = $this->db->getDomainID();
|
2019-10-06 14:12:39 +00:00
|
|
|
$this->assertIsString( $old, 'Prefix is string' );
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->assertSame( $old, $this->db->tablePrefix(), "Prefix unchanged" );
|
2019-03-25 14:51:45 +00:00
|
|
|
$this->assertSame( $old, $this->db->tablePrefix( 'xxx_' ) );
|
|
|
|
|
$this->assertSame( 'xxx_', $this->db->tablePrefix(), "Prefix set" );
|
2016-09-16 18:32:23 +00:00
|
|
|
$this->db->tablePrefix( $old );
|
2019-03-25 14:51:45 +00:00
|
|
|
$this->assertNotEquals( 'xxx_', $this->db->tablePrefix() );
|
2020-05-26 13:14:46 +00:00
|
|
|
$this->assertSame( $oldDomain, $this->db->getDomainID() );
|
2016-09-16 18:32:23 +00:00
|
|
|
|
|
|
|
|
$old = $this->db->dbSchema();
|
2020-05-26 13:14:46 +00:00
|
|
|
$oldDomain = $this->db->getDomainID();
|
2019-10-06 14:12:39 +00:00
|
|
|
$this->assertIsString( $old, 'Schema is string' );
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->assertSame( $old, $this->db->dbSchema(), "Schema unchanged" );
|
2019-03-28 23:04:59 +00:00
|
|
|
|
|
|
|
|
$this->db->selectDB( 'y' );
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->assertSame( $old, $this->db->dbSchema( 'xxx' ) );
|
|
|
|
|
$this->assertSame( 'xxx', $this->db->dbSchema(), "Schema set" );
|
2016-09-16 18:32:23 +00:00
|
|
|
$this->db->dbSchema( $old );
|
|
|
|
|
$this->assertNotEquals( 'xxx', $this->db->dbSchema() );
|
2020-05-26 13:14:46 +00:00
|
|
|
$this->assertSame( "y", $this->db->getDomainID() );
|
2019-03-28 23:04:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::tablePrefix
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::dbSchema
|
|
|
|
|
*/
|
|
|
|
|
public function testSchemaWithNoDB() {
|
|
|
|
|
$ud = DatabaseDomain::newUnspecified();
|
|
|
|
|
|
|
|
|
|
$this->assertEquals( $ud->getId(), $this->db->getDomainID() );
|
|
|
|
|
$this->assertSame( '', $this->db->dbSchema() );
|
|
|
|
|
|
2019-10-11 22:22:26 +00:00
|
|
|
$this->expectException( DBUnexpectedError::class );
|
2019-03-28 23:04:59 +00:00
|
|
|
$this->db->dbSchema( 'xxx' );
|
2018-08-14 23:44:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::selectDomain
|
|
|
|
|
*/
|
|
|
|
|
public function testSelectDomain() {
|
2020-05-26 13:14:46 +00:00
|
|
|
$oldDomain = $this->db->getDomainID();
|
2018-08-14 23:44:41 +00:00
|
|
|
$oldDatabase = $this->db->getDBname();
|
|
|
|
|
$oldSchema = $this->db->dbSchema();
|
|
|
|
|
$oldPrefix = $this->db->tablePrefix();
|
|
|
|
|
|
2019-03-25 14:51:45 +00:00
|
|
|
$this->db->selectDomain( 'testselectdb-xxx_' );
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->assertSame( 'testselectdb', $this->db->getDBname() );
|
|
|
|
|
$this->assertSame( '', $this->db->dbSchema() );
|
2019-03-25 14:51:45 +00:00
|
|
|
$this->assertSame( 'xxx_', $this->db->tablePrefix() );
|
2018-08-14 23:44:41 +00:00
|
|
|
|
|
|
|
|
$this->db->selectDomain( $oldDomain );
|
|
|
|
|
$this->assertSame( $oldDatabase, $this->db->getDBname() );
|
|
|
|
|
$this->assertSame( $oldSchema, $this->db->dbSchema() );
|
|
|
|
|
$this->assertSame( $oldPrefix, $this->db->tablePrefix() );
|
2020-05-26 13:14:46 +00:00
|
|
|
$this->assertSame( $oldDomain, $this->db->getDomainID() );
|
2018-08-14 23:44:41 +00:00
|
|
|
|
2019-03-25 14:51:45 +00:00
|
|
|
$this->db->selectDomain( 'testselectdb-schema-xxx_' );
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->assertSame( 'testselectdb', $this->db->getDBname() );
|
|
|
|
|
$this->assertSame( 'schema', $this->db->dbSchema() );
|
2019-03-25 14:51:45 +00:00
|
|
|
$this->assertSame( 'xxx_', $this->db->tablePrefix() );
|
2018-08-14 23:44:41 +00:00
|
|
|
|
|
|
|
|
$this->db->selectDomain( $oldDomain );
|
|
|
|
|
$this->assertSame( $oldDatabase, $this->db->getDBname() );
|
|
|
|
|
$this->assertSame( $oldSchema, $this->db->dbSchema() );
|
|
|
|
|
$this->assertSame( $oldPrefix, $this->db->tablePrefix() );
|
2020-05-26 13:14:46 +00:00
|
|
|
$this->assertSame( $oldDomain, $this->db->getDomainID() );
|
2016-09-16 18:32:23 +00:00
|
|
|
}
|
2018-10-17 15:26:51 +00:00
|
|
|
|
2019-07-15 02:54:47 +00:00
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::getLBInfo
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::setLBInfo
|
|
|
|
|
*/
|
|
|
|
|
public function testGetSetLBInfo() {
|
|
|
|
|
$db = $this->getMockDB();
|
|
|
|
|
|
|
|
|
|
$this->assertEquals( [], $db->getLBInfo() );
|
|
|
|
|
$this->assertNull( $db->getLBInfo( 'pringles' ) );
|
|
|
|
|
|
|
|
|
|
$db->setLBInfo( 'soda', 'water' );
|
|
|
|
|
$this->assertEquals( [ 'soda' => 'water' ], $db->getLBInfo() );
|
|
|
|
|
$this->assertNull( $db->getLBInfo( 'pringles' ) );
|
|
|
|
|
$this->assertEquals( 'water', $db->getLBInfo( 'soda' ) );
|
|
|
|
|
|
|
|
|
|
$db->setLBInfo( 'basketball', 'Lebron' );
|
|
|
|
|
$this->assertEquals( [ 'soda' => 'water', 'basketball' => 'Lebron' ], $db->getLBInfo() );
|
|
|
|
|
$this->assertEquals( 'water', $db->getLBInfo( 'soda' ) );
|
|
|
|
|
$this->assertEquals( 'Lebron', $db->getLBInfo( 'basketball' ) );
|
|
|
|
|
|
|
|
|
|
$db->setLBInfo( 'soda', null );
|
|
|
|
|
$this->assertEquals( [ 'basketball' => 'Lebron' ], $db->getLBInfo() );
|
|
|
|
|
|
|
|
|
|
$db->setLBInfo( [ 'King' => 'James' ] );
|
|
|
|
|
$this->assertNull( $db->getLBInfo( 'basketball' ) );
|
|
|
|
|
$this->assertEquals( [ 'King' => 'James' ], $db->getLBInfo() );
|
|
|
|
|
}
|
2019-12-19 13:36:17 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Wikimedia\Rdbms\Database::isWriteQuery
|
|
|
|
|
* @param string $query
|
|
|
|
|
* @param bool $res
|
|
|
|
|
* @dataProvider provideIsWriteQuery
|
|
|
|
|
*/
|
|
|
|
|
public function testIsWriteQuery( string $query, bool $res ) {
|
2020-01-07 20:39:39 +00:00
|
|
|
$db = TestingAccessWrapper::newFromObject( $this->db );
|
2020-03-20 13:16:46 +00:00
|
|
|
$this->assertSame( $res, $db->isWriteQuery( $query, 0 ) );
|
2019-12-19 13:36:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Provider for testIsWriteQuery
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
2020-01-07 20:39:39 +00:00
|
|
|
public function provideIsWriteQuery() : array {
|
2019-12-19 13:36:17 +00:00
|
|
|
return [
|
|
|
|
|
[ 'SELECT foo', false ],
|
|
|
|
|
[ ' SELECT foo FROM bar', false ],
|
|
|
|
|
[ 'BEGIN', false ],
|
|
|
|
|
[ 'SHOW EXPLAIN FOR 12;', false ],
|
|
|
|
|
[ 'USE foobar', false ],
|
|
|
|
|
[ '(SELECT 1)', false ],
|
|
|
|
|
[ 'INSERT INTO foo', true ],
|
|
|
|
|
[ 'TRUNCATE bar', true ],
|
|
|
|
|
[ 'DELETE FROM baz', true ],
|
|
|
|
|
[ 'CREATE TABLE foobar', true ]
|
|
|
|
|
];
|
|
|
|
|
}
|
Permit temporary table writes on replica DB connections
In I8e17644d1b447416adee18e42cf0122b52a80b22, MediaWiki's DBAL was adjusted to
reject any write query on read-only DB replica connections. This poses a problem
for extensions that use temporary tables in their queries, as such queries now
have to be executed on the source DB rather than a replica to work around this
fact. An example of such an extension is Semantic MediaWiki, whose QueryEngine
uses temporary tables extensively in serving reads. The current situation, where
all writes, including non-persistent ones, must be executed on a source DB
connection, causes scalability issues since it's no longer possible to
distribute these queries between multiple replicas.
An old code comment in the DBAL cited MySQL bug 33669 as a potential blocker to
permitting temporary table operations on read-only connections. However, that
bug was closed a decade ago, and Fandom's Semantic MediaWiki cluster has been
permitting such operations on its MySQL 5.7 replica nodes (running with
--read-only) for several years now, without observing any adverse side-effect.
This patch accordingly relaxes the restrictions placed by the MediaWiki DBAL on
temporary table operations to enable executing them even on read-only replica DB
connections. Several unit tests were added to verify the conditions under which
a given write query may be allowed to execute on a connection.
Bug: T259362
Change-Id: I90a1427a15d0aee07e7b24ba4248b7ef4475c227
2020-07-31 15:37:26 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Database::executeQuery()
|
|
|
|
|
* @covers Database::assertIsWritableMaster()
|
|
|
|
|
*/
|
|
|
|
|
public function testShouldRejectPersistentWriteQueryOnReplicaDatabaseConnection(): void {
|
|
|
|
|
$this->expectException( DBReadOnlyRoleError::class );
|
|
|
|
|
$this->expectDeprecationMessage( 'Server is configured as a read-only replica database.' );
|
|
|
|
|
|
|
|
|
|
$dbr = new DatabaseTestHelper(
|
|
|
|
|
__CLASS__ . '::' . $this->getName(),
|
|
|
|
|
[ 'topologyRole' => Database::ROLE_STREAMING_REPLICA ]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$dbr->query( "INSERT INTO test_table (a_column) VALUES ('foo');", __METHOD__ );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Database::executeQuery()
|
|
|
|
|
* @covers Database::assertIsWritableMaster()
|
|
|
|
|
*/
|
|
|
|
|
public function testShouldAcceptTemporaryTableOperationsOnReplicaDatabaseConnection(): void {
|
|
|
|
|
$dbr = new DatabaseTestHelper(
|
|
|
|
|
__CLASS__ . '::' . $this->getName(),
|
|
|
|
|
[ 'topologyRole' => Database::ROLE_STREAMING_REPLICA ]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$resCreate = $dbr->query(
|
|
|
|
|
"CREATE TEMPORARY TABLE temp_test_table (temp_column int);",
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$resModify = $dbr->query(
|
|
|
|
|
"INSERT INTO temp_test_table (temp_column) VALUES (42);",
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertInstanceOf( IResultWrapper::class, $resCreate );
|
|
|
|
|
$this->assertInstanceOf( IResultWrapper::class, $resModify );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Database::executeQuery()
|
|
|
|
|
* @covers Database::assertIsWritableMaster()
|
|
|
|
|
*/
|
|
|
|
|
public function testShouldRejectPseudoPermanentTemporaryTableOperationsOnReplicaDatabaseConnection(): void {
|
|
|
|
|
$this->expectException( DBReadOnlyRoleError::class );
|
|
|
|
|
$this->expectDeprecationMessage( 'Server is configured as a read-only replica database.' );
|
|
|
|
|
|
|
|
|
|
$dbr = new DatabaseTestHelper(
|
|
|
|
|
__CLASS__ . '::' . $this->getName(),
|
|
|
|
|
[ 'topologyRole' => Database::ROLE_STREAMING_REPLICA ]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$dbr->query(
|
|
|
|
|
"CREATE TEMPORARY TABLE temp_test_table (temp_column int);",
|
|
|
|
|
__METHOD__,
|
|
|
|
|
Database::QUERY_PSEUDO_PERMANENT
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Database::executeQuery()
|
|
|
|
|
* @covers Database::assertIsWritableMaster()
|
|
|
|
|
*/
|
|
|
|
|
public function testShouldAcceptWriteQueryOnPrimaryDatabaseConnection(): void {
|
|
|
|
|
$dbr = new DatabaseTestHelper(
|
|
|
|
|
__CLASS__ . '::' . $this->getName(),
|
|
|
|
|
[ 'topologyRole' => Database::ROLE_STREAMING_MASTER ]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$res = $dbr->query( "INSERT INTO test_table (a_column) VALUES ('foo');", __METHOD__ );
|
|
|
|
|
|
|
|
|
|
$this->assertInstanceOf( IResultWrapper::class, $res );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers Database::executeQuery()
|
|
|
|
|
* @covers Database::assertIsWritableMaster()
|
|
|
|
|
*/
|
|
|
|
|
public function testShouldRejectWriteQueryOnPrimaryDatabaseConnectionWhenReplicaQueryRoleFlagIsSet(): void {
|
|
|
|
|
$this->expectException( DBReadOnlyRoleError::class );
|
|
|
|
|
$this->expectDeprecationMessage( 'Cannot write; target role is DB_REPLICA' );
|
|
|
|
|
|
|
|
|
|
$dbr = new DatabaseTestHelper(
|
|
|
|
|
__CLASS__ . '::' . $this->getName(),
|
|
|
|
|
[ 'topologyRole' => Database::ROLE_STREAMING_MASTER ]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$dbr->query(
|
|
|
|
|
"INSERT INTO test_table (a_column) VALUES ('foo');",
|
|
|
|
|
__METHOD__,
|
|
|
|
|
Database::QUERY_REPLICA_ROLE
|
|
|
|
|
);
|
|
|
|
|
}
|
2010-12-14 16:26:35 +00:00
|
|
|
}
|