use RecentChange;
use Revision;
use RuntimeException;
+use StatusValue;
use stdClass;
use Title;
+use Traversable;
use User;
use WANObjectCache;
use Wikimedia\Assert\Assert;
$canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
- $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
// Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
if ( $canUseTitleNewFromId ) {
+ $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
// TODO: better foreign title handling (introduce TitleFactory)
$title = Title::newFromID( $pageId, $titleFlags );
if ( $title ) {
* @param object[]|IResultWrapper $slotRows
* @param int $queryFlags
* @param Title $title
+ * @param array|null $slotContents a map from blobAddress to slot
+ * content blob or Content object.
*
* @return SlotRecord[]
*/
- private function constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title ) {
+ private function constructSlotRecords(
+ $revId,
+ $slotRows,
+ $queryFlags,
+ Title $title,
+ $slotContents = null
+ ) {
$slots = [];
foreach ( $slotRows as $row ) {
= $this->emulateContentId( intval( $row->rev_text_id ) );
}
- $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags ) {
- return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
+ $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
+ $blob = null;
+ if ( isset( $slotContents[$slot->getAddress()] ) ) {
+ $blob = $slotContents[$slot->getAddress()];
+ if ( $blob instanceof Content ) {
+ return $blob;
+ }
+ }
+ return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
};
$slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
/**
* @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 null|object[]|RevisionSlots $slots
+ * - Database rows generated from a query based on getSlotsQueryInfo
+ * with the 'content' flag set. Or
+ * - RevisionSlots instance
* @param int $queryFlags
* @param Title|null $title
* @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale
* @see RevisionFactory::newRevisionFromRow
*
* MCR migration note: this replaces Revision::newFromRow
- *
*/
public function newRevisionFromRowAndSlots(
$row,
- $slotRows,
+ $slots,
$queryFlags = 0,
Title $title = null,
$fromCache = false
// 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, $slotRows, $queryFlags, $title );
+ if ( !( $slots instanceof RevisionSlots ) ) {
+ $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
+ }
// If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
if ( $fromCache ) {
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.
+ * @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'] );
+ }
+
+ // We need to set the `content` flag because newRevisionFromRowAndSlots requires content
+ // metadata to be loaded.
+ $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+ $slotRows = $db->select(
+ $slotQueryInfo['tables'],
+ $slotQueryInfo['fields'],
+ $slotQueryConds,
+ __METHOD__,
+ [],
+ $slotQueryInfo['joins']
+ );
+
+ $slotRowsByRevId = [];
+ foreach ( $slotRows as $slotRow ) {
+ $slotRowsByRevId[$slotRow->slot_revision_id][] = $slotRow;
+ }
+
+ $slotContents = null;
+ if ( $options['content'] ?? false ) {
+ $blobAddresses = [];
+ foreach ( $slotRows as $slotRow ) {
+ $blobAddresses[] = $slotRow->content_address;
+ }
+ $slotContentFetchStatus = $this->blobStore
+ ->getBlobBatch( $blobAddresses, $queryFlags );
+ foreach ( $slotContentFetchStatus->getErrors() as $error ) {
+ $result->warning( $error['message'], ...$error['params'] );
+ }
+ $slotContents = $slotContentFetchStatus->getValue();
+ }
+
+ $result->setResult( true, array_map( function ( $row ) use
+ ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $slotContents, $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,
+ new RevisionSlots(
+ $this->constructSlotRecords(
+ $row->rev_id,
+ $slotRowsByRevId[$row->rev_id],
+ $queryFlags,
+ $titlesByPageId[$row->rev_page],
+ $slotContents
+ )
+ ),
+ $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.