gplx/mediawiki/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseSQLTest.php

1385 lines
49 KiB
PHP

<?php
namespace Wikimedia\Tests\Rdbms;
use DatabaseTestHelper;
use MediaWikiCoversValidator;
use MediaWikiTestCaseTrait;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\DBError;
use Wikimedia\Rdbms\DBTransactionError;
use Wikimedia\Rdbms\DBTransactionStateError;
use Wikimedia\Rdbms\DBUnexpectedError;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\Platform\SQLPlatform;
use Wikimedia\Rdbms\TransactionManager;
use Wikimedia\TestingAccessWrapper;
/**
* Test the parts of the Database abstract class that deal
* with creating SQL text.
*
* @covers \Wikimedia\Rdbms\Database
* @covers \Wikimedia\Rdbms\Subquery
* @covers \Wikimedia\Rdbms\Platform\SQLPlatform
*/
class DatabaseSQLTest extends TestCase {
use MediaWikiCoversValidator;
use MediaWikiTestCaseTrait;
/** @var DatabaseTestHelper|Database */
private $database;
/** @var SQLPlatform */
private $platform;
protected function setUp(): void {
parent::setUp();
$this->database = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => true ] );
}
protected function assertLastSql( $sqlText ) {
$this->assertEquals(
$sqlText,
$this->database->getLastSqls()
);
}
protected function assertLastSqlDb( $sqlText, DatabaseTestHelper $db ) {
$this->assertEquals( $sqlText, $db->getLastSqls() );
}
/**
* @dataProvider provideLockForUpdate
*/
public function testLockForUpdate( $sql, $sqlText ) {
$this->database->startAtomic( __METHOD__ );
$this->database->lockForUpdate(
$sql['tables'],
$sql['conds'] ?? [],
__METHOD__,
$sql['options'] ?? [],
$sql['join_conds'] ?? []
);
$this->database->endAtomic( __METHOD__ );
$this->assertLastSql( "BEGIN; $sqlText; COMMIT" );
}
public static function provideLockForUpdate() {
return [
[
[
'tables' => [ 'table' ],
'conds' => [ 'field' => [ 1, 2, 3, 4 ] ],
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table WHERE field IN (1,2,3,4) " .
"FOR UPDATE) tmp_count"
],
[
[
'tables' => [ 'table', 't2' => 'table2' ],
'conds' => [ 'field' => 'text' ],
'options' => [ 'LIMIT' => 1, 'ORDER BY' => 'field' ],
'join_conds' => [ 't2' => [
'LEFT JOIN', 'tid = t2.id'
] ],
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table LEFT JOIN table2 t2 ON ((tid = t2.id)) " .
"WHERE field = 'text' ORDER BY field LIMIT 1 FOR UPDATE) tmp_count"
],
[
[
'tables' => 'table',
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table FOR UPDATE) tmp_count"
],
];
}
/**
* @dataProvider provideSelectRowCount
* @param array $sql
* @param string $sqlText
*/
public function testSelectRowCount( $sql, $sqlText ) {
$this->database->selectRowCount(
$sql['tables'],
$sql['field'],
$sql['conds'] ?? [],
__METHOD__,
$sql['options'] ?? [],
$sql['join_conds'] ?? []
);
$this->assertLastSql( $sqlText );
}
public static function provideSelectRowCount() {
return [
[
[
'tables' => 'table',
'field' => [ '*' ],
'conds' => [ 'field' => 'text' ],
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table WHERE field = 'text' ) tmp_count"
],
[
[
'tables' => 'table',
'field' => [ 'column' ],
'conds' => [ 'field' => 'text' ],
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count"
],
[
[
'tables' => 'table',
'field' => [ 'alias' => 'column' ],
'conds' => [ 'field' => 'text' ],
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table WHERE field = 'text' AND (column IS NOT NULL) ) tmp_count"
],
[
[
'tables' => 'table',
'field' => [ 'alias' => 'column' ],
'conds' => '',
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count"
],
[
[
'tables' => 'table',
'field' => [ 'alias' => 'column' ],
'conds' => false,
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count"
],
[
[
'tables' => 'table',
'field' => [ 'alias' => 'column' ],
'conds' => null,
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table WHERE (column IS NOT NULL) ) tmp_count"
],
[
[
'tables' => 'table',
'field' => [ 'alias' => 'column' ],
'conds' => 1,
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table WHERE (1) AND (column IS NOT NULL) ) tmp_count"
],
[
[
'tables' => 'table',
'field' => [ 'alias' => 'column' ],
'conds' => 0,
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT 1 FROM table WHERE (0) AND (column IS NOT NULL) ) tmp_count"
],
[
[
'tables' => 'table',
'field' => [ 'column' ],
'conds' => 1,
'options' => 'DISTINCT',
],
"SELECT COUNT(*) AS rowcount FROM " .
"(SELECT DISTINCT column FROM table WHERE (1) AND (column IS NOT NULL) ) tmp_count"
],
];
}
/**
* @dataProvider provideUpsert
*/
public function testUpsert( $sql, $sqlText ) {
$this->database->setNextQueryAffectedRowCounts( [ 0 ] );
foreach ( $sql['set'] as $k => $v ) {
$sql['set'][$k] = preg_replace_callback(
'/\$EXCLUDED\((\w+)\)/',
function ( $m ) {
return $this->database->buildExcludedValue( $m[1] );
},
$v
);
}
$this->database->upsert(
$sql['table'],
$sql['rows'],
$sql['uniqueIndexes'],
$sql['set'],
__METHOD__
);
$this->assertLastSql( $sqlText );
}
public static function provideUpsert() {
return [
[
[
'table' => 'upsert_table',
'rows' => [ 'id' => 'text', 'field1' => 'text2' ],
'uniqueIndexes' => 'id',
'set' => [ 'field2' => 'set' ],
],
"BEGIN; " .
"UPDATE upsert_table SET field2 = 'set' " .
"WHERE (id = 'text'); " .
"INSERT INTO upsert_table (id,field1) VALUES ('text','text2'); " .
"COMMIT"
],
[
[
'table' => 'upsert_table',
'rows' => [ 'id' => 'text', 'field1' => 'text2' ],
'uniqueIndexes' => [ [ 'id' ] ],
'set' => [ 'field2' => 'set' ],
],
"BEGIN; " .
"UPDATE upsert_table SET field2 = 'set' " .
"WHERE (id = 'text'); " .
"INSERT INTO upsert_table (id,field1) VALUES ('text','text2'); " .
"COMMIT"
],
[
[
'table' => 'upsert_table2',
'rows' => [ 'code' => 'text', 'name' => 'more', 'field3' => 'text2' ],
'uniqueIndexes' => [ [ 'code', 'name' ] ],
'set' => [ 'field2' => 'set' ],
],
"BEGIN; " .
"UPDATE upsert_table2 SET field2 = 'set' " .
"WHERE (code = 'text' AND name = 'more'); " .
"INSERT INTO upsert_table2 (code,name,field3) VALUES ('text','more','text2'); " .
"COMMIT"
],
[
[
'table' => 'upsert_table',
'rows' => [
[ 'id' => 1, 'field1' => 10, 'field2' => 1583464659, 'field3' => 1 ],
[ 'id' => 6, 'field1' => 60, 'field2' => 1583464659, 'field3' => 1 ]
],
'uniqueIndexes' => 'id',
'set' => [
'field1 = field1 + $EXCLUDED(field1)',
'field2 = COALESCE(field2,$EXCLUDED(field2))'
],
],
"BEGIN; " .
"WITH __VALS (__id,__field1,__field2,__field3) " .
"AS (VALUES (1,10,1583464659,1)) " .
"UPDATE upsert_table SET " .
"field1 = field1 + (SELECT __field1 FROM __VALS)," .
"field2 = COALESCE(field2,(SELECT __field2 FROM __VALS)) " .
"WHERE (id = 1); " .
"INSERT INTO upsert_table (id,field1,field2,field3) " .
"VALUES (1,10,1583464659,1); " .
"WITH __VALS (__id,__field1,__field2,__field3) " .
"AS (VALUES (6,60,1583464659,1)) " .
"UPDATE upsert_table SET " .
"field1 = field1 + (SELECT __field1 FROM __VALS)," .
"field2 = COALESCE(field2,(SELECT __field2 FROM __VALS)) " .
"WHERE (id = 6); " .
"INSERT INTO upsert_table (id,field1,field2,field3) " .
"VALUES (6,60,1583464659,1); " .
"COMMIT"
],
];
}
/**
* @dataProvider provideInsert
*/
public function testInsert( $sql, $sqlText ) {
$this->database->insert(
$sql['table'],
$sql['rows'],
__METHOD__,
$sql['options'] ?? []
);
$this->assertLastSql( $sqlText );
}
public static function provideInsert() {
return [
[
[
'table' => 'table',
'rows' => [ 'field' => 'text', 'field2' => 2 ],
],
"INSERT INTO table " .
"(field,field2) " .
"VALUES ('text',2)"
],
[
[
'table' => 'table',
'rows' => [ 'field' => 'text', 'field2' => 2 ],
'options' => 'IGNORE',
],
"INSERT IGNORE INTO table " .
"(field,field2) " .
"VALUES ('text',2)"
],
[
[
'table' => 'table',
'rows' => [
[ 'field' => 'text', 'field2' => 2 ],
[ 'field' => 'multi', 'field2' => 3 ],
],
'options' => 'IGNORE',
],
"INSERT IGNORE INTO table " .
"(field,field2) " .
"VALUES " .
"('text',2)," .
"('multi',3)"
],
];
}
/**
* @dataProvider provideInsertSelect
*/
public function testInsertSelect( $sql, $sqlTextNative, $sqlSelect, $sqlInsert ) {
$this->database->insertSelect(
$sql['destTable'],
$sql['srcTable'],
$sql['varMap'],
$sql['conds'],
__METHOD__,
$sql['insertOptions'] ?? [],
$sql['selectOptions'] ?? [],
$sql['selectJoinConds'] ?? []
);
$this->assertLastSql( $sqlTextNative );
$dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
$dbWeb->forceNextResult( [
array_flip( array_keys( $sql['varMap'] ) )
] );
$dbWeb->insertSelect(
$sql['destTable'],
$sql['srcTable'],
$sql['varMap'],
$sql['conds'],
__METHOD__,
$sql['insertOptions'] ?? [],
$sql['selectOptions'] ?? [],
$sql['selectJoinConds'] ?? []
);
$this->assertLastSqlDb( implode( '; ', [ $sqlSelect, 'BEGIN', $sqlInsert, 'COMMIT' ] ), $dbWeb );
}
public static function provideInsertSelect() {
return [
[
[
'destTable' => 'insert_table',
'srcTable' => 'select_table',
'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
'conds' => '*',
],
"INSERT INTO insert_table " .
"(field_insert,field) " .
"SELECT field_select,field2 " .
"FROM select_table",
"SELECT field_select AS field_insert,field2 AS field " .
"FROM select_table FOR UPDATE",
"INSERT INTO insert_table (field_insert,field) VALUES (0,1)"
],
[
[
'destTable' => 'insert_table',
'srcTable' => 'select_table',
'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
'conds' => [ 'field' => 2 ],
],
"INSERT INTO insert_table " .
"(field_insert,field) " .
"SELECT field_select,field2 " .
"FROM select_table " .
"WHERE field = 2",
"SELECT field_select AS field_insert,field2 AS field FROM " .
"select_table WHERE field = 2 FOR UPDATE",
"INSERT INTO insert_table (field_insert,field) VALUES (0,1)"
],
[
[
'destTable' => 'insert_table',
'srcTable' => 'select_table',
'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
'conds' => [ 'field' => 2 ],
'insertOptions' => 'IGNORE',
'selectOptions' => [ 'ORDER BY' => 'field' ],
],
"INSERT IGNORE INTO insert_table " .
"(field_insert,field) " .
"SELECT field_select,field2 " .
"FROM select_table " .
"WHERE field = 2 " .
"ORDER BY field",
"SELECT field_select AS field_insert,field2 AS field " .
"FROM select_table WHERE field = 2 ORDER BY field FOR UPDATE",
"INSERT IGNORE INTO insert_table (field_insert,field) VALUES (0,1)"
],
[
[
'destTable' => 'insert_table',
'srcTable' => [ 'select_table1', 'select_table2' ],
'varMap' => [ 'field_insert' => 'field_select', 'field' => 'field2' ],
'conds' => [ 'field' => 2 ],
'insertOptions' => [ 'NO_AUTO_COLUMNS' ],
'selectOptions' => [ 'ORDER BY' => 'field', 'FORCE INDEX' => [ 'select_table1' => 'index1' ] ],
'selectJoinConds' => [
'select_table2' => [ 'LEFT JOIN', [ 'select_table1.foo = select_table2.bar' ] ],
],
],
"INSERT INTO insert_table " .
"(field_insert,field) " .
"SELECT field_select,field2 " .
"FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
"WHERE field = 2 " .
"ORDER BY field",
"SELECT field_select AS field_insert,field2 AS field " .
"FROM select_table1 LEFT JOIN select_table2 ON ((select_table1.foo = select_table2.bar)) " .
"WHERE field = 2 ORDER BY field FOR UPDATE",
"INSERT INTO insert_table (field_insert,field) VALUES (0,1)"
],
];
}
public function testInsertSelectBatching() {
$dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
$rows = [];
for ( $i = 0; $i <= 25000; $i++ ) {
$rows[] = [ 'field' => $i ];
}
$dbWeb->forceNextResult( $rows );
$dbWeb->insertSelect(
'insert_table',
'select_table',
[ 'field' => 'field2' ],
'*',
__METHOD__
);
$this->assertLastSqlDb( implode( '; ', [
'SELECT field2 AS field FROM select_table FOR UPDATE',
'BEGIN',
"INSERT INTO insert_table (field) VALUES (" . implode( "),(", range( 0, 9999 ) ) . ")",
"INSERT INTO insert_table (field) VALUES (" . implode( "),(", range( 10000, 19999 ) ) . ")",
"INSERT INTO insert_table (field) VALUES (" . implode( "),(", range( 20000, 25000 ) ) . ")",
'COMMIT'
] ), $dbWeb );
}
/**
* @dataProvider provideReplace
*/
public function testReplace( $sql, $sqlText ) {
$this->database->replace(
$sql['table'],
$sql['uniqueIndexes'],
$sql['rows'],
__METHOD__
);
$this->assertLastSql( $sqlText );
}
public static function provideReplace() {
return [
[
[
'table' => 'replace_table',
'uniqueIndexes' => [ 'field' ],
'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
],
"BEGIN; DELETE FROM replace_table " .
"WHERE (field = 'text'); " .
"INSERT INTO replace_table " .
"(field,field2) " .
"VALUES ('text','text2'); COMMIT"
],
[
[
'table' => 'module_deps',
'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
'rows' => [
'md_module' => 'module',
'md_skin' => 'skin',
'md_deps' => 'deps',
],
],
"BEGIN; DELETE FROM module_deps " .
"WHERE (md_module = 'module' AND md_skin = 'skin'); " .
"INSERT INTO module_deps " .
"(md_module,md_skin,md_deps) " .
"VALUES ('module','skin','deps'); COMMIT"
],
[
[
'table' => 'module_deps',
'uniqueIndexes' => [ [ 'md_module', 'md_skin' ] ],
'rows' => [
[
'md_module' => 'module',
'md_skin' => 'skin',
'md_deps' => 'deps',
], [
'md_module' => 'module2',
'md_skin' => 'skin2',
'md_deps' => 'deps2',
],
],
],
"BEGIN; DELETE FROM module_deps " .
"WHERE (md_module = 'module' AND md_skin = 'skin'); " .
"INSERT INTO module_deps " .
"(md_module,md_skin,md_deps) " .
"VALUES ('module','skin','deps'); " .
"DELETE FROM module_deps " .
"WHERE (md_module = 'module2' AND md_skin = 'skin2'); " .
"INSERT INTO module_deps " .
"(md_module,md_skin,md_deps) " .
"VALUES ('module2','skin2','deps2'); COMMIT"
],
];
}
public function testTransactionCommit() {
$this->database->begin( __METHOD__ );
$this->database->commit( __METHOD__ );
$this->assertLastSql( 'BEGIN; COMMIT' );
}
public function testTransactionRollback() {
$this->database->begin( __METHOD__ );
$this->database->rollback( __METHOD__ );
$this->assertLastSql( 'BEGIN; ROLLBACK' );
}
public function testDropTable() {
$this->database->setExistingTables( [ 'table' ] );
$this->database->dropTable( 'table', __METHOD__ );
$this->assertLastSql( 'DROP TABLE table CASCADE' );
}
public function testDropNonExistingTable() {
$this->assertFalse(
$this->database->dropTable( 'non_existing', __METHOD__ )
);
}
public function testSessionTempTables() {
$temp1 = $this->database->tableName( 'tmp_table_1' );
$temp2 = $this->database->tableName( 'tmp_table_2' );
$temp3 = $this->database->tableName( 'tmp_table_3' );
$this->database->query( "CREATE TEMPORARY TABLE $temp1 LIKE orig_tbl", __METHOD__ );
$this->database->query( "CREATE TEMPORARY TABLE $temp2 LIKE orig_tbl", __METHOD__ );
$this->database->query( "CREATE TEMPORARY TABLE $temp3 LIKE orig_tbl", __METHOD__ );
$this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
$this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
$this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
$this->database->dropTable( 'tmp_table_1', __METHOD__ );
$this->database->dropTable( 'tmp_table_2', __METHOD__ );
$this->database->dropTable( 'tmp_table_3', __METHOD__ );
$this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
$this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
$this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
$this->database->query( "CREATE TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
$this->database->query( "CREATE TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
$this->database->query( "CREATE TEMPORARY TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
$this->assertTrue( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
$this->assertTrue( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
$this->assertTrue( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
$this->database->query( "DROP TEMPORARY TABLE tmp_table_1 LIKE orig_tbl", __METHOD__ );
$this->database->query( "DROP TEMPORARY TABLE 'tmp_table_2' LIKE orig_tbl", __METHOD__ );
$this->database->query( "DROP TABLE `tmp_table_3` LIKE orig_tbl", __METHOD__ );
$this->assertFalse( $this->database->tableExists( "tmp_table_1", __METHOD__ ) );
$this->assertFalse( $this->database->tableExists( "tmp_table_2", __METHOD__ ) );
$this->assertFalse( $this->database->tableExists( "tmp_table_3", __METHOD__ ) );
$this->database->query( "DROP TEMPORARY TABLE IF EXISTS `tmp_table_4`, `tmp_table_5`", __METHOD__ );
}
public function testAtomicSections() {
$this->database->startAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ );
$this->assertLastSql( 'BEGIN; COMMIT' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->cancelAtomic( __METHOD__ );
$this->assertLastSql( 'BEGIN; ROLLBACK' );
$this->database->begin( __METHOD__ );
$this->database->startAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ );
$this->database->commit( __METHOD__ );
$this->assertLastSql( 'BEGIN; COMMIT' );
$this->database->begin( __METHOD__ );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->endAtomic( __METHOD__ );
$this->database->commit( __METHOD__ );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
$this->database->begin( __METHOD__ );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->cancelAtomic( __METHOD__ );
$this->database->commit( __METHOD__ );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->cancelAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
$noOpCallack = static function () {
};
$this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE );
$this->assertLastSql( 'BEGIN; COMMIT' );
$this->database->doAtomicSection( __METHOD__, $noOpCallack );
$this->assertLastSql( 'BEGIN; COMMIT' );
$this->database->begin( __METHOD__ );
$this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE );
$this->database->rollback( __METHOD__ );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' );
$fname = __METHOD__;
$triggerMap = [
'-' => '-',
IDatabase::TRIGGER_COMMIT => 'tCommit',
IDatabase::TRIGGER_ROLLBACK => 'tRollback',
IDatabase::TRIGGER_CANCEL => 'tCancel',
];
$pcCallback = function ( IDatabase $db ) use ( $fname ) {
$this->database->query( "SELECT 0", $fname );
};
$callback1 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
$this->database->query( "SELECT 1, {$triggerMap[$trigger]} AS t", $fname );
};
$callback2 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
$this->database->query( "SELECT 2, {$triggerMap[$trigger]} AS t", $fname );
};
$callback3 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
$this->database->query( "SELECT 3, {$triggerMap[$trigger]} AS t", $fname );
};
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
$this->database->cancelAtomic( __METHOD__ );
$this->assertLastSql( 'BEGIN; ROLLBACK' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
$this->database->cancelAtomic( __METHOD__ );
$this->assertLastSql( 'BEGIN; ROLLBACK' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionResolution( $callback1, __METHOD__ );
$this->database->cancelAtomic( __METHOD__ );
$this->assertLastSql( 'BEGIN; ROLLBACK; SELECT 1, tRollback AS t' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $callback1, __METHOD__ );
$this->database->cancelAtomic( __METHOD__ );
$this->assertLastSql( 'BEGIN; ROLLBACK; SELECT 1, tRollback AS t' );
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
$this->database->cancelAtomic( __METHOD__ );
$this->database->onTransactionPreCommitOrIdle( $pcCallback, __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertLastSql( implode( "; ", [
'BEGIN',
'SAVEPOINT wikimedia_rdbms_atomic1',
'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
'SELECT 0',
'SELECT 0',
'COMMIT'
] ) );
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionCommitOrIdle( $callback2, __METHOD__ );
$this->database->cancelAtomic( __METHOD__ );
$this->database->onTransactionCommitOrIdle( $callback3, __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertLastSql( implode( "; ", [
'BEGIN',
'SAVEPOINT wikimedia_rdbms_atomic1',
'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
'COMMIT',
'SELECT 1, tCommit AS t',
'SELECT 3, tCommit AS t'
] ) );
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->onTransactionResolution( $callback1, __METHOD__ );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionResolution( $callback2, __METHOD__ );
$this->database->cancelAtomic( __METHOD__ );
$this->database->onTransactionResolution( $callback3, __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertLastSql( implode( "; ", [
'BEGIN',
'SAVEPOINT wikimedia_rdbms_atomic1',
'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
'COMMIT',
'SELECT 1, tCommit AS t',
'SELECT 2, tRollback AS t',
'SELECT 3, tCommit AS t'
] ) );
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->onAtomicSectionCancel( $callback1, __METHOD__ );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $callback2, __METHOD__ );
$this->database->cancelAtomic( __METHOD__ );
$this->database->onAtomicSectionCancel( $callback3, __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertLastSql( implode( "; ", [
'BEGIN',
'SAVEPOINT wikimedia_rdbms_atomic1',
'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
'SELECT 2, tCancel AS t',
'COMMIT',
] ) );
$makeCallback = function ( $id ) use ( $fname, $triggerMap ) {
return function ( $trigger = '-' ) use ( $id, $fname, $triggerMap ) {
$this->database->query( "SELECT $id, {$triggerMap[$trigger]} AS t", $fname );
};
};
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
$this->database->cancelAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertLastSql( implode( "; ", [
'BEGIN',
'SAVEPOINT wikimedia_rdbms_atomic1',
'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
'COMMIT',
'SELECT 1, tRollback AS t'
] ) );
$this->database->startAtomic( __METHOD__ . '_level1', IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
$this->database->startAtomic( __METHOD__ . '_level2' );
$this->database->startAtomic( __METHOD__ . '_level3', IDatabase::ATOMIC_CANCELABLE );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionResolution( $makeCallback( 2 ), __METHOD__ );
$this->database->endAtomic( __METHOD__ );
$this->database->onTransactionResolution( $makeCallback( 3 ), __METHOD__ );
$this->database->cancelAtomic( __METHOD__ . '_level3' );
$this->database->endAtomic( __METHOD__ . '_level2' );
$this->database->onTransactionResolution( $makeCallback( 4 ), __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_level1' );
$this->assertLastSql( implode( "; ", [
'BEGIN',
'SAVEPOINT wikimedia_rdbms_atomic1',
'SAVEPOINT wikimedia_rdbms_atomic2',
'RELEASE SAVEPOINT wikimedia_rdbms_atomic2',
'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
'COMMIT; SELECT 1, tCommit AS t',
'SELECT 2, tRollback AS t',
'SELECT 3, tRollback AS t',
'SELECT 4, tCommit AS t'
] ) );
$this->database->startAtomic( __METHOD__ . '_level1', IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $makeCallback( 1 ), __METHOD__ );
$this->database->startAtomic( __METHOD__ . '_level2' );
$this->database->startAtomic( __METHOD__ . '_level3', IDatabase::ATOMIC_CANCELABLE );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $makeCallback( 2 ), __METHOD__ );
$this->database->endAtomic( __METHOD__ );
$this->database->onAtomicSectionCancel( $makeCallback( 3 ), __METHOD__ );
$this->database->cancelAtomic( __METHOD__ . '_level3' );
$this->database->endAtomic( __METHOD__ . '_level2' );
$this->database->onAtomicSectionCancel( $makeCallback( 4 ), __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_level1' );
$this->assertLastSql( implode( "; ", [
'BEGIN',
'SAVEPOINT wikimedia_rdbms_atomic1',
'SAVEPOINT wikimedia_rdbms_atomic2',
'RELEASE SAVEPOINT wikimedia_rdbms_atomic2',
'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
'SELECT 2, tCancel AS t',
'SELECT 3, tCancel AS t',
'COMMIT',
] ) );
}
public function testAtomicSectionsRecovery() {
$this->database->begin( __METHOD__ );
try {
$this->database->doAtomicSection(
__METHOD__,
function () {
$this->database->startAtomic( 'inner_func1' );
$this->database->startAtomic( 'inner_func2' );
throw new RuntimeException( 'Test exception' );
},
IDatabase::ATOMIC_CANCELABLE
);
$this->fail( 'Expected exception not thrown' );
} catch ( RuntimeException $ex ) {
$this->assertSame( 'Test exception', $ex->getMessage() );
}
$this->database->commit( __METHOD__ );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
$this->database->begin( __METHOD__ );
try {
$this->database->doAtomicSection(
__METHOD__,
static function () {
throw new RuntimeException( 'Test exception' );
}
);
$this->fail( 'Test exception not thrown' );
} catch ( RuntimeException $ex ) {
$this->assertSame( 'Test exception', $ex->getMessage() );
}
try {
$this->database->commit( __METHOD__ );
$this->fail( 'Test exception not thrown' );
} catch ( DBTransactionError $ex ) {
$this->assertSame(
'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR',
$ex->getMessage()
);
}
$this->database->rollback( __METHOD__ );
$this->assertLastSql( 'BEGIN; ROLLBACK' );
}
public function testAtomicSectionsCallbackCancellation() {
$fname = __METHOD__;
$callback1Called = null;
$callback1 = function ( $trigger = '-' ) use ( $fname, &$callback1Called ) {
$callback1Called = $trigger;
$this->database->query( "SELECT 1", $fname );
};
$callback2Called = null;
$callback2 = function ( $trigger = '-' ) use ( $fname, &$callback2Called ) {
$callback2Called = $trigger;
$this->database->query( "SELECT 2", $fname );
};
$callback3Called = null;
$callback3 = function ( $trigger = '-' ) use ( $fname, &$callback3Called ) {
$callback3Called = $trigger;
$this->database->query( "SELECT 3", $fname );
};
$callback4Called = 0;
$callback4 = function () use ( $fname, &$callback4Called ) {
$callback4Called++;
$this->database->query( "SELECT 4", $fname );
};
$callback5Called = 0;
$callback5 = function () use ( $fname, &$callback5Called ) {
$callback5Called++;
$this->database->query( "SELECT 5", $fname );
};
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->startAtomic( __METHOD__ . '_inner' );
$this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
$this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
$this->database->onTransactionResolution( $callback3, __METHOD__ );
$this->database->onAtomicSectionCancel( $callback4, __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_inner' );
$this->database->cancelAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertNull( $callback1Called );
$this->assertNull( $callback2Called );
$this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
$this->assertSame( 1, $callback4Called );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; SELECT 4; COMMIT; SELECT 3' );
$callback1Called = null;
$callback2Called = null;
$callback3Called = null;
$callback4Called = 0;
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE );
$this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
$this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
$this->database->onTransactionResolution( $callback3, __METHOD__ );
$this->database->onAtomicSectionCancel( $callback4, __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_inner' );
$this->database->cancelAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertNull( $callback1Called );
$this->assertNull( $callback2Called );
$this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
$this->assertSame( 1, $callback4Called );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; RELEASE SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; SELECT 4; COMMIT; SELECT 3' );
$callback1Called = null;
$callback2Called = null;
$callback3Called = null;
$callback4Called = 0;
$this->database->startAtomic( __METHOD__ . '_outer' );
$atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->startAtomic( __METHOD__ . '_inner' );
$this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
$this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
$this->database->onTransactionResolution( $callback3, __METHOD__ );
$this->database->onAtomicSectionCancel( $callback4, __METHOD__ );
$this->database->cancelAtomic( __METHOD__, $atomicId );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertNull( $callback1Called );
$this->assertNull( $callback2Called );
$this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
$this->assertSame( 1, $callback4Called );
$callback1Called = null;
$callback2Called = null;
$callback3Called = null;
$callback4Called = 0;
$this->database->startAtomic( __METHOD__ . '_outer' );
$atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->startAtomic( __METHOD__ . '_inner' );
$this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
$this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
$this->database->onTransactionResolution( $callback3, __METHOD__ );
$this->database->onAtomicSectionCancel( $callback4, __METHOD__ );
try {
$this->database->cancelAtomic( __METHOD__ . '_X', $atomicId );
} catch ( DBUnexpectedError $e ) {
$m = __METHOD__;
$this->assertSame(
"Invalid atomic section ended (got {$m}_X but expected {$m})",
$e->getMessage()
);
}
$this->database->cancelAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertNull( $callback1Called );
$this->assertNull( $callback2Called );
$this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
$this->assertSame( 1, $callback4Called );
$callback4Called = 0;
$callback5Called = 0;
$this->database->getLastSqls(); // flush
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $callback5, __METHOD__ );
$this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $callback4, __METHOD__ );
$this->database->cancelAtomic( __METHOD__ . '_inner' );
$this->database->cancelAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic2; SELECT 4; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; SELECT 5; COMMIT' );
$this->assertSame( 1, $callback4Called );
$this->assertSame( 1, $callback5Called );
$callback4Called = 0;
$callback5Called = 0;
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $callback5, __METHOD__ );
$this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $callback4, __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_inner' );
$this->database->cancelAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; RELEASE SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; SELECT 5; SELECT 4; COMMIT' );
$this->assertSame( 1, $callback4Called );
$this->assertSame( 1, $callback5Called );
$callback4Called = 0;
$callback5Called = 0;
$this->database->startAtomic( __METHOD__ . '_outer' );
$sectionId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $callback5, __METHOD__ );
$this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE );
$this->database->onAtomicSectionCancel( $callback4, __METHOD__ );
$this->database->cancelAtomic( __METHOD__, $sectionId );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; SELECT 5; SELECT 4; COMMIT' );
$this->assertSame( 1, $callback4Called );
$this->assertSame( 1, $callback5Called );
$wrapper = TestingAccessWrapper::newFromObject( $this->database );
$callback1Called = null;
$callback2Called = null;
$callback3Called = null;
$callback4Called = 0;
$this->database->startAtomic( __METHOD__ . '_outer' );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->startAtomic( __METHOD__ . '_inner' );
$this->database->onTransactionCommitOrIdle( $callback1, __METHOD__ );
$this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
$this->database->onTransactionResolution( $callback3, __METHOD__ );
$this->database->onAtomicSectionCancel( $callback4, __METHOD__ );
$wrapper->transactionManager->setTransactionError( new DBUnexpectedError( null, 'error' ) );
$this->database->cancelAtomic( __METHOD__ . '_inner' );
$this->database->cancelAtomic( __METHOD__ );
$this->database->endAtomic( __METHOD__ . '_outer' );
$this->assertNull( $callback1Called );
$this->assertNull( $callback2Called );
$this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
$this->assertSame( 1, $callback4Called );
}
public function testAtomicSectionsTrxRound() {
$this->database->setFlag( IDatabase::DBO_TRX );
$this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->database->query( 'SELECT 1', __METHOD__ );
$this->database->endAtomic( __METHOD__ );
$this->database->commit( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SELECT 1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
}
public static function provideAtomicSectionMethodsForErrors() {
return [
[ 'endAtomic' ],
[ 'cancelAtomic' ],
];
}
/**
* @dataProvider provideAtomicSectionMethodsForErrors
*/
public function testNoAtomicSection( $method ) {
try {
$this->database->$method( __METHOD__ );
$this->fail( 'Expected exception not thrown' );
} catch ( DBUnexpectedError $ex ) {
$this->assertSame(
'No atomic section is open (got ' . __METHOD__ . ')',
$ex->getMessage()
);
}
}
/**
* @dataProvider provideAtomicSectionMethodsForErrors
*/
public function testInvalidAtomicSectionEnded( $method ) {
$this->database->startAtomic( __METHOD__ . 'X' );
try {
$this->database->$method( __METHOD__ );
$this->fail( 'Expected exception not thrown' );
} catch ( DBUnexpectedError $ex ) {
$this->assertSame(
'Invalid atomic section ended (got ' . __METHOD__ . ' but expected ' .
__METHOD__ . 'X)',
$ex->getMessage()
);
}
}
public function testUncancellableAtomicSection() {
$this->database->startAtomic( __METHOD__ );
try {
$this->database->cancelAtomic( __METHOD__ );
$this->database->select( 'test', '1', [], __METHOD__ );
$this->fail( 'Expected exception not thrown' );
} catch ( DBTransactionError $ex ) {
$this->assertSame(
'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR',
$ex->getMessage()
);
}
}
public function testNoAtomicSectionForCallback() {
try {
$this->database->onAtomicSectionCancel( static function () {
}, __METHOD__ );
$this->fail( 'Expected exception not thrown' );
} catch ( DBUnexpectedError $ex ) {
$this->assertSame(
'No atomic section is open (got ' . __METHOD__ . ')',
$ex->getMessage()
);
}
}
public function testTransactionErrorState1() {
$wrapper = TestingAccessWrapper::newFromObject( $this->database );
$this->database->begin( __METHOD__ );
$wrapper->transactionManager->setTransactionError( new DBUnexpectedError( null, 'error' ) );
$this->expectException( DBTransactionStateError::class );
$this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
}
public function testTransactionErrorState2() {
$wrapper = TestingAccessWrapper::newFromObject( $this->database );
// TODO: This really needs a better place
$this->database->startAtomic( __METHOD__ );
$wrapper->transactionManager->setTransactionError( new DBUnexpectedError( null, 'error' ) );
$this->database->rollback( __METHOD__ );
$this->assertSame( 0, $this->database->trxLevel() );
$this->assertEquals( TransactionManager::STATUS_TRX_NONE, $wrapper->trxStatus() );
$this->assertLastSql( 'BEGIN; ROLLBACK' );
$this->database->startAtomic( __METHOD__ );
$this->assertEquals( TransactionManager::STATUS_TRX_OK, $wrapper->trxStatus() );
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
$this->database->endAtomic( __METHOD__ );
$this->assertEquals( TransactionManager::STATUS_TRX_NONE, $wrapper->trxStatus() );
$this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = 1; COMMIT' );
$this->assertSame( 0, $this->database->trxLevel(), 'Use after rollback()' );
$this->database->begin( __METHOD__ );
$this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
$this->database->update( 'y', [ 'a' => 1 ], [ 'field' => 1 ], __METHOD__ );
$wrapper->transactionManager->setTransactionError( new DBUnexpectedError( null, 'error' ) );
$this->database->cancelAtomic( __METHOD__ );
$this->assertEquals( TransactionManager::STATUS_TRX_OK, $wrapper->trxStatus() );
$this->database->startAtomic( __METHOD__ );
$this->database->delete( 'y', [ 'field' => 1 ], __METHOD__ );
$this->database->endAtomic( __METHOD__ );
$this->database->commit( __METHOD__ );
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; UPDATE y SET a = 1 WHERE field = 1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM y WHERE field = 1; COMMIT' );
$this->assertSame( 0, $this->database->trxLevel(), 'Use after rollback()' );
// Next transaction
$this->database->startAtomic( __METHOD__ );
$this->assertEquals( TransactionManager::STATUS_TRX_OK, $wrapper->trxStatus() );
$this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
$this->database->endAtomic( __METHOD__ );
$this->assertEquals( TransactionManager::STATUS_TRX_NONE, $wrapper->trxStatus() );
$this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = 3; COMMIT' );
$this->assertSame( 0, $this->database->trxLevel() );
}
public function testImplicitTransactionRollback() {
$doError = function () {
$this->database->forceNextResult( false, 666, 'Evilness' );
try {
$this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
$this->fail( 'Expected exception not thrown' );
} catch ( DBError $e ) {
$this->assertSame( 666, $e->errno );
}
};
$this->database->setFlag( Database::DBO_TRX );
// Implicit transaction does not get silently rolled back
$this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
$doError();
try {
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
$this->fail( 'Expected exception not thrown' );
} catch ( DBTransactionError $e ) {
$this->assertEquals(
'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR',
$e->getMessage()
);
}
try {
$this->database->commit( __METHOD__, Database::FLUSHING_INTERNAL );
$this->fail( 'Expected exception not thrown' );
} catch ( DBTransactionError $e ) {
$this->assertEquals(
'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR',
$e->getMessage()
);
}
$this->database->rollback( __METHOD__, Database::FLUSHING_INTERNAL );
$this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK' );
// Likewise if there were prior writes
$this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
$doError();
try {
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
$this->fail( 'Expected exception not thrown' );
} catch ( DBTransactionStateError $e ) {
}
$this->database->rollback( __METHOD__, Database::FLUSHING_INTERNAL );
// phpcs:ignore
$this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = 1; DELETE FROM error WHERE 1; ROLLBACK' );
}
public function testTransactionStatementRollbackIgnoring() {
$wrapper = TestingAccessWrapper::newFromObject( $this->database );
$warning = [];
$wrapper->deprecationLogger = static function ( $msg ) use ( &$warning ) {
$warning[] = $msg;
};
$doError = function () {
$this->database->forceNextResult(
false,
666,
'Evilness',
[ 'isKnownStatementRollbackError' => true ]
);
try {
$this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
$this->fail( 'Expected exception not thrown' );
} catch ( DBError $e ) {
$this->assertSame( 666, $e->errno );
}
};
$expectWarning = 'Caller from ' . __METHOD__ .
' ignored an error originally raised from ' . __CLASS__ . '::SomeCaller: [666] Evilness';
// Rollback doesn't raise a warning
$warning = [];
$this->database->startAtomic( __METHOD__ );
$doError();
$this->database->rollback( __METHOD__ );
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
$this->assertSame( [], $warning );
// phpcs:ignore
$this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK; DELETE FROM x WHERE field = 1' );
// cancelAtomic() doesn't raise a warning
$warning = [];
$this->database->begin( __METHOD__ );
$this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
$doError();
$this->database->cancelAtomic( __METHOD__ );
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
$this->database->commit( __METHOD__ );
$this->assertSame( [], $warning );
// phpcs:ignore
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM error WHERE 1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM x WHERE field = 1; COMMIT' );
// Commit does raise a warning
$warning = [];
$this->database->begin( __METHOD__ );
$doError();
$this->database->commit( __METHOD__ );
$this->assertSame( [ $expectWarning ], $warning );
$this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; COMMIT' );
// Deprecation only gets raised once
$warning = [];
$this->database->begin( __METHOD__ );
$doError();
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
$this->database->commit( __METHOD__ );
$this->assertSame( [ $expectWarning ], $warning );
// phpcs:ignore
$this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; DELETE FROM x WHERE field = 1; COMMIT' );
}
public function testPrematureClose1() {
$fname = __METHOD__;
$this->database->begin( __METHOD__ );
$this->database->onTransactionCommitOrIdle( function () use ( $fname ) {
$this->database->query( 'SELECT 1', $fname );
} );
$this->database->onTransactionResolution( function () use ( $fname ) {
$this->database->query( 'SELECT 2', $fname );
} );
$this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
$this->database->close();
$this->assertFalse( $this->database->isOpen() );
$this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = 3; ROLLBACK; SELECT 2' );
$this->assertSame( 0, $this->database->trxLevel() );
}
public function testPrematureClose2() {
$fname = __METHOD__;
$this->database->startAtomic( __METHOD__ );
$this->database->onTransactionCommitOrIdle( function () use ( $fname ) {
$this->database->query( 'SELECT 1', $fname );
} );
$this->database->onAtomicSectionCancel( function () use ( $fname ) {
$this->database->query( 'SELECT 2', $fname );
} );
$this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
$this->database->close();
$this->assertFalse( $this->database->isOpen() );
$this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = 3; ROLLBACK; SELECT 2' );
$this->assertSame( 0, $this->database->trxLevel() );
}
public function testPrematureClose3() {
$this->database->setFlag( IDatabase::DBO_TRX );
$this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
$this->assertSame( 1, $this->database->trxLevel() );
$this->database->close();
$this->assertFalse( $this->database->isOpen() );
$this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = 3; ROLLBACK' );
$this->assertSame( 0, $this->database->trxLevel() );
}
public function testPrematureClose4() {
$this->database->setFlag( IDatabase::DBO_TRX );
$this->database->query( 'SELECT 1', __METHOD__ );
$this->assertSame( 1, $this->database->trxLevel() );
$this->database->close();
$this->database->clearFlag( IDatabase::DBO_TRX );
$this->assertFalse( $this->database->isOpen() );
$this->assertLastSql( 'BEGIN; SELECT 1; ROLLBACK' );
$this->assertSame( 0, $this->database->trxLevel() );
}
public function testSelectFieldValues() {
$this->database->forceNextResult( [
(object)[ 'value' => 'row1' ],
(object)[ 'value' => 'row2' ],
(object)[ 'value' => 'row3' ],
] );
$this->assertSame(
[ 'row1', 'row2', 'row3' ],
$this->database->selectFieldValues( 'table', 'table.field', 'conds', __METHOD__ )
);
$this->assertLastSql( 'SELECT table.field AS value FROM table WHERE conds' );
}
}