Файловый менеджер - Редактировать - /var/www/html/mediawiki-1.43.1/tests/phpunit/unit/includes/libs/rdbms/database/DatabaseSQLTest.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' ); } }
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0 |
proxy
|
phpinfo
|
ÐаÑтройка