From dd14601afbf1cae379dda770eda74b7a4f652d15 Mon Sep 17 00:00:00 2001 From: daniel Date: Sun, 19 May 2019 10:48:10 +0200 Subject: [PATCH] Join slot and content tables when dumping XML This introduces a way to construct a RevisionRecord based on a known set of SlotRecords. To allow this to be used consistently with the legacy revision schema, some tweaks had to be made to getSlotsQueryInfo(). Bug: T220493 Change-Id: I5ea972bb07ca1cfb3a2ad8ef120aef77e460745c --- includes/Revision/RevisionStore.php | 108 +++++++++++++++--- includes/export/WikiExporter.php | 97 ++++++++++++---- includes/export/XmlDumpWriter.php | 17 ++- .../McrReadNewRevisionStoreDbTest.php | 10 ++ .../Revision/McrRevisionStoreDbTest.php | 10 ++ .../McrWriteBothRevisionStoreDbTest.php | 10 ++ .../NoContentModelRevisionStoreDbTest.php | 10 ++ .../Revision/PreMcrRevisionStoreDbTest.php | 10 ++ .../Revision/RevisionQueryInfoTest.php | 57 ++++----- .../Revision/RevisionStoreDbTestBase.php | 65 ++++++++++- 10 files changed, 323 insertions(+), 71 deletions(-) diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index faa162a1d6..56867eb6ff 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -63,6 +63,7 @@ use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\DBConnRef; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\ILoadBalancer; +use Wikimedia\Rdbms\ResultWrapper; /** * Service for looking up page revisions. @@ -1606,10 +1607,11 @@ class RevisionStore /** * @param int $revId The revision to load slots for. * @param int $queryFlags + * @param Title $title * * @return SlotRecord[] */ - private function loadSlotRecords( $revId, $queryFlags ) { + private function loadSlotRecords( $revId, $queryFlags, Title $title ) { $revQuery = self::getSlotsQueryInfo( [ 'content' ] ); list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags ); @@ -1626,12 +1628,45 @@ class RevisionStore $revQuery['joins'] ); + $slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title ); + + return $slots; + } + + /** + * Factory method for SlotRecords based on known slot rows. + * + * @param int $revId The revision to load slots for. + * @param object[]|ResultWrapper $slotRows + * @param int $queryFlags + * @param Title $title + * + * @return SlotRecord[] + */ + private function constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title ) { $slots = []; - foreach ( $res as $row ) { - // resolve role names and model names from in-memory cache, instead of joining. - $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id ); - $row->model_name = $this->contentModelStore->getName( (int)$row->content_model ); + foreach ( $slotRows as $row ) { + // Resolve role names and model names from in-memory cache, if they were not joined in. + if ( !isset( $row->role_name ) ) { + $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id ); + } + + if ( !isset( $row->model_name ) ) { + if ( isset( $row->content_model ) ) { + $row->model_name = $this->contentModelStore->getName( (int)$row->content_model ); + } else { + // We may get here if $row->model_name is set but null, perhaps because it + // came from rev_content_model, which is NULL for the default model. + $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name ); + $row->model_name = $slotRoleHandler->getDefaultModel( $title ); + } + } + + if ( !isset( $row->content_id ) && isset( $row->rev_text_id ) ) { + $row->slot_content_id + = $this->emulateContentId( intval( $row->rev_text_id ) ); + } $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags ) { return $this->loadSlotContent( $slot, null, null, null, $queryFlags ); @@ -1650,13 +1685,14 @@ class RevisionStore } /** - * Factory method for RevisionSlots. + * Factory method for RevisionSlots based on a revision ID. * * @note If other code has a need to construct RevisionSlots objects, this should be made * public, since RevisionSlots instances should not be constructed directly. * * @param int $revId * @param object $revisionRow + * @param object[]|null $slotRows * @param int $queryFlags * @param Title $title * @@ -1666,10 +1702,15 @@ class RevisionStore private function newRevisionSlots( $revId, $revisionRow, + $slotRows, $queryFlags, Title $title ) { - if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) { + if ( $slotRows ) { + $slots = new RevisionSlots( + $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title ) + ); + } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) { $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title ); // @phan-suppress-next-line PhanTypeInvalidCallableArraySize false positive $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] ); @@ -1677,8 +1718,8 @@ class RevisionStore // XXX: do we need the same kind of caching here // that getKnownCurrentRevision uses (if $revId == page_latest?) - $slots = new RevisionSlots( function () use( $revId, $queryFlags ) { - return $this->loadSlotRecords( $revId, $queryFlags ); + $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) { + return $this->loadSlotRecords( $revId, $queryFlags, $title ); } ); } @@ -1752,7 +1793,7 @@ class RevisionStore // Legacy because $row may have come from self::selectFields() $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true ); - $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title ); + $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, null, $queryFlags, $title ); return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId ); } @@ -1762,7 +1803,7 @@ class RevisionStore * * MCR migration note: this replaces Revision::newFromRow * - * @param object $row + * @param object $row A database row generated from a query based on getQueryInfo() * @param int $queryFlags * @param Title|null $title * @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale @@ -1774,6 +1815,32 @@ class RevisionStore $queryFlags = 0, Title $title = null, $fromCache = false + ) { + return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache ); + } + + /** + * @param object $row A database row generated from a query based on getQueryInfo() + * @param null|object[] $slotRows Database rows generated from a query based on + * getSlotsQueryInfo with the 'content' flag set. + * @param int $queryFlags + * @param Title|null $title + * @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale + * data is returned from getters, by querying the database as needed + * + * @return RevisionRecord + * @throws MWException + * @see RevisionFactory::newRevisionFromRow + * + * MCR migration note: this replaces Revision::newFromRow + * + */ + public function newRevisionFromRowAndSlots( + $row, + $slotRows, + $queryFlags = 0, + Title $title = null, + $fromCache = false ) { Assert::parameterType( 'object', $row, '$row' ); @@ -1807,7 +1874,7 @@ class RevisionStore // Legacy because $row may have come from self::selectFields() $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true ); - $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title ); + $slots = $this->newRevisionSlots( $row->rev_id, $row, $slotRows, $queryFlags, $title ); // If this is a cached row, instantiate a cache-aware revision class to avoid stale data. if ( $fromCache ) { @@ -2405,21 +2472,24 @@ class RevisionStore if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) { $db = $this->getDBConnectionRef( DB_REPLICA ); - $ret['tables']['slots'] = 'revision'; + $ret['tables'][] = 'revision'; - $ret['fields']['slot_revision_id'] = 'slots.rev_id'; + $ret['fields']['slot_revision_id'] = 'rev_id'; $ret['fields']['slot_content_id'] = 'NULL'; - $ret['fields']['slot_origin'] = 'slots.rev_id'; + $ret['fields']['slot_origin'] = 'rev_id'; $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN ); if ( in_array( 'content', $options, true ) ) { - $ret['fields']['content_size'] = 'slots.rev_len'; - $ret['fields']['content_sha1'] = 'slots.rev_sha1'; + $ret['fields']['content_size'] = 'rev_len'; + $ret['fields']['content_sha1'] = 'rev_sha1'; $ret['fields']['content_address'] - = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ); + = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'rev_text_id' ] ); + + // Allow the content_id field to be emulated later + $ret['fields']['rev_text_id'] = 'rev_text_id'; if ( $this->contentHandlerUseDB ) { - $ret['fields']['model_name'] = 'slots.rev_content_model'; + $ret['fields']['model_name'] = 'rev_content_model'; } else { $ret['fields']['model_name'] = 'NULL'; } diff --git a/includes/export/WikiExporter.php b/includes/export/WikiExporter.php index fb1053c249..0b0c8014b9 100644 --- a/includes/export/WikiExporter.php +++ b/includes/export/WikiExporter.php @@ -27,6 +27,7 @@ * @defgroup Dump Dump */ +use MediaWiki\MediaWikiServices as MediaWikiServicesAlias; use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\Rdbms\IDatabase; @@ -339,18 +340,28 @@ class WikiExporter { ); } - $revOpts = [ 'page' ]; - - $revQuery = Revision::getQueryInfo( $revOpts ); + $revQuery = MediaWikiServicesAlias::getInstance()->getRevisionStore()->getQueryInfo( + [ 'page' ] + ); + $slotQuery = MediaWikiServicesAlias::getInstance()->getRevisionStore()->getSlotsQueryInfo( + [ 'content' ] + ); - // We want page primary rather than revision + // We want page primary rather than revision. + // We also want to join in the slots and content tables. + // NOTE: This means we may get multiple rows per revision, and more rows + // than the batch size! Should be ok, since the max number of slots is + // fixed and low (dozens at worst). $tables = array_merge( [ 'page' ], array_diff( $revQuery['tables'], [ 'page' ] ) ); + $tables = array_merge( $tables, array_diff( $slotQuery['tables'], $tables ) ); $join = $revQuery['joins'] + [ - 'revision' => $revQuery['joins']['page'] + 'revision' => $revQuery['joins']['page'], + 'slots' => [ 'JOIN', [ 'slot_revision_id = rev_id' ] ], + 'content' => [ 'JOIN', [ 'content_id = slot_content_id' ] ], ]; unset( $join['page'] ); - $fields = $revQuery['fields']; + $fields = array_merge( $revQuery['fields'], $slotQuery['fields'] ); $fields[] = 'page_restrictions'; if ( $this->text != self::STUB ) { @@ -387,7 +398,6 @@ class WikiExporter { # Full history dumps... # query optimization for history stub dumps if ( $this->text == self::STUB ) { - $tables = $revQuery['tables']; $opts[] = 'STRAIGHT_JOIN'; $opts['USE INDEX']['revision'] = 'rev_page_id'; unset( $join['revision'] ); @@ -464,24 +474,36 @@ class WikiExporter { } /** - * Runs through a query result set dumping page and revision records. - * The result set should be sorted/grouped by page to avoid duplicate - * page records in the output. + * Runs through a query result set dumping page, revision, and slot records. + * The result set should join the page, revision, slots, and content tables, + * and be sorted/grouped by page and revision to avoid duplicate page records in the output. * * @param IResultWrapper $results * @param object $lastRow the last row output from the previous call (or null if none) * @return object the last row processed */ protected function outputPageStreamBatch( $results, $lastRow ) { - foreach ( $results as $row ) { + $rowCarry = null; + while ( true ) { + $slotRows = $this->getSlotRowBatch( $results, $rowCarry ); + + if ( !$slotRows ) { + break; + } + + // All revision info is present in all slot rows. + // Use the first slot row as the revision row. + $revRow = $slotRows[0]; + if ( $this->limitNamespaces && - !in_array( $row->page_namespace, $this->limitNamespaces ) ) { - $lastRow = $row; + !in_array( $revRow->page_namespace, $this->limitNamespaces ) ) { + $lastRow = $revRow; continue; } + if ( $lastRow === null || - $lastRow->page_namespace !== $row->page_namespace || - $lastRow->page_title !== $row->page_title ) { + $lastRow->page_namespace !== $revRow->page_namespace || + $lastRow->page_title !== $revRow->page_title ) { if ( $lastRow !== null ) { $output = ''; if ( $this->dumpUploads ) { @@ -490,17 +512,52 @@ class WikiExporter { $output .= $this->writer->closePage(); $this->sink->writeClosePage( $output ); } - $output = $this->writer->openPage( $row ); - $this->sink->writeOpenPage( $row, $output ); + $output = $this->writer->openPage( $revRow ); + $this->sink->writeOpenPage( $revRow, $output ); } - $output = $this->writer->writeRevision( $row ); - $this->sink->writeRevision( $row, $output ); - $lastRow = $row; + $output = $this->writer->writeRevision( $revRow, $slotRows ); + $this->sink->writeRevision( $revRow, $output ); + $lastRow = $revRow; + } + + if ( $rowCarry ) { + throw new LogicException( 'Error while processing a stream of slot rows' ); } return $lastRow; } + /** + * Returns all slot rows for a revision. + * Takes and returns a carry row from the last batch; + * + * @param IResultWrapper|array $results + * @param null|object &$carry A row carried over from the last call to getSlotRowBatch() + * + * @return object[] + */ + protected function getSlotRowBatch( $results, &$carry = null ) { + $slotRows = []; + $prev = null; + + if ( $carry ) { + $slotRows[] = $carry; + $prev = $carry; + $carry = null; + } + + while ( $row = $results->fetchObject() ) { + if ( $prev && $prev->rev_id !== $row->rev_id ) { + $carry = $row; + break; + } + $slotRows[] = $row; + $prev = $row; + } + + return $slotRows; + } + /** * Final page stream output, after all batches are complete * diff --git a/includes/export/XmlDumpWriter.php b/includes/export/XmlDumpWriter.php index 0659ec18cb..d1b993d99e 100644 --- a/includes/export/XmlDumpWriter.php +++ b/includes/export/XmlDumpWriter.php @@ -177,7 +177,7 @@ class XmlDumpWriter { */ public function openPage( $row ) { $out = " \n"; - $this->currentTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); + $this->currentTitle = Title::newFromRow( $row ); $canonicalTitle = self::canonicalTitle( $this->currentTitle ); $out .= ' ' . Xml::elementClean( 'title', [], $canonicalTitle ) . "\n"; $out .= ' ' . Xml::element( 'ns', [], strval( $row->page_namespace ) ) . "\n"; @@ -237,10 +237,21 @@ class XmlDumpWriter { * data filled in from the given database row. * * @param object $row + * @param null|object[] $slotRows + * * @return string + * @throws FatalError + * @throws MWException * @private */ - function writeRevision( $row ) { + function writeRevision( $row, $slotRows = null ) { + $rev = $this->getRevisionStore()->newRevisionFromRowAndSlots( + $row, + $slotRows, + 0, + $this->currentTitle + ); + $out = " \n"; $out .= " " . Xml::element( 'id', null, strval( $row->rev_id ) ) . "\n"; if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) { @@ -311,7 +322,6 @@ class XmlDumpWriter { strval( $text ) ) . "\n"; } elseif ( isset( $row->_load_content ) ) { // TODO: make this fully MCR aware, see T174031 - $rev = $this->getRevisionStore()->newRevisionFromRow( $row, 0, $this->currentTitle ); $slot = $rev->getSlot( 'main' ); try { $content = $slot->getContent(); @@ -350,7 +360,6 @@ class XmlDumpWriter { } else { // Backwards-compatible stub output for MCR aware schema // TODO: MCR: emit content addresses instead of text ids, see T174031, T199121 - $rev = $this->getRevisionStore()->newRevisionFromRow( $row, 0, $this->currentTitle ); $slot = $rev->getSlot( 'main' ); // Note that this is currently the ONLY reason we have a BlobStore here at all. diff --git a/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php index fed47f061f..43a698a869 100644 --- a/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php @@ -144,4 +144,14 @@ class McrReadNewRevisionStoreDbTest extends RevisionStoreDbTestBase { ]; } + /** + * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given + * revision. + * + * @return array + */ + protected function getSlotRevisionConditions( $revId ) { + return [ 'slot_revision_id' => $revId ]; + } + } diff --git a/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php index 0aa220c5da..7d301a9c44 100644 --- a/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php @@ -187,4 +187,14 @@ class McrRevisionStoreDbTest extends RevisionStoreDbTestBase { $this->assertRevisionRecordsEqual( $return, $loaded ); } + /** + * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given + * revision. + * + * @return array + */ + protected function getSlotRevisionConditions( $revId ) { + return [ 'slot_revision_id' => $revId ]; + } + } diff --git a/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php index 856c343371..8c0960bd38 100644 --- a/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php @@ -183,4 +183,14 @@ class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase { $this->assertRevisionExistsInDatabase( $return ); } + /** + * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given + * revision. + * + * @return array + */ + protected function getSlotRevisionConditions( $revId ) { + return [ 'rev_id' => $revId ]; + } + } diff --git a/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php index 1250a6b034..51cfc63763 100644 --- a/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php @@ -189,4 +189,14 @@ class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase { ]; } + /** + * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given + * revision. + * + * @return array + */ + protected function getSlotRevisionConditions( $revId ) { + return [ 'rev_id' => $revId ]; + } + } diff --git a/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php index 011c79e22f..468ab60800 100644 --- a/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php @@ -89,4 +89,14 @@ class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase { ]; } + /** + * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given + * revision. + * + * @return array + */ + protected function getSlotRevisionConditions( $revId ) { + return [ 'rev_id' => $revId ]; + } + } diff --git a/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php b/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php index 35bc917ba6..57619c55b6 100644 --- a/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php +++ b/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php @@ -731,13 +731,13 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [], [ 'tables' => [ - 'slots' => 'revision', + 'revision', ], 'fields' => array_merge( [ - 'slot_revision_id' => 'slots.rev_id', + 'slot_revision_id' => 'rev_id', 'slot_content_id' => 'NULL', - 'slot_origin' => 'slots.rev_id', + 'slot_origin' => 'rev_id', 'role_name' => $db->addQuotes( SlotRecord::MAIN ), ] ), @@ -752,19 +752,20 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'content' ], [ 'tables' => [ - 'slots' => 'revision', + 'revision', ], 'fields' => array_merge( [ - 'slot_revision_id' => 'slots.rev_id', + 'slot_revision_id' => 'rev_id', 'slot_content_id' => 'NULL', - 'slot_origin' => 'slots.rev_id', + 'slot_origin' => 'rev_id', 'role_name' => $db->addQuotes( SlotRecord::MAIN ), - 'content_size' => 'slots.rev_len', - 'content_sha1' => 'slots.rev_sha1', + 'content_size' => 'rev_len', + 'content_sha1' => 'rev_sha1', 'content_address' => $db->buildConcat( [ - $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ), - 'model_name' => 'slots.rev_content_model', + $db->addQuotes( 'tt:' ), 'rev_text_id' ] ), + 'rev_text_id' => 'rev_text_id', + 'model_name' => 'rev_content_model', ] ), 'joins' => [], @@ -778,19 +779,20 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'content', 'model', 'role' ], [ 'tables' => [ - 'slots' => 'revision', + 'revision', ], 'fields' => array_merge( [ - 'slot_revision_id' => 'slots.rev_id', + 'slot_revision_id' => 'rev_id', 'slot_content_id' => 'NULL', - 'slot_origin' => 'slots.rev_id', + 'slot_origin' => 'rev_id', 'role_name' => $db->addQuotes( SlotRecord::MAIN ), - 'content_size' => 'slots.rev_len', - 'content_sha1' => 'slots.rev_sha1', + 'content_size' => 'rev_len', + 'content_sha1' => 'rev_sha1', 'content_address' => $db->buildConcat( [ - $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ), - 'model_name' => 'slots.rev_content_model', + $db->addQuotes( 'tt:' ), 'rev_text_id' ] ), + 'rev_text_id' => 'rev_text_id', + 'model_name' => 'rev_content_model', ] ), 'joins' => [], @@ -804,13 +806,13 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [], [ 'tables' => [ - 'slots' => 'revision', + 'revision', ], 'fields' => array_merge( [ - 'slot_revision_id' => 'slots.rev_id', + 'slot_revision_id' => 'rev_id', 'slot_content_id' => 'NULL', - 'slot_origin' => 'slots.rev_id', + 'slot_origin' => 'rev_id', 'role_name' => $db->addQuotes( SlotRecord::MAIN ), ] ), @@ -825,19 +827,20 @@ class RevisionQueryInfoTest extends MediaWikiTestCase { [ 'content' ], [ 'tables' => [ - 'slots' => 'revision', + 'revision', ], 'fields' => array_merge( [ - 'slot_revision_id' => 'slots.rev_id', + 'slot_revision_id' => 'rev_id', 'slot_content_id' => 'NULL', - 'slot_origin' => 'slots.rev_id', + 'slot_origin' => 'rev_id', 'role_name' => $db->addQuotes( SlotRecord::MAIN ), - 'content_size' => 'slots.rev_len', - 'content_sha1' => 'slots.rev_sha1', + 'content_size' => 'rev_len', + 'content_sha1' => 'rev_sha1', 'content_address' => - $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ), - 'model_name' => 'slots.rev_content_model', + $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'rev_text_id' ] ), + 'rev_text_id' => 'rev_text_id', + 'model_name' => 'rev_content_model', ] ), 'joins' => [], diff --git a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php index 3467153a55..7b017ab67f 100644 --- a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php +++ b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php @@ -897,12 +897,71 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { } if ( $revMain->hasContentId() ) { - $this->assertSame( $revMain->getContentId(), $recMain->getContentId(), 'getContentId' ); + // XXX: the content ID value is ill-defined when SCHEMA_COMPAT_WRITE_BOTH and + // SCHEMA_COMPAT_READ_OLD is set, since revision insertion will report the + // content ID used with the new schema, while loading the revision from the + // old schema will report an emulated ID. + if ( $this->getMcrMigrationStage() & SCHEMA_COMPAT_READ_NEW ) { + $this->assertSame( $revMain->getContentId(), $recMain->getContentId(), 'getContentId' ); + } } } + /** + * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots + * @covers \MediaWiki\Revision\RevisionStore::getQueryInfo + */ + public function testNewRevisionFromRowAndSlot_getQueryInfo() { + $page = $this->getTestPage(); + $text = __METHOD__ . 'o-ö'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__ . 'a' + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $info = $store->getQueryInfo(); + $row = $this->db->selectRow( + $info['tables'], + $info['fields'], + [ 'rev_id' => $rev->getId() ], + __METHOD__, + [], + $info['joins'] + ); + + $info = $store->getSlotsQueryInfo( [ 'content' ] ); + $slotRows = $this->db->select( + $info['tables'], + $info['fields'], + $this->getSlotRevisionConditions( $rev->getId() ), + __METHOD__, + [], + $info['joins'] + ); + + $record = $store->newRevisionFromRowAndSlots( + $row, + iterator_to_array( $slotRows ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given + * revision. + * + * @return array + */ + abstract protected function getSlotRevisionConditions( $revId ); + /** * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots * @covers \MediaWiki\Revision\RevisionStore::getQueryInfo */ public function testNewRevisionFromRow_getQueryInfo() { @@ -935,6 +994,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { /** * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots */ public function testNewRevisionFromRow_anonEdit() { $page = $this->getTestPage(); @@ -957,6 +1017,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { /** * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots */ public function testNewRevisionFromRow_anonEdit_legacyEncoding() { $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); @@ -981,6 +1042,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { /** * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots */ public function testNewRevisionFromRow_userEdit() { $page = $this->getTestPage(); @@ -1105,6 +1167,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase { /** * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots */ public function testNewRevisionFromRow_no_user() { $store = MediaWikiServices::getInstance()->getRevisionStore(); -- 2.20.1