From f9cc7e0416cb397410149455dbb8894628e2fb17 Mon Sep 17 00:00:00 2001 From: Petr Pchelko Date: Fri, 30 Aug 2019 11:26:00 -0700 Subject: [PATCH] Introduce RevisionStore::newRevisionsFromBatch Bug: T228988 Change-Id: Ia82e47e44dd70def6d6d5d4f598f9ae969645aae --- includes/Revision/RevisionStore.php | 121 ++++++++++++++++++ .../Revision/McrRevisionStoreDbTest.php | 42 ++++++ .../Revision/RevisionStoreDbTestBase.php | 110 +++++++++++++++- 3 files changed, 266 insertions(+), 7 deletions(-) diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index 818a53619d..73f622a3db 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -54,8 +54,10 @@ use Psr\Log\NullLogger; use RecentChange; use Revision; use RuntimeException; +use StatusValue; use stdClass; use Title; +use Traversable; use User; use WANObjectCache; use Wikimedia\Assert\Assert; @@ -1876,6 +1878,125 @@ class RevisionStore return $rev; } + /** + * Construct a RevisionRecord instance for each row in $rows, + * and return them as an associative array indexed by revision ID. + * @param Traversable|array $rows the rows to construct revision records from + * @param array $options Supports the following options: + * 'slots' - whether metadata about revision slots should be + * loaded immediately. Supports falsy or truthy value as well + * as an explicit list of slot role names. + * 'content'- whether the actual content of the slots should be + * preloaded. TODO: no supported yet. + * @param int $queryFlags + * @param Title|null $title + * @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions + * and an array of errors for the revisions failed to fetch. + */ + public function newRevisionsFromBatch( + $rows, + array $options = [], + $queryFlags = 0, + Title $title = null + ) { + $result = new StatusValue(); + + $rowsByRevId = []; + $pageIds = []; + $titlesByPageId = []; + foreach ( $rows as $row ) { + if ( isset( $rowsByRevId[$row->rev_id] ) ) { + throw new InvalidArgumentException( "Duplicate rows in newRevisionsFromBatch {$row->rev_id}" ); + } + if ( $title && $row->rev_page != $title->getArticleID() ) { + throw new InvalidArgumentException( + "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}" + ); + } + $pageIds[] = $row->rev_page; + $rowsByRevId[$row->rev_id] = $row; + } + + if ( empty( $rowsByRevId ) ) { + $result->setResult( true, [] ); + return $result; + } + + // If the title is not supplied, batch-fetch Title objects. + if ( $title ) { + $titlesByPageId[$title->getArticleID()] = $title; + } else { + $pageIds = array_unique( $pageIds ); + foreach ( Title::newFromIDs( $pageIds ) as $t ) { + $titlesByPageId[$t->getArticleID()] = $t; + } + } + + if ( !isset( $options['slots'] ) || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) { + $result->setResult( true, + array_map( function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) { + try { + return $this->newRevisionFromRow( + $row, + $queryFlags, + $titlesByPageId[$row->rev_page] + ); + } catch ( MWException $e ) { + $result->warning( 'internalerror', $e->getMessage() ); + return null; + } + }, $rowsByRevId ) + ); + return $result; + } + + $slotQueryConds = [ 'slot_revision_id' => array_keys( $rowsByRevId ) ]; + if ( is_array( $options['slots'] ) ) { + $slotQueryConds['slot_role_id'] = array_map( function ( $slot_name ) { + return $this->slotRoleStore->getId( $slot_name ); + }, $options['slots'] ); + } + + // TODO: Support optional fetching of the content + $queryInfo = self::getSlotsQueryInfo( [ 'content' ] ); + $db = $this->getDBConnectionRefForQueryFlags( $queryFlags ); + $slotRows = $db->select( + $queryInfo['tables'], + $queryInfo['fields'], + $slotQueryConds, + __METHOD__, + [], + $queryInfo['joins'] + ); + + $slotRowsByRevId = []; + foreach ( $slotRows as $slotRow ) { + $slotRowsByRevId[$slotRow->slot_revision_id][] = $slotRow; + } + $result->setResult( true, array_map( function ( $row ) use + ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $result ) { + if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) { + $result->warning( + 'internalerror', + "Couldn't find slots for rev {$row->rev_id}" + ); + return null; + } + try { + return $this->newRevisionFromRowAndSlots( + $row, + $slotRowsByRevId[$row->rev_id], + $queryFlags, + $titlesByPageId[$row->rev_page] + ); + } catch ( MWException $e ) { + $result->warning( 'internalerror', $e->getMessage() ); + return null; + } + }, $rowsByRevId ) ); + return $result; + } + /** * Constructs a new MutableRevisionRecord based on the given associative array following * the MW1.29 convention for the Revision constructor. diff --git a/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php index 7d301a9c44..c0ff44b0b8 100644 --- a/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php @@ -197,4 +197,46 @@ class McrRevisionStoreDbTest extends RevisionStoreDbTestBase { return [ 'slot_revision_id' => $revId ]; } + /** + * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch + * @throws \MWException + */ + public function testNewRevisionsFromBatch_error() { + $page = $this->getTestPage(); + $text = __METHOD__ . 'b-ä'; + /** @var Revision $rev1 */ + $rev1 = $page->doEditContent( + new WikitextContent( $text . '1' ), + __METHOD__ . 'b', + 0, + false, + $this->getTestUser()->getUser() + )->value['revision']; + $invalidRow = $this->revisionToRow( $rev1 ); + $invalidRow->rev_id = 100500; + $result = MediaWikiServices::getInstance()->getRevisionStore() + ->newRevisionsFromBatch( + [ $this->revisionToRow( $rev1 ), $invalidRow ], + [ + 'slots' => [ SlotRecord::MAIN ], + 'content' => true + ] + ); + $this->assertFalse( $result->isGood() ); + $this->assertNotEmpty( $result->getErrors() ); + $records = $result->getValue(); + $this->assertRevisionRecordMatchesRevision( $rev1, $records[$rev1->getId()] ); + $this->assertSame( $text . '1', + $records[$rev1->getId()]->getContent( SlotRecord::MAIN )->serialize() ); + $this->assertEquals( $page->getTitle()->getDBkey(), + $records[$rev1->getId()]->getPageAsLinkTarget()->getDBkey() ); + $this->assertNull( $records[$invalidRow->rev_id] ); + $this->assertSame( [ [ + 'type' => 'warning', + 'message' => 'internalerror', + 'params' => [ + "Couldn't find slots for rev 100500" + ] + ] ], $result->getErrors() ); + } } diff --git a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php index 55bfab7d81..b0b9ddf313 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php +++ b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php @@ -102,23 +102,24 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { } /** + * @param string|null $pageTitle whether to force-create a new page * @return WikiPage */ - protected function getTestPage() { - if ( $this->testPage ) { + protected function getTestPage( $pageTitle = null ) { + if ( !is_null( $pageTitle ) && $this->testPage ) { return $this->testPage; } - $title = $this->getTestPageTitle(); - $this->testPage = WikiPage::factory( $title ); + $title = is_null( $pageTitle ) ? $this->getTestPageTitle() : Title::newFromText( $pageTitle ); + $page = WikiPage::factory( $title ); - if ( !$this->testPage->exists() ) { + if ( !$page->exists() ) { // Make sure we don't write to the live db. $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) ); $user = static::getTestSysop()->getUser(); - $this->testPage->doEditContent( + $page->doEditContent( new WikitextContent( 'UTContent-' . __CLASS__ ), 'UTPageSummary-' . __CLASS__, EDIT_NEW | EDIT_SUPPRESS_RC, @@ -127,7 +128,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { ); } - return $this->testPage; + if ( is_null( $pageTitle ) ) { + $this->testPage = $page; + } + return $page; } /** @@ -1959,4 +1963,96 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { $this->assertSame( RevisionRecord::DELETED_TEXT, $deletedAfter ); } + public function provideNewRevisionsFromBatchOptions() { + yield 'No preload slots or content, single page' => [ + null, + [] + ]; + yield 'Preload slots and content, single page' => [ + null, + [ + 'slots' => [ SlotRecord::MAIN ], + 'content' => true + ] + ]; + yield 'No preload slots or content, multiple pages' => [ + 'Other_Page', + [] + ]; + yield 'Preload slots and content, multiple pages' => [ + 'Other_Page', + [ + 'slots' => [ SlotRecord::MAIN ], + 'content' => true + ] + ]; + } + + /** + * @dataProvider provideNewRevisionsFromBatchOptions + * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch + * @param string|null $otherPageTitle + * @param array|null $options + * @throws \MWException + */ + public function testNewRevisionsFromBatch_preloadContent( + $otherPageTitle = null, + array $options = [] + ) { + $page1 = $this->getTestPage(); + $text = __METHOD__ . 'b-ä'; + /** @var Revision $rev1 */ + $rev1 = $page1->doEditContent( + new WikitextContent( $text . '1' ), + __METHOD__ . 'b', + 0, + false, + $this->getTestUser()->getUser() + )->value['revision']; + $page2 = $this->getTestPage( $otherPageTitle ); + /** @var Revision $rev2 */ + $rev2 = $page2->doEditContent( + new WikitextContent( $text . '2' ), + __METHOD__ . 'b', + 0, + false, + $this->getTestUser()->getUser() + )->value['revision']; + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->newRevisionsFromBatch( + [ $this->revisionToRow( $rev1 ), $this->revisionToRow( $rev2 ) ], + $options + ); + $this->assertTrue( $result->isGood() ); + $this->assertEmpty( $result->getErrors() ); + $records = $result->getValue(); + $this->assertRevisionRecordMatchesRevision( $rev1, $records[$rev1->getId()] ); + $this->assertRevisionRecordMatchesRevision( $rev2, $records[$rev2->getId()] ); + + $this->assertSame( $text . '1', + $records[$rev1->getId()]->getContent( SlotRecord::MAIN )->serialize() ); + $this->assertSame( $text . '2', + $records[$rev2->getId()]->getContent( SlotRecord::MAIN )->serialize() ); + $this->assertEquals( $page1->getTitle()->getDBkey(), + $records[$rev1->getId()]->getPageAsLinkTarget()->getDBkey() ); + $this->assertEquals( $page2->getTitle()->getDBkey(), + $records[$rev2->getId()]->getPageAsLinkTarget()->getDBkey() ); + } + + /** + * @covers \MediaWiki\Revision\RevisionStore::newRevisionsFromBatch + */ + public function testNewRevisionsFromBatch_emptyBatch() { + $result = MediaWikiServices::getInstance()->getRevisionStore() + ->newRevisionsFromBatch( + [], + [ + 'slots' => [ SlotRecord::MAIN ], + 'content' => true + ] + ); + $this->assertTrue( $result->isGood() ); + $this->assertEmpty( $result->getValue() ); + $this->assertEmpty( $result->getErrors() ); + } } -- 2.20.1