use Psr\Log\NullLogger;
use RecentChange;
use Revision;
+use RuntimeException;
use stdClass;
use Title;
use User;
$slotRoles = $rev->getSlotRoles();
// Make sure the main slot is always provided throughout migration
- if ( !in_array( 'main', $slotRoles ) ) {
+ if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
throw new InvalidArgumentException(
'main slot must be provided'
);
}
// If we are not writing into the new schema, we can't support extra slots.
- if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) && $slotRoles !== [ 'main' ] ) {
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
+ && $slotRoles !== [ SlotRecord::MAIN ]
+ ) {
throw new InvalidArgumentException(
'Only the main slot is supported when not writing to the MCR enabled schema!'
);
}
// As long as we are not reading from the new schema, we don't want to write extra slots.
- if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) && $slotRoles !== [ 'main' ] ) {
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
+ && $slotRoles !== [ SlotRecord::MAIN ]
+ ) {
throw new InvalidArgumentException(
'Only the main slot is supported when not reading from the MCR enabled schema!'
);
$this->failOnNull( $user->getId(), 'user field' );
$this->failOnEmpty( $user->getName(), 'user_text field' );
+ if ( !$rev->isReadyForInsertion() ) {
+ // This is here for future-proofing. At the time this check being added, it
+ // was redundant to the individual checks above.
+ throw new IncompleteRevisionException( 'Revision is incomplete' );
+ }
+
// TODO: we shouldn't need an actual Title here.
$title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
$pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
// Technically, this could go away after MCR migration: while
// calling code may require a main slot to exist, RevisionStore
// really should not know or care about that requirement.
- $rev->getSlot( 'main', RevisionRecord::RAW );
+ $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
foreach ( $slotRoles as $role ) {
$slot = $rev->getSlot( $role, RevisionRecord::RAW );
Assert::postcondition(
$slot->getContent() !== null,
- $role . ' slot must have content'
+ $role . ' slot must have content'
);
Assert::postcondition(
$slot->hasRevision(),
- $role . ' slot must have a revision associated'
+ $role . ' slot must have a revision associated'
);
}
foreach ( $slotRoles as $role ) {
$slot = $rev->getSlot( $role, RevisionRecord::RAW );
- if ( $slot->hasRevision() ) {
- // If the SlotRecord already has a revision ID set, this means it already exists
- // in the database, and should already belong to the current revision.
+ // If the SlotRecord already has a revision ID set, this means it already exists
+ // in the database, and should already belong to the current revision.
+ // However, a slot may already have a revision, but no content ID, if the slot
+ // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
+ // mode, and the respective archive row was not yet migrated to the new schema.
+ // In that case, a new slot row (and content row) must be inserted even during
+ // undeletion.
+ if ( $slot->hasRevision() && $slot->hasContentId() ) {
// TODO: properly abort transaction if the assertion fails!
Assert::parameter(
$slot->getRevision() === $revisionId,
$newSlots[$role] = $slot;
// Write the main slot's text ID to the revision table for backwards compatibility
- if ( $slot->getRole() === 'main'
+ if ( $slot->getRole() === SlotRecord::MAIN
&& $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
) {
$blobAddress = $slot->getAddress();
* @param IDatabase $dbw
* @param int $revisionId
* @param string &$blobAddress (may change!)
+ *
+ * @return int the text row id
*/
private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
$textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
[ 'rev_id' => $revisionId ],
__METHOD__
);
+
+ return $textId;
}
/**
$blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
}
+ $contentId = null;
+
// Write the main slot's text ID to the revision table for backwards compatibility
- if ( $protoSlot->getRole() === 'main'
+ if ( $protoSlot->getRole() === SlotRecord::MAIN
&& $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
) {
- $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+ // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
+ // with the real content ID below.
+ $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+ $contentId = $this->emulateContentId( $textId );
}
if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
}
$this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
- } else {
- $contentId = null;
}
$savedSlot = SlotRecord::newSaved(
if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
// In non MCR mode this IF section will relate to the main slot
- $mainSlot = $rev->getSlot( 'main' );
+ $mainSlot = $rev->getSlot( SlotRecord::MAIN );
$model = $mainSlot->getModel();
$format = $mainSlot->getFormat();
) {
$this->checkDatabaseWikiId( $dbw );
+ $pageId = $title->getArticleID();
+
// T51581: Lock the page table row to ensure no other process
// is adding a revision to the page at the same time.
// Avoid locking extra tables, compare T191892.
$pageLatest = $dbw->selectField(
'page',
'page_latest',
- [ 'page_id' => $title->getArticleID() ],
+ [ 'page_id' => $pageId ],
__METHOD__,
[ 'FOR UPDATE' ]
);
$title
);
+ if ( !$oldRevision ) {
+ $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
+ $this->logger->error(
+ $msg,
+ [ 'exception' => new RuntimeException( $msg ) ]
+ );
+ return null;
+ }
+
// Construct the new revision
$timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
$newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
*/
private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
$mainSlotRow = new stdClass();
- $mainSlotRow->role_name = 'main';
+ $mainSlotRow->role_name = SlotRecord::MAIN;
$mainSlotRow->model_name = null;
$mainSlotRow->slot_revision_id = null;
+ $mainSlotRow->slot_content_id = null;
$mainSlotRow->content_address = null;
$content = null;
$mainSlotRow->format_name = isset( $row->rev_content_format )
? strval( $row->rev_content_format )
: null;
+
+ if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
+ // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
+ $mainSlotRow->slot_content_id
+ = $this->emulateContentId( intval( $row->rev_text_id ) );
+ }
} elseif ( is_array( $row ) ) {
$mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
$mainSlotRow->format_name = $handler->getDefaultFormat();
}
}
+
+ if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
+ // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
+ $mainSlotRow->slot_content_id
+ = $this->emulateContentId( intval( $row['text_id'] ) );
+ }
} else {
throw new MWException( 'Revision constructor passed invalid row format.' );
}
};
}
- // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
- // the inherited slot to have the same content_id as the original slot. In that case,
- // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
- $mainSlotRow->slot_content_id =
- function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
- $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
- return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, 'main' );
- };
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+ // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
+ // the inherited slot to have the same content_id as the original slot. In that case,
+ // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
+ $mainSlotRow->slot_content_id =
+ function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+ return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
+ };
+ }
return new SlotRecord( $mainSlotRow, $content );
}
+ /**
+ * Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode,
+ * based on the revision's text ID (rev_text_id or ar_text_id, respectively).
+ * Note that in SCHEMA_COMPAT_WRITE_BOTH, a callback to findSlotContentId() should be used
+ * instead, since in that mode, some revision rows may already have a real content ID,
+ * while other's don't - and for the ones that don't, we should indicate that it
+ * is missing and cause SlotRecords::hasContentId() to return false.
+ *
+ * @param int $textId
+ * @return int The emulated content ID
+ */
+ private function emulateContentId( $textId ) {
+ // Return a negative number to ensure the ID is distinct from any real content IDs
+ // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
+ // mode.
+ return -$textId;
+ }
+
/**
* Loads a Content object based on a slot row.
*
$slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
}
- if ( !isset( $slots['main'] ) ) {
+ if ( !isset( $slots[SlotRecord::MAIN] ) ) {
throw new RevisionAccessException(
'Main slot of revision ' . $revId . ' not found in database!'
);
) {
if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
$mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
- $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+ $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
} else {
// XXX: do we need the same kind of caching here
// that getKnownCurrentRevision uses (if $revId == page_latest?)
$row->ar_actor ?? null
);
} catch ( InvalidArgumentException $ex ) {
- wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
- $user = new UserIdentityValue( 0, '', 0 );
+ wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
+ $user = new UserIdentityValue( 0, 'Unknown user', 0 );
}
$db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
$row->rev_actor ?? null
);
} catch ( InvalidArgumentException $ex ) {
- wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
- $user = new UserIdentityValue( 0, '', 0 );
+ wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
+ $user = new UserIdentityValue( 0, 'Unknown user', 0 );
}
$db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
$ret['fields']['slot_revision_id'] = 'slots.rev_id';
$ret['fields']['slot_content_id'] = 'NULL';
$ret['fields']['slot_origin'] = 'slots.rev_id';
- $ret['fields']['role_name'] = $db->addQuotes( 'main' );
+ $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
if ( in_array( 'content', $options, true ) ) {
$ret['fields']['content_size'] = 'slots.rev_len';