+ /**
+ * Gets the slot rows associated with a batch of revisions.
+ * The serialized content of each slot can be included by setting the 'blobs' option.
+ * Callers are responsible for unserializing and interpreting the content blobs
+ * based on the model_name and role_name fields.
+ *
+ * @param Traversable|array $rowsOrIds list of revision ids, or revision rows from a db query.
+ * @param array $options Supports the following options:
+ * 'slots' - a list of slot role names to fetch. If omitted or true or null,
+ * all slots are fetched
+ * 'blobs'- whether the serialized content of each slot should be loaded.
+ * If true, the serialiezd content will be present in the slot row
+ * in the blob_data field.
+ * @param int $queryFlags
+ *
+ * @return StatusValue a status containing, if isOK() returns true, a two-level nested
+ * associative array, mapping from revision ID to an associative array that maps from
+ * role name to a database row object. The database row object will contain the fields
+ * defined by getSlotQueryInfo() with the 'content' flag set, plus the blob_data field
+ * if the 'blobs' is set in $options. The model_name and role_name fields will also be
+ * set.
+ */
+ private function getSlotRowsForBatch(
+ $rowsOrIds,
+ array $options = [],
+ $queryFlags = 0
+ ) {
+ $readNew = $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW );
+ $result = new StatusValue();
+
+ $revIds = [];
+ foreach ( $rowsOrIds as $row ) {
+ $revIds[] = is_object( $row ) ? (int)$row->rev_id : (int)$row;
+ }
+
+ // Nothing to do.
+ // Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
+ if ( empty( $revIds ) ) {
+ $result->setResult( true, [] );
+ return $result;
+ }
+
+ // We need to set the `content` flag to join in content meta-data
+ $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
+ $revIdField = $slotQueryInfo['keys']['rev_id'];
+ $slotQueryConds = [ $revIdField => $revIds ];
+
+ if ( $readNew && isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
+ if ( empty( $options['slots'] ) ) {
+ // Degenerate case: return no slots for each revision.
+ $result->setResult( true, array_fill_keys( $revIds, [] ) );
+ return $result;
+ }
+
+ $roleIdField = $slotQueryInfo['keys']['role_id'];
+ $slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
+ return $this->slotRoleStore->getId( $slot_name );
+ }, $options['slots'] );
+ }
+
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+ $slotRows = $db->select(
+ $slotQueryInfo['tables'],
+ $slotQueryInfo['fields'],
+ $slotQueryConds,
+ __METHOD__,
+ [],
+ $slotQueryInfo['joins']
+ );
+
+ $slotContents = null;
+ if ( $options['blobs'] ?? 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();
+ }
+
+ $slotRowsByRevId = [];
+ foreach ( $slotRows as $slotRow ) {
+ if ( $slotContents === null ) {
+ // nothing to do
+ } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
+ $slotRow->blob_data = $slotContents[$slotRow->content_address];
+ } else {
+ $result->warning(
+ 'internalerror',
+ "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
+ );
+ $slotRow->blob_data = null;
+ }
+
+ // conditional needed for SCHEMA_COMPAT_READ_OLD
+ if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
+ $slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
+ }
+
+ // conditional needed for SCHEMA_COMPAT_READ_OLD
+ if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
+ $slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
+ }
+
+ $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
+ }
+
+ $result->setResult( true, $slotRowsByRevId );
+ return $result;
+ }
+
+ /**
+ * Gets raw (serialized) content blobs for the given set of revisions.
+ * Callers are responsible for unserializing and interpreting the content blobs
+ * based on the model_name field and the slot role.
+ *
+ * This method is intended for bulk operations in maintenance scripts.
+ * It may be chosen over newRevisionsFromBatch by code that are only interested
+ * in raw content, as opposed to meta data. Code that needs to access meta data of revisions,
+ * slots, or content objects should use newRevisionsFromBatch() instead.
+ *
+ * @param Traversable|array $rowsOrIds list of revision ids, or revision rows from a db query.
+ * @param array|null $slots the role names for which to get slots.
+ * @param int $queryFlags
+ *
+ * @return StatusValue a status containing, if isOK() returns true, a two-level nested
+ * associative array, mapping from revision ID to an associative array that maps from
+ * role name to an anonymous object object containing two fields:
+ * - model_name: the name of the content's model
+ * - blob_data: serialized content data
+ */
+ public function getContentBlobsForBatch(
+ $rowsOrIds,
+ $slots = null,
+ $queryFlags = 0
+ ) {
+ $result = $this->getSlotRowsForBatch(
+ $rowsOrIds,
+ [ 'slots' => $slots, 'blobs' => true ],
+ $queryFlags
+ );
+
+ if ( $result->isOK() ) {
+ // strip out all internal meta data that we don't want to expose
+ foreach ( $result->value as $revId => $rowsByRole ) {
+ foreach ( $rowsByRole as $role => $slotRow ) {
+ if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
+ // In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
+ // if we didn't ask for it.
+ unset( $result->value[$revId][$role] );
+ continue;
+ }
+
+ $result->value[$revId][$role] = (object)[
+ 'blob_data' => $slotRow->blob_data,
+ 'model_name' => $slotRow->model_name,
+ ];
+ }
+ }
+ }
+
+ return $result;
+ }
+