rdbms: Add callback for atomic section cancellation
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / rdbms / database / DatabaseSQLTest.php
index 40c260c..3d79afe 100644 (file)
@@ -1343,7 +1343,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
        }
 
        /**
-        * @covers Wikimedia\Rdbms\Database::registerTempTableOperation
+        * @covers Wikimedia\Rdbms\Database::registerTempTableWrite
         */
        public function testSessionTempTables() {
                $temp1 = $this->database->tableName( 'tmp_table_1' );
@@ -1488,7 +1488,8 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                $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 );
@@ -1518,6 +1519,11 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                $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 );
@@ -1567,6 +1573,21 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                        '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 );
@@ -1609,6 +1630,29 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                        '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',
+               ] ) );
        }
 
        /**
@@ -1692,6 +1736,16 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                        $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 );
@@ -1699,57 +1753,67 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                $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 ) {
@@ -1764,30 +1828,65 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                $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__ );
@@ -1795,6 +1894,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                $this->assertNull( $callback1Called );
                $this->assertNull( $callback2Called );
                $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+               $this->assertEquals( 1, $callback4Called );
        }
 
        /**
@@ -1876,9 +1976,25 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                }
        }
 
+       /**
+        * @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 );
@@ -2091,6 +2207,9 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                        $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' );
@@ -2103,7 +2222,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                }
 
                $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() );
        }