use Wikimedia\Rdbms\DBConnRef;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\Rdbms\ResultWrapper;
/**
* Service for looking up page revisions.
/**
* @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 );
$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 );
}
/**
- * 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
*
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 ] );
// 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 );
} );
}
// 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 );
}
*
* 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
$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' );
// 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 ) {
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';
}
* @defgroup Dump Dump
*/
+use MediaWiki\MediaWikiServices as MediaWikiServicesAlias;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
);
}
- $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 ) {
# 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'] );
}
/**
- * 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 ) {
$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
*
*/
public function openPage( $row ) {
$out = " <page>\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";
* 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 = " <revision>\n";
$out .= " " . Xml::element( 'id', null, strval( $row->rev_id ) ) . "\n";
if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
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();
} 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.
];
}
+ /**
+ * 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 ];
+ }
+
}
$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 ];
+ }
+
}
$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 ];
+ }
+
}
];
}
+ /**
+ * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given
+ * revision.
+ *
+ * @return array
+ */
+ protected function getSlotRevisionConditions( $revId ) {
+ return [ 'rev_id' => $revId ];
+ }
+
}
];
}
+ /**
+ * Conditions to use together with getSlotsQueryInfo() when selecting slot rows for a given
+ * revision.
+ *
+ * @return array
+ */
+ protected function getSlotRevisionConditions( $revId ) {
+ return [ 'rev_id' => $revId ];
+ }
+
}
[],
[
'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' ],
[
'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' => [],
[ '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' => [],
[],
[
'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' ],
[
'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' => [],
}
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() {
/**
* @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots
*/
public function testNewRevisionFromRow_anonEdit() {
$page = $this->getTestPage();
/**
* @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots
*/
public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
$this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
/**
* @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots
*/
public function testNewRevisionFromRow_userEdit() {
$page = $this->getTestPage();
/**
* @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRowAndSlots
*/
public function testNewRevisionFromRow_no_user() {
$store = MediaWikiServices::getInstance()->getRevisionStore();