$triggerMap = [
'-' => '-',
IDatabase::TRIGGER_COMMIT => 'tCommit',
- IDatabase::TRIGGER_ROLLBACK => 'tRollback'
+ IDatabase::TRIGGER_ROLLBACK => 'tRollback',
+ IDatabase::TRIGGER_CANCEL => 'tCancel',
];
$pcCallback = function ( IDatabase $db ) use ( $fname ) {
$this->database->query( "SELECT 0", $fname );
$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 );
'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 );
'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',
+ ] ) );
}
/**
$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->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->assertEquals( 1, $callback4Called );
// phpcs:ignore Generic.Files.LineLength
- $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
+ $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->assertEquals( 1, $callback4Called );
// phpcs:ignore Generic.Files.LineLength
- $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; RELEASE SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
+ $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->assertEquals( 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 ) {
$this->assertNull( $callback1Called );
$this->assertNull( $callback2Called );
$this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+ $this->assertEquals( 1, $callback4Called );
+ $callback4Called = 0;
+ $callback5Called = 0;
+ $this->database->getLastSqls(); // flush
$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( $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->assertNull( $callback1Called );
- $this->assertNull( $callback2Called );
- $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+ // phpcs:ignore Generic.Files.LineLength
+ $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->assertEquals( 1, $callback4Called );
+ $this->assertEquals( 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' );
+ // phpcs:ignore Generic.Files.LineLength
+ $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->assertEquals( 1, $callback4Called );
+ $this->assertEquals( 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' );
+ // phpcs:ignore Generic.Files.LineLength
+ $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; SELECT 5; SELECT 4; COMMIT' );
+ $this->assertEquals( 1, $callback4Called );
+ $this->assertEquals( 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->trxStatus = Database::STATUS_TRX_ERROR;
$this->database->cancelAtomic( __METHOD__ . '_inner' );
$this->database->cancelAtomic( __METHOD__ );
$this->assertNull( $callback1Called );
$this->assertNull( $callback2Called );
$this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+ $this->assertEquals( 1, $callback4Called );
}
/**
}
}
+ /**
+ * @covers \Wikimedia\Rdbms\Database::onAtomicSectionCancel
+ */
+ public function testNoAtomicSectionForCallback() {
+ try {
+ $this->database->onAtomicSectionCancel( function () {
+ }, __METHOD__ );
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( DBUnexpectedError $ex ) {
+ $this->assertSame(
+ 'No atomic section is open (got ' . __METHOD__ . ').',
+ $ex->getMessage()
+ );
+ }
+ }
+
/**
* @expectedException \Wikimedia\Rdbms\DBTransactionStateError
- * @covers \Wikimedia\Rdbms\Database::assertTransactionStatus
+ * @covers \Wikimedia\Rdbms\Database::assertQueryIsCurrentlyAllowed
*/
public function testTransactionErrorState1() {
$wrapper = TestingAccessWrapper::newFromObject( $this->database );
$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->fail( 'Expected exception not thrown' );
}
$this->assertFalse( $this->database->isOpen() );
- $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
+ $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK; SELECT 2' );
$this->assertEquals( 0, $this->database->trxLevel() );
}