use Wikimedia\TestingAccessWrapper;
use Wikimedia\Rdbms\DBTransactionStateError;
use Wikimedia\Rdbms\DBUnexpectedError;
+use Wikimedia\Rdbms\DBTransactionError;
/**
* Test the parts of the Database abstract class that deal
class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
+ use PHPUnit4And6Compat;
/** @var DatabaseTestHelper|Database */
private $database;
// phpcs:ignore Generic.Files.LineLength
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
- $this->database->doAtomicSection( __METHOD__, function () {
- } );
+ $noOpCallack = 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__, function () {
- } );
+ $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE );
$this->database->rollback( __METHOD__ );
// phpcs:ignore Generic.Files.LineLength
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' );
$this->database->begin( __METHOD__ );
try {
- $this->database->doAtomicSection( __METHOD__, function () {
- throw new RuntimeException( 'Test exception' );
- } );
+ $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__ );
// phpcs:ignore Generic.Files.LineLength
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+
+ $this->database->begin( __METHOD__ );
+ try {
+ $this->database->doAtomicSection(
+ __METHOD__,
+ function () {
+ throw new RuntimeException( 'Test exception' );
+ }
+ );
+ $this->fail( 'Test exception not thrown' );
+ } catch ( RuntimeException $ex ) {
+ $this->assertSame( 'Test exception', $ex->getMessage() );
+ }
+ $this->database->rollback( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; ROLLBACK' );
}
public static function provideAtomicSectionMethodsForErrors() {
$this->fail( 'Expected exception not thrown' );
} catch ( DBUnexpectedError $ex ) {
$this->assertSame(
- 'No atomic transaction is open (got ' . __METHOD__ . ').',
+ 'No atomic section is open (got ' . __METHOD__ . ').',
$ex->getMessage()
);
}
$this->fail( 'Expected exception not thrown' );
} catch ( DBUnexpectedError $ex ) {
$this->assertSame(
- 'Invalid atomic section ended (got ' . __METHOD__ . ').',
+ 'Invalid atomic section ended (got ' . __METHOD__ . ' but expected ' .
+ __METHOD__ . 'X' . ').',
$ex->getMessage()
);
}
$this->database->startAtomic( __METHOD__ );
try {
$this->database->cancelAtomic( __METHOD__ );
+ $this->database->select( 'test', '1', [], __METHOD__ );
$this->fail( 'Expected exception not thrown' );
- } catch ( DBUnexpectedError $ex ) {
+ } catch ( DBTransactionError $ex ) {
$this->assertSame(
- 'Uncancelable atomic section canceled (got ' . __METHOD__ . ').',
+ 'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
$ex->getMessage()
);
}
* @covers \Wikimedia\Rdbms\Database::query
*/
public function testImplicitTransactionRollback() {
- $doError = function ( $wasKnown = true ) {
+ $doError = function () {
$this->database->forceNextQueryError( 666, 'Evilness' );
try {
$this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
// Implicit transaction gets silently rolled back
$this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
- call_user_func( $doError, false );
+ call_user_func( $doError );
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
$this->database->commit( __METHOD__, Database::FLUSHING_INTERNAL );
// phpcs:ignore
// ... unless there were prior writes
$this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
- call_user_func( $doError, false );
+ call_user_func( $doError );
try {
$this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
$this->fail( 'Expected exception not thrown' );
$this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; DELETE FROM error WHERE 1; ROLLBACK' );
}
+ /**
+ * @covers \Wikimedia\Rdbms\Database::query
+ */
+ public function testTransactionStatementRollbackIgnoring() {
+ $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+ $warning = [];
+ $wrapper->deprecationLogger = function ( $msg ) use ( &$warning ) {
+ $warning[] = $msg;
+ };
+
+ $doError = function () {
+ $this->database->forceNextQueryError( 666, 'Evilness', [
+ 'wasKnownStatementRollbackError' => 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__ );
+ call_user_func( $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 );
+ call_user_func( $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__ );
+ call_user_func( $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__ );
+ call_user_func( $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' );
+ }
+
/**
* @covers \Wikimedia\Rdbms\Database::close
*/
$this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
$this->assertEquals( 0, $this->database->trxLevel() );
}
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::close
+ */
+ public function testPrematureClose3() {
+ try {
+ $this->database->setFlag( IDatabase::DBO_TRX );
+ $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+ $this->assertEquals( 1, $this->database->trxLevel() );
+ $this->database->close();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( DBUnexpectedError $ex ) {
+ $this->assertSame(
+ 'Wikimedia\Rdbms\Database::close: ' .
+ 'mass commit/rollback of peer transaction required (DBO_TRX set).',
+ $ex->getMessage()
+ );
+ }
+
+ $this->assertFalse( $this->database->isOpen() );
+ $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
+ $this->assertEquals( 0, $this->database->trxLevel() );
+ }
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::close
+ */
+ public function testPrematureClose4() {
+ $this->database->setFlag( IDatabase::DBO_TRX );
+ $this->database->query( 'SELECT 1', __METHOD__ );
+ $this->assertEquals( 1, $this->database->trxLevel() );
+ $this->database->close();
+ $this->database->clearFlag( IDatabase::DBO_TRX );
+
+ $this->assertFalse( $this->database->isOpen() );
+ $this->assertLastSql( 'BEGIN; SELECT 1; COMMIT' );
+ $this->assertEquals( 0, $this->database->trxLevel() );
+ }
}