Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
Assert::parameterType( 'integer', $migrationStage, '$migrationStage' );
- if ( $migrationStage > MIGRATION_WRITE_BOTH ) {
- throw new InvalidArgumentException( 'New schema is not fully supported yet' );
- }
-
$this->loadBalancer = $loadBalancer;
$this->blobStore = $blobStore;
$this->cache = $cache;
// TODO: pass in a DBTransactionContext instead of a database connection.
$this->checkDatabaseWikiId( $dbw );
- if ( !$rev->getSlotRoles() ) {
- throw new InvalidArgumentException( 'At least one slot needs to be defined!' );
+ $slotRoles = $rev->getSlotRoles();
+
+ // Make sure the main slot is always provided throughout migration
+ if ( !in_array( 'main', $slotRoles ) ) {
+ throw new InvalidArgumentException(
+ 'main slot must be provided'
+ );
}
- // RevisionStore currently only supports writing a single slot
- if ( $rev->getSlotRoles() !== [ 'main' ] ) {
- throw new InvalidArgumentException( 'Only the main slot is supported for now!' );
+ // While inserting into the old schema make sure only the main slot is allowed.
+ // TODO: support extra slots in MIGRATION_WRITE_BOTH mode!
+ if ( $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH && $slotRoles !== [ 'main' ] ) {
+ throw new InvalidArgumentException(
+ 'Only the main slot is supported with MCR migration mode <= MIGRATION_WRITE_BOTH!'
+ );
}
+ // Checks
+ $this->failOnNull( $rev->getSize(), 'size field' );
+ $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
+ $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
+ $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
+ $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
+ $this->failOnNull( $user->getId(), 'user field' );
+ $this->failOnEmpty( $user->getName(), 'user_text field' );
+
// 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
? $this->getPreviousRevisionId( $dbw, $rev )
: $rev->getParentId();
- // Record the text (or external storage URL) to the blob store
- $mainSlot = $rev->getSlot( 'main', RevisionRecord::RAW );
+ /** @var RevisionRecord $rev */
+ $rev = $dbw->doAtomicSection(
+ __METHOD__,
+ function ( IDatabase $dbw, $fname ) use (
+ $rev,
+ $user,
+ $comment,
+ $title,
+ $pageId,
+ $parentId
+ ) {
+ return $this->insertRevisionInternal(
+ $rev,
+ $dbw,
+ $user,
+ $comment,
+ $title,
+ $pageId,
+ $parentId
+ );
+ }
+ );
- $size = $this->failOnNull( $rev->getSize(), 'size field' );
- $sha1 = $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
+ // sanity checks
+ Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
+ Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
+ Assert::postcondition(
+ $rev->getComment( RevisionRecord::RAW ) !== null,
+ 'revision must have a comment'
+ );
+ Assert::postcondition(
+ $rev->getUser( RevisionRecord::RAW ) !== null,
+ 'revision must have a user'
+ );
- $dbw->startAtomic( __METHOD__ );
+ // Trigger exception if the main slot is missing.
+ // Technically, this could go away with MIGRATION_NEW: 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 );
+
+ foreach ( $slotRoles as $role ) {
+ $slot = $rev->getSlot( $role, RevisionRecord::RAW );
+ Assert::postcondition(
+ $slot->getContent() !== null,
+ $role . ' slot must have content'
+ );
+ Assert::postcondition(
+ $slot->hasRevision(),
+ $role . ' slot must have a revision associated'
+ );
+ }
- if ( !$mainSlot->hasAddress() ) {
- $content = $mainSlot->getContent();
- $format = $content->getDefaultFormat();
- $model = $content->getModel();
+ Hooks::run( 'RevisionRecordInserted', [ $rev ] );
- $this->checkContentModel( $content, $title );
+ // TODO: deprecate in 1.32!
+ $legacyRevision = new Revision( $rev );
+ Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
- $data = $content->serialize( $format );
+ return $rev;
+ }
- // Hints allow the blob store to optimize by "leaking" application level information to it.
- // TODO: with the new MCR storage schema, we rev_id have this before storing the blobs.
- // When we have it, add rev_id as a hint. Can be used with rev_parent_id for
- // differential storage or compression of subsequent revisions.
- $blobHints = [
- BlobStore::DESIGNATION_HINT => 'page-content', // BlobStore may be used for other things too.
- BlobStore::PAGE_HINT => $pageId,
- BlobStore::ROLE_HINT => $mainSlot->getRole(),
- BlobStore::PARENT_HINT => $parentId,
- BlobStore::SHA1_HINT => $mainSlot->getSha1(),
- BlobStore::MODEL_HINT => $model,
- BlobStore::FORMAT_HINT => $format,
- ];
+ private function insertRevisionInternal(
+ RevisionRecord $rev,
+ IDatabase $dbw,
+ User $user,
+ CommentStoreComment $comment,
+ Title $title,
+ $pageId,
+ $parentId
+ ) {
+ $slotRoles = $rev->getSlotRoles();
- $blobAddress = $this->blobStore->storeBlob( $data, $blobHints );
- } else {
- $blobAddress = $mainSlot->getAddress();
- $model = $mainSlot->getModel();
- $format = $mainSlot->getFormat();
+ $revisionRow = $this->insertRevisionRowOn(
+ $dbw,
+ $rev,
+ $title,
+ $parentId
+ );
+
+ $revisionId = $revisionRow['rev_id'];
+
+ $blobHints = [
+ BlobStore::PAGE_HINT => $pageId,
+ BlobStore::REVISION_HINT => $revisionId,
+ BlobStore::PARENT_HINT => $parentId,
+ ];
+
+ $newSlots = [];
+ 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.
+ // TODO: properly abort transaction if the assertion fails!
+ Assert::parameter(
+ $slot->getRevision() === $revisionId,
+ 'slot role ' . $slot->getRole(),
+ 'Existing slot should belong to revision '
+ . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
+ );
+
+ // Slot exists, nothing to do, move along.
+ // This happens when restoring archived revisions.
+
+ $newSlots[$role] = $slot;
+
+ // Write the main slot's text ID to the revision table for backwards compatibility
+ if ( $slot->getRole() === 'main' && $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
+ $blobAddress = $slot->getAddress();
+ $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+ }
+ } else {
+ $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
+ }
}
- $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
+ $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
+ $rev = new RevisionStoreRecord(
+ $title,
+ $user,
+ $comment,
+ (object)$revisionRow,
+ new RevisionSlots( $newSlots ),
+ $this->wikiId
+ );
+
+ return $rev;
+ }
+
+ /**
+ * @param IDatabase $dbw
+ * @param int $revisionId
+ * @param string &$blobAddress (may change!)
+ */
+ private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
+ $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
if ( !$textId ) {
throw new LogicException(
'Blob address not supported in 1.29 database schema: ' . $blobAddress
// may be a new value, not anything already contained in $blobAddress.
$blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
- $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
- $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
- $timestamp = $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
-
- // Checks.
- $this->failOnNull( $user->getId(), 'user field' );
- $this->failOnEmpty( $user->getName(), 'user_text field' );
-
- // Record the edit in revisions
- $revisionRow = [
- 'rev_page' => $pageId,
- 'rev_parent_id' => $parentId,
- 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
- 'rev_timestamp' => $dbw->timestamp( $timestamp ),
- 'rev_deleted' => $rev->getVisibility(),
- 'rev_len' => $size,
- 'rev_sha1' => $sha1,
- ];
-
- if ( $rev->getId() !== null ) {
- // Needed to restore revisions with their original ID
- $revisionRow['rev_id'] = $rev->getId();
- }
-
- list( $commentFields, $commentCallback ) =
- $this->commentStore->insertWithTempTable( $dbw, 'rev_comment', $comment );
- $revisionRow += $commentFields;
-
- list( $actorFields, $actorCallback ) =
- $this->actorMigration->getInsertValuesWithTempTable( $dbw, 'rev_user', $user );
- $revisionRow += $actorFields;
-
- if ( $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
- $revisionRow['rev_text_id'] = $textId;
-
- // MCR migration note: rev_content_model and rev_content_format will go away
- if ( $this->contentHandlerUseDB ) {
- $defaultModel = ContentHandler::getDefaultModelFor( $title );
- $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+ $dbw->update(
+ 'revision',
+ [ 'rev_text_id' => $textId ],
+ [ 'rev_id' => $revisionId ],
+ __METHOD__
+ );
+ }
- $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
- $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
- }
+ /**
+ * @param IDatabase $dbw
+ * @param int $revisionId
+ * @param SlotRecord $protoSlot
+ * @param Title $title
+ * @param array $blobHints See the BlobStore::XXX_HINT constants
+ * @return SlotRecord
+ */
+ private function insertSlotOn(
+ IDatabase $dbw,
+ $revisionId,
+ SlotRecord $protoSlot,
+ Title $title,
+ array $blobHints = []
+ ) {
+ if ( $protoSlot->hasAddress() ) {
+ $blobAddress = $protoSlot->getAddress();
} else {
- /**
- * rev_text_id has NOT NULL and no DEFAULT, so set to 0 when we are not writing to it.
- * WARNING: This should NOT be removed after migration until a schema change has been
- * made in WMF production giving rev_text_id a DEFAULT value of 0 (otherwise inserts
- * will fail)
- * Task: https://phabricator.wikimedia.org/T190148#4064625
- */
- $revisionRow['rev_text_id'] = 0;
+ $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
}
- $dbw->insert( 'revision', $revisionRow, __METHOD__ );
-
- $hasSlots = false;
- $contentId = false;
+ // Write the main slot's text ID to the revision table for backwards compatibility
+ if ( $protoSlot->getRole() === 'main' && $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
+ $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+ }
- if ( isset( $revisionRow['rev_id'] ) ) {
- // Restoring a revision, slots should already exist,
- // unless the archive row wasn't migrated yet.
- if ( $this->mcrMigrationStage === MIGRATION_NEW ) {
- $hasSlots = true;
+ if ( $this->mcrMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ if ( $protoSlot->hasContentId() ) {
+ $contentId = $protoSlot->getContentId();
} else {
- $contentId = $this->findSlotContentId( $dbw, $revisionRow['rev_id'], 'main' );
- $hasSlots = (bool)$contentId;
+ $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
}
- } else {
- // not restoring a revision, use auto-increment value
- $revisionRow['rev_id'] = intval( $dbw->insertId() );
- }
- if ( $this->mcrMigrationStage > MIGRATION_OLD && $mainSlot->hasContentId() ) {
- // re-use content row of inherited slot!
- $contentId = $mainSlot->getContentId();
+ $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
+ } else {
+ $contentId = null;
}
- $revisionId = $revisionRow['rev_id'];
+ $savedSlot = SlotRecord::newSaved(
+ $revisionId,
+ $contentId,
+ $blobAddress,
+ $protoSlot
+ );
- $commentCallback( $revisionId );
- $actorCallback( $revisionId, $revisionRow );
+ return $savedSlot;
+ }
- // Insert IP revision into ip_changes for use when querying for a range.
+ /**
+ * Insert IP revision into ip_changes for use when querying for a range.
+ * @param IDatabase $dbw
+ * @param User $user
+ * @param RevisionRecord $rev
+ * @param int $revisionId
+ */
+ private function insertIpChangesRow(
+ IDatabase $dbw,
+ User $user,
+ RevisionRecord $rev,
+ $revisionId
+ ) {
if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
$ipcRow = [
'ipc_rev_id' => $revisionId,
- 'ipc_rev_timestamp' => $revisionRow['rev_timestamp'],
+ 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
'ipc_hex' => IP::toHex( $user->getName() ),
];
$dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
}
+ }
- if ( $this->mcrMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ /**
+ * @param IDatabase $dbw
+ * @param RevisionRecord $rev
+ * @param Title $title
+ * @param int $parentId
+ *
+ * @return array a revision table row
+ *
+ * @throws MWException
+ * @throws MWUnknownContentModelException
+ */
+ private function insertRevisionRowOn(
+ IDatabase $dbw,
+ RevisionRecord $rev,
+ Title $title,
+ $parentId
+ ) {
+ $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
- // Only insert slot rows for new revisions (not restored revisions).
- // Also, never insert content rows if not inserting slot rows.
- if ( !$hasSlots ) {
+ list( $commentFields, $commentCallback ) =
+ $this->commentStore->insertWithTempTable(
+ $dbw,
+ 'rev_comment',
+ $rev->getComment( RevisionRecord::RAW )
+ );
+ $revisionRow += $commentFields;
- // Only insert content rows for new content (not inherited content)
- if ( !$contentId ) {
- Assert::invariant( !$hasSlots, 'Re-using slots, but not content ID is known' );
- $contentId = $this->insertContentRowOn( $mainSlot, $dbw, $blobAddress );
- }
+ list( $actorFields, $actorCallback ) =
+ $this->actorMigration->getInsertValuesWithTempTable(
+ $dbw,
+ 'rev_user',
+ $rev->getUser( RevisionRecord::RAW )
+ );
+ $revisionRow += $actorFields;
- $this->insertSlotRowOn( $mainSlot, $dbw, $revisionId, $contentId );
- }
- } else {
- $contentId = null;
+ $dbw->insert( 'revision', $revisionRow, __METHOD__ );
+
+ if ( !isset( $revisionRow['rev_id'] ) ) {
+ // only if auto-increment was used
+ $revisionRow['rev_id'] = intval( $dbw->insertId() );
}
- $dbw->endAtomic( __METHOD__ );
+ $commentCallback( $revisionRow['rev_id'] );
+ $actorCallback( $revisionRow['rev_id'], $revisionRow );
- $newSlot = SlotRecord::newSaved( $revisionId, $contentId, $blobAddress, $mainSlot );
- $slots = new RevisionSlots( [ 'main' => $newSlot ] );
+ return $revisionRow;
+ }
- $rev = new RevisionStoreRecord(
- $title,
- $user,
- $comment,
- (object)$revisionRow,
- $slots,
- $this->wikiId
- );
+ /**
+ * @param IDatabase $dbw
+ * @param RevisionRecord $rev
+ * @param Title $title
+ * @param int $parentId
+ *
+ * @return array [ 0 => array $revisionRow, 1 => callable ]
+ * @throws MWException
+ * @throws MWUnknownContentModelException
+ */
+ private function getBaseRevisionRow(
+ IDatabase $dbw,
+ RevisionRecord $rev,
+ Title $title,
+ $parentId
+ ) {
+ // Record the edit in revisions
+ $revisionRow = [
+ 'rev_page' => $rev->getPageId(),
+ 'rev_parent_id' => $parentId,
+ 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
+ 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
+ 'rev_deleted' => $rev->getVisibility(),
+ 'rev_len' => $rev->getSize(),
+ 'rev_sha1' => $rev->getSha1(),
+ ];
- $newSlot = $rev->getSlot( 'main', RevisionRecord::RAW );
+ if ( $rev->getId() !== null ) {
+ // Needed to restore revisions with their original ID
+ $revisionRow['rev_id'] = $rev->getId();
+ }
- // sanity checks
- Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
- Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
- Assert::postcondition(
- $rev->getComment( RevisionRecord::RAW ) !== null,
- 'revision must have a comment'
- );
- Assert::postcondition(
- $rev->getUser( RevisionRecord::RAW ) !== null,
- 'revision must have a user'
- );
+ if ( $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
+ // In non MCR more this IF section will relate to the main slot
+ $mainSlot = $rev->getSlot( 'main' );
+ $model = $mainSlot->getModel();
+ $format = $mainSlot->getFormat();
- Assert::postcondition( $newSlot !== null, 'revision must have a main slot' );
- Assert::postcondition(
- $newSlot->getAddress() !== null,
- 'main slot must have an address'
- );
+ // MCR migration note: rev_content_model and rev_content_format will go away
+ if ( $this->contentHandlerUseDB ) {
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
- Hooks::run( 'RevisionRecordInserted', [ $rev ] );
+ $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
+ $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
+ }
+ }
- // TODO: deprecate in 1.32!
- $legacyRevision = new Revision( $rev );
- Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
+ return $revisionRow;
+ }
- return $rev;
+ /**
+ * @param SlotRecord $slot
+ * @param Title $title
+ * @param array $blobHints See the BlobStore::XXX_HINT constants
+ *
+ * @throws MWException
+ * @return string the blob address
+ */
+ private function storeContentBlob(
+ SlotRecord $slot,
+ Title $title,
+ array $blobHints = []
+ ) {
+ $content = $slot->getContent();
+ $format = $content->getDefaultFormat();
+ $model = $content->getModel();
+
+ $this->checkContent( $content, $title );
+
+ return $this->blobStore->storeBlob(
+ $content->serialize( $format ),
+ // These hints "leak" some information from the higher abstraction layer to
+ // low level storage to allow for optimization.
+ array_merge(
+ $blobHints,
+ [
+ BlobStore::DESIGNATION_HINT => 'page-content',
+ BlobStore::ROLE_HINT => $slot->getRole(),
+ BlobStore::SHA1_HINT => $slot->getSha1(),
+ BlobStore::MODEL_HINT => $model,
+ BlobStore::FORMAT_HINT => $format,
+ ]
+ )
+ );
}
/**
* @throws MWException
* @throws MWUnknownContentModelException
*/
- private function checkContentModel( Content $content, Title $title ) {
+ private function checkContent( Content $content, Title $title ) {
// Note: may return null for revisions that have not yet been inserted
$model = $content->getModel();
$blobFlags = null;
if ( is_object( $row ) ) {
+ if ( $this->mcrMigrationStage >= MIGRATION_NEW ) {
+ // Don't emulate from a row when using the new schema.
+ // Emulating from an array is still OK.
+ throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
+ }
+
// archive row
if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
$row = $this->mapArchiveFields( $row );
}
if ( !$content ) {
+ // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
+ // is missing, but "empty revisions" with no content are used in some edge cases.
+
$content = function ( SlotRecord $slot )
use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
{
return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, 'main' );
};
- // use negative IDs for fake slot records.
- $mainSlotRow->slot_id = -( $mainSlotRow->slot_revision_id );
return new SlotRecord( $mainSlotRow, $content );
}
);
}
+ /**
+ * @param int $revId The revision to load slots for.
+ * @param int $queryFlags
+ *
+ * @return SlotRecord[]
+ */
+ private function loadSlotRecords( $revId, $queryFlags ) {
+ $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
+
+ list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+ $db = $this->getDBConnectionRef( $dbMode );
+
+ $res = $db->select(
+ $revQuery['tables'],
+ $revQuery['fields'],
+ [
+ 'slot_revision_id' => $revId,
+ ],
+ __METHOD__,
+ $dbOptions,
+ $revQuery['joins']
+ );
+
+ $slots = [];
+
+ foreach ( $res as $row ) {
+ $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) {
+ return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
+ };
+
+ $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
+ }
+
+ if ( !isset( $slots['main'] ) ) {
+ throw new RevisionAccessException(
+ 'Main slot of revision ' . $revId . ' not found in database!'
+ );
+ };
+
+ return $slots;
+ }
+
+ /**
+ * Factory method for RevisionSlots.
+ *
+ * @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 int $queryFlags
+ * @param Title $title
+ *
+ * @return RevisionSlots
+ * @throws MWException
+ */
+ private function newRevisionSlots(
+ $revId,
+ $revisionRow,
+ $queryFlags,
+ Title $title
+ ) {
+ if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
+ // TODO: in MIGRATION_WRITE_BOTH, we could use the old and the new method:
+ // e.g. call emulateMainSlot_1_29() if loadSlotRecords() fails.
+
+ $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
+ $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+ } else {
+ // 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 );
+ } );
+ }
+
+ return $slots;
+ }
+
/**
* Make a fake revision object from an archive table row. This is queried
* for permissions or even inserted (as in Special:Undelete)
// Legacy because $row may have come from self::selectFields()
->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'ar_comment', $row, true );
- $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title );
- $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+ $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
}
/**
- * @see RevisionFactory::newRevisionFromRow_1_29
+ * @see RevisionFactory::newRevisionFromRow
*
* MCR migration note: this replaces Revision::newFromRow
*
* @param Title|null $title
*
* @return RevisionRecord
- * @throws MWException
- * @throws RevisionAccessException
*/
- private function newRevisionFromRow_1_29( $row, $queryFlags = 0, Title $title = null ) {
+ public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
Assert::parameterType( 'object', $row, '$row' );
if ( !$title ) {
// Legacy because $row may have come from self::selectFields()
->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'rev_comment', $row, true );
- $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title );
- $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+ $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
}
- /**
- * @see RevisionFactory::newRevisionFromRow
- *
- * MCR migration note: this replaces Revision::newFromRow
- *
- * @param object $row
- * @param int $queryFlags
- * @param Title|null $title
- *
- * @return RevisionRecord
- */
- public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
- return $this->newRevisionFromRow_1_29( $row, $queryFlags, $title );
- }
-
/**
* Constructs a new MutableRevisionRecord based on the given associative array following
* the MW1.29 convention for the Revision constructor.
// if we have a content object, use it to set the model and type
if ( !empty( $fields['content'] ) ) {
- if ( !( $fields['content'] instanceof Content ) ) {
- throw new MWException( 'content field must contain a Content object.' );
+ if ( !( $fields['content'] instanceof Content ) && !is_array( $fields['content'] ) ) {
+ throw new MWException(
+ 'content field must contain a Content object or an array of Content objects.'
+ );
}
+ }
- if ( !empty( $fields['text_id'] ) ) {
+ if ( !empty( $fields['text_id'] ) ) {
+ if ( $this->mcrMigrationStage >= MIGRATION_NEW ) {
+ throw new MWException( "Cannot use text_id field with MCR schema" );
+ }
+
+ if ( !empty( $fields['content'] ) ) {
throw new MWException(
"Text already stored in external store (id {$fields['text_id']}), " .
- "can't serialize content object"
+ "can't specify content object"
);
}
}
}
}
- $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
-
$revision = new MutableRevisionRecord( $title, $this->wikiId );
$this->initializeMutableRevisionFromArray( $revision, $fields );
- $revision->setSlot( $mainSlot );
+
+ if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
+ foreach ( $fields['content'] as $role => $content ) {
+ $revision->setContent( $role, $content );
+ }
+ } else {
+ $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
+ $revision->setSlot( $mainSlot );
+ }
return $revision;
}
/**
* Return the tables, fields, and join conditions to be selected to create
- * a new revision object.
+ * a new RevisionStoreRecord object.
*
* MCR migration note: this replaces Revision::getQueryInfo
*
* @param array $options Any combination of the following strings
* - 'page': Join with the page table, and select fields to identify the page
* - 'user': Join with the user table, and select the user name
- * - 'text': Join with the text table, and select fields to load page text
+ * - 'text': Join with the text table, and select fields to load page text. This
+ * option is deprecated in MW 1.32 with MCR migration stage MIGRATION_WRITE_BOTH,
+ * and disallowed with MIGRATION_MEW.
*
* @return array With three keys:
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
if ( in_array( 'text', $options, true ) ) {
if ( $this->mcrMigrationStage === MIGRATION_NEW ) {
throw new InvalidArgumentException( 'text table can no longer be joined directly' );
+ } elseif ( $this->mcrMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
}
$ret['tables'][] = 'text';
/**
* Return the tables, fields, and join conditions to be selected to create
- * a new archived revision object.
+ * a new SlotRecord.
+ *
+ * @since 1.32
+ *
+ * @param array $options Any combination of the following strings
+ * - 'content': Join with the content table, and select content meta-data fields
+ *
+ * @return array With three keys:
+ * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+ * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+ * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+ */
+ public function getSlotsQueryInfo( $options = [] ) {
+ $ret = [
+ 'tables' => [],
+ 'fields' => [],
+ 'joins' => [],
+ ];
+
+ if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
+ $db = $this->getDBConnectionRef( DB_REPLICA );
+ $ret['tables']['slots'] = 'revision';
+
+ $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' );
+
+ if ( in_array( 'content', $options, true ) ) {
+ $ret['fields']['content_size'] = 'slots.rev_len';
+ $ret['fields']['content_sha1'] = 'slots.rev_sha1';
+ $ret['fields']['content_address']
+ = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] );
+
+ if ( $this->contentHandlerUseDB ) {
+ $ret['fields']['model_name'] = 'slots.rev_content_model';
+ } else {
+ $ret['fields']['model_name'] = 'NULL';
+ }
+ }
+
+ // XXX: in MIGRATION_WRITE_BOTH mode, emulate *and* select - using a UNION?
+ // See Anomie's idea at <https://gerrit.wikimedia.org/r/c/416465/
+ // 8..10/includes/Storage/RevisionStore.php#2113>
+ } else {
+ $ret['tables'][] = 'slots';
+ $ret['tables'][] = 'slot_roles';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'slot_revision_id',
+ 'slot_content_id',
+ 'slot_origin',
+ 'role_name'
+ ] );
+ $ret['joins']['slot_roles'] = [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ];
+
+ if ( in_array( 'content', $options, true ) ) {
+ $ret['tables'][] = 'content';
+ $ret['tables'][] = 'content_models';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'content_size',
+ 'content_sha1',
+ 'content_address',
+ 'model_name'
+ ] );
+ $ret['joins']['content'] = [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ];
+ $ret['joins']['content_models'] = [ 'INNER JOIN', [ 'content_model = model_id' ] ];
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new RevisionArchiveRecord object.
*
* MCR migration note: this replaces Revision::getArchiveQueryInfo
*
Assert::parameterType( 'object', $row, '$row' );
Assert::parameterType( 'Content|callable', $content, '$content' );
- Assert::parameter(
- property_exists( $row, 'slot_id' ),
- '$row->slot_id',
- 'must exist'
- );
Assert::parameter(
property_exists( $row, 'slot_revision_id' ),
'$row->slot_revision_id',
* Task: https://phabricator.wikimedia.org/T190148
* Copying the value from the revision table should not lead to any issues for now.
*/
- 'ar_text_id' => $row->rev_text_id,
'ar_len' => $row->rev_len,
'ar_page_id' => $id,
'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
'ar_sha1' => $row->rev_sha1,
] + $commentStore->insert( $dbw, 'ar_comment', $comment )
+ $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
+
+ if ( $wgMultiContentRevisionSchemaMigrationStage < MIGRATION_NEW ) {
+ $rowInsert['ar_text_id'] = $row->rev_text_id;
+ }
+
if (
$wgContentHandlerUseDB &&
$wgMultiContentRevisionSchemaMigrationStage <= MIGRATION_WRITE_BOTH
# tests/phpunit/includes/Storage
'MediaWiki\Tests\Storage\McrSchemaDetection' => "$testDir/phpunit/includes/Storage/McrSchemaDetection.php",
+ 'MediaWiki\Tests\Storage\McrSchemaOverride' => "$testDir/phpunit/includes/Storage/McrSchemaOverride.php",
'MediaWiki\Tests\Storage\McrWriteBothSchemaOverride' => "$testDir/phpunit/includes/Storage/McrWriteBothSchemaOverride.php",
'MediaWiki\Tests\Storage\RevisionSlotsTest' => "$testDir/phpunit/includes/Storage/RevisionSlotsTest.php",
'MediaWiki\Tests\Storage\RevisionRecordTests' => "$testDir/phpunit/includes/Storage/RevisionRecordTests.php",
/**
* @throws LogicException if the given database connection is not a set up to use
* mock tables.
+ *
+ * @since 1.31 this is no longer private.
*/
- private function ensureMockDatabaseConnection( IDatabase $db ) {
+ protected function ensureMockDatabaseConnection( IDatabase $db ) {
if ( $db->tablePrefix() !== $this->dbPrefix() ) {
throw new LogicException(
'Trying to delete mock tables, but table prefix does not indicate a mock database.'
[
'rev_id',
'rev_page',
- 'rev_text_id',
'rev_minor_edit',
'rev_deleted',
'rev_len',
[ [
strval( $rev->getId() ),
strval( $this->testPage->getId() ),
- strval( $textId ),
'0',
'0',
'13',
);
}
+ public function provideInsertOn_exceptionOnIncomplete() {
+ $content = new TextContent( '' );
+ $user = User::newFromName( 'Foo' );
+
+ yield 'no parent' => [
+ [
+ 'content' => $content,
+ 'comment' => 'test',
+ 'user' => $user,
+ ],
+ IncompleteRevisionException::class,
+ "rev_page field must not be 0!"
+ ];
+
+ yield 'no comment' => [
+ [
+ 'content' => $content,
+ 'page' => 7,
+ 'user' => $user,
+ ],
+ IncompleteRevisionException::class,
+ "comment must not be NULL!"
+ ];
+
+ yield 'no content' => [
+ [
+ 'comment' => 'test',
+ 'page' => 7,
+ 'user' => $user,
+ ],
+ IncompleteRevisionException::class,
+ "Uninitialized field: content_address" // XXX: message may change
+ ];
+ }
+
/**
+ * @dataProvider provideInsertOn_exceptionOnIncomplete
* @covers Revision::insertOn
*/
- public function testInsertOn_exceptionOnNoPage() {
+ public function testInsertOn_exceptionOnIncomplete( $array, $expException, $expMessage ) {
// If an ExternalStore is set don't use it.
$this->setMwGlobals( 'wgDefaultExternalStore', false );
- $this->setExpectedException(
- IncompleteRevisionException::class,
- "rev_page field must not be 0!"
- );
+ $this->setExpectedException( $expException, $expMessage );
$title = Title::newFromText( 'Nonexistant-' . __METHOD__ );
- $rev = new Revision( [], 0, $title );
+ $rev = new Revision( $array, 0, $title );
$rev->insertOn( wfGetDB( DB_MASTER ) );
}
$rev = new Revision( [
'page' => $this->testPage->getId(),
'content_model' => $this->testPage->getContentModel(),
- 'text_id' => 123456789, // not in the test DB
+ 'id' => 123456789, // not in the test DB
] );
Wikimedia\suppressWarnings(); // bad text_id will trigger a warning.
--- /dev/null
+<?php
+use MediaWiki\Tests\Storage\McrSchemaOverride;
+
+/**
+ * Tests Revision against the MCR DB schema after schema migration.
+ *
+ * @covers Revision
+ *
+ * @group Revision
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class RevisionMcrDbTest extends RevisionDbTestBase {
+
+ use McrSchemaOverride;
+
+ public function setUp() {
+ parent::setUp();
+ }
+
+ protected function getContentHandlerUseDB() {
+ return true;
+ }
+
+}
'content' => new WikitextContent( 'GOAT' ),
'text_id' => 'someid',
],
- new MWException( "Text already stored in external store (id someid), " .
- "can't serialize content object" )
+ new MWException( 'Text already stored in external store (id someid),' )
];
yield 'with bad content object (class)' => [
[ 'content' => new stdClass() ],
- new MWException( 'content field must contain a Content object.' )
+ new MWException( 'content field must contain a Content object' )
];
yield 'with bad content object (string)' => [
[ 'content' => 'ImAGoat' ],
- new MWException( 'content field must contain a Content object.' )
+ new MWException( 'content field must contain a Content object' )
];
yield 'bad row format' => [
'imastring, not a row',
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Storage;
+
+use CommentStoreComment;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\SlotRecord;
+use TextContent;
+use Title;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the post-migration MCR DB schema.
+ *
+ * @covers \MediaWiki\Storage\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+ use McrSchemaOverride;
+
+ protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+ $numberOfSlots = count( $rev->getSlotRoles() );
+
+ $this->assertSelect(
+ 'slots',
+ [ 'count(*)' ],
+ [ 'slot_revision_id' => $rev->getId() ],
+ [ [ (string)$numberOfSlots ] ]
+ );
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
+
+ $this->assertSelect(
+ $revQuery['tables'],
+ [ 'count(*)' ],
+ [
+ 'slot_revision_id' => $rev->getId(),
+ ],
+ [ [ (string)$numberOfSlots ] ],
+ [],
+ $revQuery['joins']
+ );
+
+ $this->assertSelect(
+ 'content',
+ [ 'count(*)' ],
+ [ 'content_address' => $rev->getSlot( 'main' )->getAddress() ],
+ [ [ 1 ] ]
+ );
+
+ parent::assertRevisionExistsInDatabase( $rev );
+ }
+
+ /**
+ * @param SlotRecord $a
+ * @param SlotRecord $b
+ */
+ protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+ parent::assertSameSlotContent( $a, $b );
+
+ // Assert that the same content ID has been used
+ $this->assertSame( $a->getContentId(), $b->getContentId() );
+ }
+
+ public function provideInsertRevisionOn_successes() {
+ foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
+ yield $case;
+ }
+
+ yield 'Multi-slot revision insertion' => [
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'aux' => new TextContent( 'Egg' ),
+ ],
+ 'page' => true,
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ ];
+ }
+
+ public function provideNewNullRevision() {
+ foreach ( parent::provideNewNullRevision() as $case ) {
+ yield $case;
+ }
+
+ yield [
+ Title::newFromText( 'UTPage_notAutoCreated' ),
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'aux' => new WikitextContent( 'Omelet' ),
+ ],
+ ],
+ CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
+ ];
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+ yield $case;
+ }
+
+ yield 'Basic array, multiple roles' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 29,
+ 'parent_id' => 1,
+ 'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
+ 'comment' => 'Goat Comment!',
+ 'content' => [
+ 'main' => new WikitextContent( 'Söme Cöntent' ),
+ 'aux' => new TextContent( 'Öther Cöntent' ),
+ ]
+ ]
+ ];
+ }
+
+ public function testGetQueryInfo_NoSlotDataJoin() {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $queryInfo = $store->getQueryInfo();
+
+ // with the new schema enabled, query info should not join the main slot info
+ $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
+ $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
+ }
+
+ public function provideGetArchiveQueryInfo() {
+ yield [
+ [
+ 'tables' => [
+ 'archive',
+ ],
+ 'fields' => array_merge(
+ $this->getDefaultArchiveFields( false ),
+ [
+ 'ar_comment_text' => 'ar_comment',
+ 'ar_comment_data' => 'NULL',
+ 'ar_comment_cid' => 'NULL',
+ 'ar_user_text' => 'ar_user_text',
+ 'ar_user' => 'ar_user',
+ 'ar_actor' => 'NULL',
+ ]
+ ),
+ 'joins' => [
+ ],
+ ]
+ ];
+ }
+
+ public function provideGetQueryInfo() {
+ // TODO: more option variations
+ yield [
+ [ 'page', 'user' ],
+ [
+ 'tables' => [
+ 'revision',
+ 'page',
+ 'user',
+ ],
+ 'fields' => array_merge(
+ $this->getDefaultQueryFields( false ),
+ $this->getCommentQueryFields(),
+ $this->getActorQueryFields(),
+ [
+ 'page_namespace',
+ 'page_title',
+ 'page_id',
+ 'page_latest',
+ 'page_is_redirect',
+ 'page_len',
+ 'user_name',
+ ]
+ ),
+ 'joins' => [
+ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+ ],
+ ]
+ ];
+ }
+
+ public function provideGetSlotsQueryInfo() {
+ yield [
+ [],
+ [
+ 'tables' => [
+ 'slots',
+ 'slot_roles',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id',
+ 'slot_content_id',
+ 'slot_origin',
+ 'role_name',
+ ]
+ ),
+ 'joins' => [
+ 'slot_roles' => [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ],
+ ],
+ ]
+ ];
+ yield [
+ [ 'content' ],
+ [
+ 'tables' => [
+ 'slots',
+ 'slot_roles',
+ 'content',
+ 'content_models',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id',
+ 'slot_content_id',
+ 'slot_origin',
+ 'role_name',
+ 'content_size',
+ 'content_sha1',
+ 'content_address',
+ 'model_name',
+ ]
+ ),
+ 'joins' => [
+ 'slot_roles' => [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ],
+ 'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+ 'content_models' => [ 'INNER JOIN', [ 'content_model = model_id' ] ],
+ ],
+ ]
+ ];
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Storage;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the post-migration
+ * MCR database schema.
+ */
+trait McrSchemaOverride {
+
+ use PatchFileLocation;
+ use McrSchemaDetection;
+
+ /**
+ * @return int
+ */
+ protected function getMcrMigrationStage() {
+ return MIGRATION_NEW;
+ }
+
+ /**
+ * @return string[]
+ */
+ protected function getMcrTablesToReset() {
+ return [
+ 'content',
+ 'content_models',
+ 'slots',
+ 'slot_roles',
+ ];
+ }
+
+ protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+ $overrides = [
+ 'scripts' => [],
+ 'drop' => [],
+ 'create' => [],
+ 'alter' => [],
+ ];
+
+ if ( !$this->hasMcrTables( $db ) ) {
+ $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots.sql' );
+ }
+
+ if ( !$this->hasPreMcrFields( $db ) ) {
+ $overrides['alter'][] = 'revision';
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'drop-pre-mcr-fields', __DIR__ );
+ }
+
+ return $overrides;
+ }
+
+}
<?php
namespace MediaWiki\Tests\Storage;
+use InvalidArgumentException;
use MediaWiki\Storage\RevisionRecord;
use MediaWiki\Storage\SlotRecord;
+use Revision;
+use WikitextContent;
/**
* Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
use McrWriteBothSchemaOverride;
- protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
- parent::assertRevisionExistsInDatabase( $rev );
+ protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+ $row = parent::revisionToRow( $rev, $options );
+ $row->rev_text_id = (string)$rev->getTextId();
+ $row->rev_content_format = (string)$rev->getContentFormat();
+ $row->rev_content_model = (string)$rev->getContentModel();
+
+ return $row;
+ }
+
+ protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
$this->assertSelect(
- 'slots', [ 'count(*)' ], [ 'slot_revision_id' => $rev->getId() ], [ [ '1' ] ]
+ 'slots',
+ [ 'count(*)' ],
+ [ 'slot_revision_id' => $rev->getId() ],
+ [ [ '1' ] ]
);
+
$this->assertSelect(
'content',
[ 'count(*)' ],
[ 'content_address' => $rev->getSlot( 'main' )->getAddress() ],
[ [ '1' ] ]
);
+
+ parent::assertRevisionExistsInDatabase( $rev );
}
/**
]
];
yield [
- [ 'page', 'user', 'text' ],
+ [ 'page', 'user' ],
[
- 'tables' => [ 'revision', 'page', 'user', 'text' ],
+ 'tables' => [ 'revision', 'page', 'user' ],
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
'page_is_redirect',
'page_len',
'user_name',
- 'old_text',
- 'old_flags',
]
),
'joins' => [
'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
- 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
],
]
];
}
+ public function provideGetSlotsQueryInfo() {
+ $db = wfGetDB( DB_REPLICA );
+
+ yield [
+ [],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( 'main' ),
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield [
+ [ 'content' ],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( 'main' ),
+ 'content_size' => 'slots.rev_len',
+ 'content_sha1' => 'slots.rev_sha1',
+ 'content_address' =>
+ 'CONCAT(' . $db->addQuotes( 'tt:' ) . ',slots.rev_text_id)',
+ 'model_name' => 'slots.rev_content_model',
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ }
+
+ public function provideInsertRevisionOn_failures() {
+ foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
+ yield $case;
+ }
+
+ yield 'slot that is not main slot' => [
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'lalala' => new WikitextContent( 'Duck' ),
+ ],
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ new InvalidArgumentException( 'Only the main slot is supported' )
+ ];
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+ yield $case;
+ }
+
+ yield 'Basic array, with page & id' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'text_id' => 2,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'content_format' => 'text/x-wiki',
+ 'content_model' => 'wikitext',
+ ]
+ ];
+ }
+
}
<?php
namespace MediaWiki\Tests\Storage;
+use Revision;
+
/**
* Tests RevisionStore against the pre-MCR, pre-ContentHandler DB schema.
*
return false;
}
+ protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+ $row = parent::revisionToRow( $rev, $options );
+
+ $row->rev_text_id = (string)$rev->getTextId();
+
+ return $row;
+ }
+
public function provideGetArchiveQueryInfo() {
yield [
[
];
}
+ public function provideGetSlotsQueryInfo() {
+ $db = wfGetDB( DB_REPLICA );
+
+ yield [
+ [],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( 'main' ),
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield [
+ [ 'content' ],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( 'main' ),
+ 'content_size' => 'slots.rev_len',
+ 'content_sha1' => 'slots.rev_sha1',
+ 'content_address' =>
+ 'CONCAT(' . $db->addQuotes( 'tt:' ) . ',slots.rev_text_id)',
+ 'model_name' => 'NULL',
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+ yield $case;
+ }
+
+ yield 'Basic array, with page & id' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'text_id' => 2,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ ]
+ ];
+ }
+
}
<?php
namespace MediaWiki\Tests\Storage;
+use InvalidArgumentException;
+use Revision;
+use WikitextContent;
+
/**
* Tests RevisionStore against the pre-MCR DB schema.
*
use PreMcrSchemaOverride;
+ protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+ $row = parent::revisionToRow( $rev, $options );
+
+ $row->rev_text_id = (string)$rev->getTextId();
+ $row->rev_content_format = (string)$rev->getContentFormat();
+ $row->rev_content_model = (string)$rev->getContentModel();
+
+ return $row;
+ }
+
public function provideGetArchiveQueryInfo() {
yield [
[
];
}
+ public function provideGetSlotsQueryInfo() {
+ $db = wfGetDB( DB_REPLICA );
+
+ yield [
+ [],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( 'main' ),
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield [
+ [ 'content' ],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( 'main' ),
+ 'content_size' => 'slots.rev_len',
+ 'content_sha1' => 'slots.rev_sha1',
+ 'content_address' =>
+ 'CONCAT(' . $db->addQuotes( 'tt:' ) . ',slots.rev_text_id)',
+ 'model_name' => 'slots.rev_content_model',
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ }
+
+ public function provideInsertRevisionOn_failures() {
+ foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
+ yield $case;
+ }
+
+ yield 'slot that is not main slot' => [
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'lalala' => new WikitextContent( 'Duck' ),
+ ],
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ new InvalidArgumentException( 'Only the main slot is supported' )
+ ];
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+ yield $case;
+ }
+
+ yield 'Basic array, with page & id' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'text_id' => 2,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'content_format' => 'text/x-wiki',
+ 'content_model' => 'wikitext',
+ ]
+ ];
+ }
+
}
namespace MediaWiki\Tests\Storage;
use CommentStoreComment;
+use Content;
use Exception;
use HashBagOStuff;
use InvalidArgumentException;
*/
abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
+ /**
+ * @var Title
+ */
+ private $testPageTitle;
+
+ /**
+ * @var WikiPage
+ */
+ private $testPage;
+
/**
* @return int
*/
// Blank out. This would fail with a modified schema, and we don't need it.
}
+ /**
+ * @return Title
+ */
+ protected function getTestPageTitle() {
+ if ( $this->testPageTitle ) {
+ return $this->testPageTitle;
+ }
+
+ $this->testPageTitle = Title::newFromText( 'UTPage-' . __CLASS__ );
+ return $this->testPageTitle;
+ }
+ /**
+ * @return WikiPage
+ */
+ protected function getTestPage() {
+ if ( $this->testPage ) {
+ return $this->testPage;
+ }
+
+ $title = $this->getTestPageTitle();
+ $this->testPage = WikiPage::factory( $title );
+
+ if ( !$this->testPage->exists() ) {
+ // Make sure we don't write to the live db.
+ $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
+
+ $user = static::getTestSysop()->getUser();
+
+ $this->testPage->doEditContent(
+ new WikitextContent( 'UTContent-' . __CLASS__ ),
+ 'UTPageSummary-' . __CLASS__,
+ EDIT_NEW | EDIT_SUPPRESS_RC,
+ false,
+ $user
+ );
+ }
+
+ return $this->testPage;
+ }
+
/**
* @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
*/
}
private function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) {
+ $this->assertEquals(
+ $r1->getPageAsLinkTarget()->getNamespace(),
+ $r2->getPageAsLinkTarget()->getNamespace()
+ );
+
+ $this->assertEquals(
+ $r1->getPageAsLinkTarget()->getText(),
+ $r2->getPageAsLinkTarget()->getText()
+ );
+
+ if ( $r1->getParentId() ) {
+ $this->assertEquals( $r1->getParentId(), $r2->getParentId() );
+ }
+
$this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() );
$this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() );
$this->assertEquals( $r1->getComment(), $r2->getComment() );
- $this->assertEquals( $r1->getPageAsLinkTarget(), $r2->getPageAsLinkTarget() );
$this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() );
$this->assertEquals( $r1->getVisibility(), $r2->getVisibility() );
$this->assertEquals( $r1->getSha1(), $r2->getSha1() );
- $this->assertEquals( $r1->getParentId(), $r2->getParentId() );
$this->assertEquals( $r1->getSize(), $r2->getSize() );
$this->assertEquals( $r1->getPageId(), $r2->getPageId() );
$this->assertEquals( $r1->getSlotRoles(), $r2->getSlotRoles() );
}
private function assertRevisionCompleteness( RevisionRecord $r ) {
+ $this->assertTrue( $r->hasSlot( 'main' ) );
+ $this->assertInstanceOf( SlotRecord::class, $r->getSlot( 'main' ) );
+ $this->assertInstanceOf( Content::class, $r->getContent( 'main' ) );
+
foreach ( $r->getSlotRoles() as $role ) {
$this->assertSlotCompleteness( $r, $r->getSlot( $role ) );
}
private function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) {
$this->assertTrue( $slot->hasAddress() );
$this->assertSame( $r->getId(), $slot->getRevision() );
+
+ $this->assertInstanceOf( Content::class, $slot->getContent() );
}
/**
*
* @return RevisionRecord
*/
- private function getRevisionRecordFromDetailsArray( $title, $details = [] ) {
+ private function getRevisionRecordFromDetailsArray( $details = [] ) {
// Convert some values that can't be provided by dataProviders
- $page = WikiPage::factory( $title );
if ( isset( $details['user'] ) && $details['user'] === true ) {
$details['user'] = $this->getTestUser()->getUser();
}
if ( isset( $details['page'] ) && $details['page'] === true ) {
- $details['page'] = $page->getId();
+ $details['page'] = $this->getTestPage()->getId();
}
if ( isset( $details['parent'] ) && $details['parent'] === true ) {
- $details['parent'] = $page->getLatest();
+ $details['parent'] = $this->getTestPage()->getLatest();
}
// Create the RevisionRecord with any available data
- $rev = new MutableRevisionRecord( $title );
+ $rev = new MutableRevisionRecord( $this->getTestPageTitle() );
isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null;
isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null;
isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null;
isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null;
isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null;
+ if ( isset( $details['content'] ) ) {
+ foreach ( $details['content'] as $role => $content ) {
+ $rev->setContent( $role, $content );
+ }
+ }
+
return $rev;
}
public function provideInsertRevisionOn_successes() {
yield 'Bare minimum revision insertion' => [
- Title::newFromText( 'UTPage' ),
[
'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
- 'parent' => true,
+ 'page' => true,
'comment' => $this->getRandomCommentStoreComment(),
'timestamp' => '20171117010101',
'user' => true,
],
];
yield 'Detailed revision insertion' => [
- Title::newFromText( 'UTPage' ),
[
'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
'parent' => true,
];
}
- private function getRandomCommentStoreComment() {
+ protected function getRandomCommentStoreComment() {
return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) );
}
* @covers \MediaWiki\Storage\RevisionStore::insertContentRowOn
*/
public function testInsertRevisionOn_successes(
- Title $title,
array $revDetails = []
) {
- $this->getExistingTestPage( $title );
- $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails );
+ $title = $this->getTestPageTitle();
+ $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
$this->overrideMwServices();
$store = MediaWikiServices::getInstance()->getRevisionStore();
$return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
+ // is the new revision correct?
+ $this->assertRevisionCompleteness( $return );
$this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() );
$this->assertRevisionRecordsEqual( $rev, $return );
- $this->assertRevisionCompleteness( $return );
+
+ // can we load it from the store?
+ $loaded = $store->getRevisionById( $return->getId() );
+ $this->assertRevisionCompleteness( $loaded );
+ $this->assertRevisionRecordsEqual( $return, $loaded );
+
+ // can we find it directly in the database?
$this->assertRevisionExistsInDatabase( $return );
}
protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+ $row = $this->revisionToRow( new Revision( $rev ), [] );
+
+ // unset nulled fields
+ unset( $row->rev_content_model );
+ unset( $row->rev_content_format );
+
+ // unset fake fields
+ unset( $row->rev_comment_text );
+ unset( $row->rev_comment_data );
+ unset( $row->rev_comment_cid );
+ unset( $row->rev_comment_id );
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $queryInfo = $store->getQueryInfo( [ 'user' ] );
+
+ $row = get_object_vars( $row );
$this->assertSelect(
- 'revision', [ 'count(*)' ], [ 'rev_id' => $rev->getId() ], [ [ '1' ] ]
+ $queryInfo['tables'],
+ array_keys( $row ),
+ [ 'rev_id' => $rev->getId() ],
+ [ array_values( $row ) ],
+ [],
+ $queryInfo['joins']
);
}
* @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
*/
public function testInsertRevisionOn_blobAddressExists() {
- $page = $this->getExistingTestPage();
- $title = $page->getTitle();
+ $title = $this->getTestPageTitle();
$revDetails = [
'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
'parent' => true,
$store = MediaWikiServices::getInstance()->getRevisionStore();
// Insert the first revision
- $revOne = $this->getRevisionRecordFromDetailsArray( $title, $revDetails );
+ $revOne = $this->getRevisionRecordFromDetailsArray( $revDetails );
$firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) );
$this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() );
$this->assertRevisionRecordsEqual( $revOne, $firstReturn );
// Insert a second revision inheriting the same blob address
$revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( 'main' ) );
- $revTwo = $this->getRevisionRecordFromDetailsArray( $title, $revDetails );
+ $revTwo = $this->getRevisionRecordFromDetailsArray( $revDetails );
$secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) );
$this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() );
$this->assertRevisionRecordsEqual( $revTwo, $secondReturn );
public function provideInsertRevisionOn_failures() {
yield 'no slot' => [
- Title::newFromText( 'UTPage' ),
[
'comment' => $this->getRandomCommentStoreComment(),
'timestamp' => '20171117010101',
'user' => true,
],
- new InvalidArgumentException( 'At least one slot needs to be defined!' )
+ new InvalidArgumentException( 'main slot must be provided' )
];
- yield 'slot that is not main slot' => [
- Title::newFromText( 'UTPage' ),
+ yield 'no main slot' => [
[
- 'slot' => SlotRecord::newUnsaved( 'lalala', new WikitextContent( 'Chicken' ) ),
+ 'slot' => SlotRecord::newUnsaved( 'aux', new WikitextContent( 'Turkey' ) ),
'comment' => $this->getRandomCommentStoreComment(),
'timestamp' => '20171117010101',
'user' => true,
],
- new InvalidArgumentException( 'Only the main slot is supported for now!' )
+ new InvalidArgumentException( 'main slot must be provided' )
];
yield 'no timestamp' => [
- Title::newFromText( 'UTPage' ),
[
'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
'comment' => $this->getRandomCommentStoreComment(),
new IncompleteRevisionException( 'timestamp field must not be NULL!' )
];
yield 'no comment' => [
- Title::newFromText( 'UTPage' ),
[
'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
'timestamp' => '20171117010101',
new IncompleteRevisionException( 'comment must not be NULL!' )
];
yield 'no user' => [
- Title::newFromText( 'UTPage' ),
[
'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
'comment' => $this->getRandomCommentStoreComment(),
* @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
*/
public function testInsertRevisionOn_failures(
- Title $title,
array $revDetails = [],
Exception $exception
) {
- $page = $this->getExistingTestPage( $title );
-
- $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails );
+ $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
$store = MediaWikiServices::getInstance()->getRevisionStore();
public function provideNewNullRevision() {
yield [
- Title::newFromText( __METHOD__ ),
+ Title::newFromText( 'UTPage_notAutoCreated' ),
+ [ 'content' => [ 'main' => new WikitextContent( 'Flubber1' ) ] ],
CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ),
true,
];
yield [
- Title::newFromText( __METHOD__ ),
+ Title::newFromText( 'UTPage_notAutoCreated' ),
+ [ 'content' => [ 'main' => new WikitextContent( 'Flubber2' ) ] ],
CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ),
false,
];
* @covers \MediaWiki\Storage\RevisionStore::newNullRevision
* @covers \MediaWiki\Storage\RevisionStore::findSlotContentId
*/
- public function testNewNullRevision( Title $title, $comment, $minor ) {
+ public function testNewNullRevision( Title $title, $revDetails, $comment, $minor = false ) {
$this->overrideMwServices();
- $page = $this->getExistingTestPage( $title );
- $rev = $page->getRevision();
+ $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
+ $page = WikiPage::factory( $title );
+
+ if ( !$page->exists() ) {
+ $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__, EDIT_NEW );
+ }
+ $revDetails['page'] = $page->getId();
+ $revDetails['timestamp'] = wfTimestampNow();
+ $revDetails['comment'] = CommentStoreComment::newUnsavedComment( 'Base' );
+ $revDetails['user'] = $user;
+
+ $baseRev = $this->getRevisionRecordFromDetailsArray( $revDetails );
$store = MediaWikiServices::getInstance()->getRevisionStore();
- $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
- $parent = $store->getRevisionById( $rev->getId() );
+ $dbw = wfGetDB( DB_MASTER );
+ $baseRev = $store->insertRevisionOn( $baseRev, $dbw );
+ $page->updateRevisionOn( $dbw, new Revision( $baseRev ), $page->getLatest() );
+
$record = $store->newNullRevision(
wfGetDB( DB_MASTER ),
$title,
$this->assertEquals( $comment, $record->getComment() );
$this->assertEquals( $minor, $record->isMinor() );
$this->assertEquals( $user->getName(), $record->getUser()->getName() );
- $this->assertEquals( $parent->getId(), $record->getParentId() );
+ $this->assertEquals( $baseRev->getId(), $record->getParentId() );
+
+ $this->assertArrayEqualsIgnoringIntKeyOrder(
+ $baseRev->getSlotRoles(),
+ $record->getSlotRoles()
+ );
- $parentSlot = $parent->getSlot( 'main' );
- $slot = $record->getSlot( 'main' );
+ foreach ( $baseRev->getSlotRoles() as $role ) {
+ $parentSlot = $baseRev->getSlot( $role );
+ $slot = $record->getSlot( $role );
- $this->assertTrue( $slot->isInherited(), 'isInherited' );
- $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' );
- $this->assertSameSlotContent( $parentSlot, $slot );
+ $this->assertTrue( $slot->isInherited(), 'isInherited' );
+ $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' );
+ $this->assertSameSlotContent( $parentSlot, $slot );
+ }
}
/**
* @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled
*/
public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() {
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
$status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
/** @var Revision $rev */
$rev = $status->value['revision'];
$this->assertGreaterThan( 0, $result );
$this->assertSame(
- $page->getRevision()->getRecentChange()->getAttribute( 'rc_id' ),
+ $store->getRecentChange( $revisionRecord )->getAttribute( 'rc_id' ),
$result
);
}
public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() {
// This assumes that sysops are auto patrolled
$sysop = $this->getTestSysop()->getUser();
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
$status = $page->doEditContent(
new WikitextContent( __METHOD__ ),
__METHOD__,
* @covers \MediaWiki\Storage\RevisionStore::getRecentChange
*/
public function testGetRecentChange() {
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
$content = new WikitextContent( __METHOD__ );
$status = $page->doEditContent( $content, __METHOD__ );
/** @var Revision $rev */
* @covers \MediaWiki\Storage\RevisionStore::getRevisionById
*/
public function testGetRevisionById() {
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
$content = new WikitextContent( __METHOD__ );
$status = $page->doEditContent( $content, __METHOD__ );
/** @var Revision $rev */
* @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle
*/
public function testGetRevisionByTitle() {
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
$content = new WikitextContent( __METHOD__ );
$status = $page->doEditContent( $content, __METHOD__ );
/** @var Revision $rev */
* @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId
*/
public function testGetRevisionByPageId() {
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
$content = new WikitextContent( __METHOD__ );
$status = $page->doEditContent( $content, __METHOD__ );
/** @var Revision $rev */
// Make sure there is 1 second between the last revision and the rev we create...
// Otherwise we might not get the correct revision and the test may fail...
// :(
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
sleep( 1 );
$content = new WikitextContent( __METHOD__ );
$status = $page->doEditContent( $content, __METHOD__ );
$this->assertSame( __METHOD__, $revRecord->getComment()->text );
}
- protected function revisionToRow( Revision $rev ) {
+ protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+ // XXX: the WikiPage object loads another RevisionRecord from the database. Not great.
$page = WikiPage::factory( $rev->getTitle() );
- return (object)[
+ $fields = [
'rev_id' => (string)$rev->getId(),
'rev_page' => (string)$rev->getPage(),
- 'rev_text_id' => (string)$rev->getTextId(),
'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ),
'rev_user_text' => (string)$rev->getUserText(),
'rev_user' => (string)$rev->getUser(),
'rev_len' => (string)$rev->getSize(),
'rev_parent_id' => (string)$rev->getParentId(),
'rev_sha1' => (string)$rev->getSha1(),
- 'rev_comment_text' => $rev->getComment(),
- 'rev_comment_data' => null,
- 'rev_comment_cid' => null,
- 'rev_content_format' => $rev->getContentFormat(),
- 'rev_content_model' => $rev->getContentModel(),
- 'page_namespace' => (string)$page->getTitle()->getNamespace(),
- 'page_title' => $page->getTitle()->getDBkey(),
- 'page_id' => (string)$page->getId(),
- 'page_latest' => (string)$page->getLatest(),
- 'page_is_redirect' => $page->isRedirect() ? '1' : '0',
- 'page_len' => (string)$page->getContent()->getSize(),
- 'user_name' => (string)$rev->getUserText(),
];
+
+ if ( in_array( 'page', $options ) ) {
+ $fields += [
+ 'page_namespace' => (string)$page->getTitle()->getNamespace(),
+ 'page_title' => $page->getTitle()->getDBkey(),
+ 'page_id' => (string)$page->getId(),
+ 'page_latest' => (string)$page->getLatest(),
+ 'page_is_redirect' => $page->isRedirect() ? '1' : '0',
+ 'page_len' => (string)$page->getContent()->getSize(),
+ ];
+ }
+
+ if ( in_array( 'user', $options ) ) {
+ $fields += [
+ 'user_name' => (string)$rev->getUserText(),
+ ];
+ }
+
+ if ( in_array( 'comment', $options ) ) {
+ $fields += [
+ 'rev_comment_text' => $rev->getComment(),
+ 'rev_comment_data' => null,
+ 'rev_comment_cid' => null,
+ ];
+ }
+
+ if ( $rev->getId() ) {
+ $fields += [
+ 'rev_id' => (string)$rev->getId(),
+ ];
+ }
+
+ return (object)$fields;
}
private function assertRevisionRecordMatchesRevision(
/**
* @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
*/
public function testNewRevisionFromRow_anonEdit() {
- $page = $this->getExistingTestPage();
-
+ $page = $this->getTestPage();
$text = __METHOD__ . 'a-ä';
/** @var Revision $rev */
$rev = $page->doEditContent(
/**
* @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
*/
public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
$this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
$this->overrideMwServices();
- $page = $this->getExistingTestPage();
-
+ $page = $this->getTestPage();
$text = __METHOD__ . 'a-ä';
/** @var Revision $rev */
$rev = $page->doEditContent(
/**
* @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
*/
public function testNewRevisionFromRow_userEdit() {
- $page = $this->getExistingTestPage();
-
+ $page = $this->getTestPage();
$text = __METHOD__ . 'b-ä';
/** @var Revision $rev */
$rev = $page->doEditContent(
$store = MediaWikiServices::getInstance()->getRevisionStore();
$title = Title::newFromText( __METHOD__ );
$text = __METHOD__ . '-bä';
- $page = $this->getExistingTestPage( $title );
+ $page = WikiPage::factory( $title );
/** @var Revision $orig */
$orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
->value['revision'];
$store = MediaWikiServices::getInstance()->getRevisionStore();
$title = Title::newFromText( __METHOD__ );
$text = __METHOD__ . '-bä';
- $page = $this->getExistingTestPage( $title );
+ $page = WikiPage::factory( $title );
/** @var Revision $orig */
$orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
->value['revision'];
*/
public function testLoadRevisionFromId() {
$title = Title::newFromText( __METHOD__ );
- $page = $this->getExistingTestPage( $title );
- $rev = $page->getRevision();
+ $page = WikiPage::factory( $title );
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
$store = MediaWikiServices::getInstance()->getRevisionStore();
$result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() );
*/
public function testLoadRevisionFromPageId() {
$title = Title::newFromText( __METHOD__ );
- $page = $this->getExistingTestPage( $title );
- $rev = $page->getRevision();
+ $page = WikiPage::factory( $title );
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
$store = MediaWikiServices::getInstance()->getRevisionStore();
$result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() );
*/
public function testLoadRevisionFromTitle() {
$title = Title::newFromText( __METHOD__ );
- $page = $this->getExistingTestPage( $title );
- $rev = $page->getRevision();
+ $page = WikiPage::factory( $title );
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
$store = MediaWikiServices::getInstance()->getRevisionStore();
$result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title );
* @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTimestamp
*/
public function testLoadRevisionFromTimestamp() {
- $page = $this->getNonexistingTestPage( __METHOD__ );
- $title = $page->getTitle();
+ $title = Title::newFromText( __METHOD__ );
+ $page = WikiPage::factory( $title );
/** @var Revision $revOne */
$revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
->value['revision'];
* @covers \MediaWiki\Storage\RevisionStore::listRevisionSizes
*/
public function testGetParentLengths() {
- $page = $this->getNonexistingTestPage( __METHOD__ );
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
/** @var Revision $revOne */
$revOne = $page->doEditContent(
new WikitextContent( __METHOD__ ), __METHOD__
* @covers \MediaWiki\Storage\RevisionStore::getPreviousRevision
*/
public function testGetPreviousRevision() {
- $page = $this->getNonexistingTestPage( __METHOD__ );
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
/** @var Revision $revOne */
$revOne = $page->doEditContent(
new WikitextContent( __METHOD__ ), __METHOD__
* @covers \MediaWiki\Storage\RevisionStore::getNextRevision
*/
public function testGetNextRevision() {
- $page = $this->getNonexistingTestPage( __METHOD__ );
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
/** @var Revision $revOne */
$revOne = $page->doEditContent(
new WikitextContent( __METHOD__ ), __METHOD__
* @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
*/
public function testGetTimestampFromId_found() {
- $page = $this->getExistingTestPage();
- $rev = $page->getRevision();
+ $page = $this->getTestPage();
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
$store = MediaWikiServices::getInstance()->getRevisionStore();
$result = $store->getTimestampFromId(
* @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
*/
public function testGetTimestampFromId_notFound() {
- $page = $this->getExistingTestPage();
- $rev = $page->getRevision();
+ $page = $this->getTestPage();
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
$store = MediaWikiServices::getInstance()->getRevisionStore();
$result = $store->getTimestampFromId(
*/
public function testCountRevisionsByPageId() {
$store = MediaWikiServices::getInstance()->getRevisionStore();
- $page = $this->getNonexistingTestPage( __METHOD__ );
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
$this->assertSame(
0,
*/
public function testCountRevisionsByTitle() {
$store = MediaWikiServices::getInstance()->getRevisionStore();
- $page = $this->getNonexistingTestPage( __METHOD__ );
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
$this->assertSame(
0,
*/
public function testUserWasLastToEdit_false() {
$sysop = $this->getTestSysop()->getUser();
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
$page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
$store = MediaWikiServices::getInstance()->getRevisionStore();
public function testUserWasLastToEdit_true() {
$startTime = wfTimestampNow();
$sysop = $this->getTestSysop()->getUser();
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
$page->doEditContent(
new WikitextContent( __METHOD__ ),
__METHOD__,
* @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision
*/
public function testGetKnownCurrentRevision() {
- $page = $this->getExistingTestPage();
+ $page = $this->getTestPage();
/** @var Revision $rev */
$rev = $page->doEditContent(
new WikitextContent( __METHOD__ . 'b' ),
}
public function provideNewMutableRevisionFromArray() {
- yield 'Basic array, with page & id' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'text_id' => 2,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 46,
- 'parent_id' => 1,
- 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'comment' => 'Goat Comment!',
- 'content_format' => 'text/x-wiki',
- 'content_model' => 'wikitext',
- ]
- ];
yield 'Basic array, content object' => [
[
'id' => 2,
yield 'Basic array, with title' => [
[
'title' => Title::newFromText( 'SomeText' ),
- 'text_id' => 2,
'timestamp' => '20171017114835',
'user_text' => '111.0.1.2',
'user' => 0,
'parent_id' => 1,
'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
'comment' => 'Goat Comment!',
- 'content_format' => 'text/x-wiki',
- 'content_model' => 'wikitext',
+ 'content' => new WikitextContent( 'Some Content' ),
]
];
yield 'Basic array, no user field' => [
[
'id' => 2,
'page' => 1,
- 'text_id' => 2,
'timestamp' => '20171017114835',
'user_text' => '111.0.1.3',
'minor_edit' => false,
'parent_id' => 1,
'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
'comment' => 'Goat Comment!',
- 'content_format' => 'text/x-wiki',
- 'content_model' => 'wikitext',
+ 'content' => new WikitextContent( 'Some Content' ),
]
];
}
$this->assertSame( $array['sha1'], $result->getSha1() );
$this->assertSame( $array['comment'], $result->getComment()->text );
if ( isset( $array['content'] ) ) {
- $this->assertTrue(
- $result->getSlot( 'main' )->getContent()->equals( $array['content'] )
- );
+ foreach ( $array['content'] as $role => $content ) {
+ $this->assertTrue(
+ $result->getContent( $role )->equals( $content )
+ );
+ }
} elseif ( isset( $array['text'] ) ) {
$this->assertSame( $array['text'], $result->getSlot( 'main' )->getContent()->serialize() );
- } else {
+ } elseif ( isset( $array['content_format'] ) ) {
$this->assertSame(
$array['content_format'],
$result->getSlot( 'main' )->getContent()->getDefaultFormat()
);
}
+ abstract public function provideGetSlotsQueryInfo();
+
+ /**
+ * @dataProvider provideGetSlotsQueryInfo
+ * @covers \MediaWiki\Storage\RevisionStore::getSlotsQueryInfo
+ */
+ public function testGetSlotsQueryInfo( $options, $expected ) {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+ $archiveQueryInfo = $store->getSlotsQueryInfo( $options );
+
+ $this->assertArrayEqualsIgnoringIntKeyOrder(
+ $expected['tables'],
+ $archiveQueryInfo['tables']
+ );
+
+ $this->assertArrayEqualsIgnoringIntKeyOrder(
+ $expected['fields'],
+ $archiveQueryInfo['fields']
+ );
+
+ $this->assertArrayEqualsIgnoringIntKeyOrder(
+ $expected['joins'],
+ $archiveQueryInfo['joins']
+ );
+ }
+
/**
* Assert that the two arrays passed are equal, ignoring the order of the values that integer
* keys.
* @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
*
* @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
*/
public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
$cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
/**
* @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
*/
public function testNewRevisionFromRow_legacyEncoding_ignored() {
$row = [
--- /dev/null
+ALTER TABLE /*_*/revision DROP COLUMN rev_text_id;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_model;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_format;
--- /dev/null
+DROP TABLE /*_*/revision;
+
+CREATE TABLE /*_*/revision (
+ rev_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ rev_page INTEGER NOT NULL,
+ rev_comment BLOB NOT NULL,
+ rev_user INTEGER NOT NULL default 0,
+ rev_user_text varchar(255) NOT NULL default '',
+ rev_timestamp blob(14) NOT NULL default '',
+ rev_minor_edit INTEGER NOT NULL default 0,
+ rev_deleted INTEGER NOT NULL default 0,
+ rev_len INTEGER unsigned,
+ rev_parent_id INTEGER default NULL,
+ rev_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
--- /dev/null
+<?php
+
+use MediaWiki\Tests\Storage\McrSchemaOverride;
+
+/**
+ * Tests WikiPage against the MCR DB schema after schema migration.
+ *
+ * @covers WikiPage
+ *
+ * @group WikiPage
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class WikiPageMcrDbTest extends WikiPageDbTestBase {
+
+ use McrSchemaOverride;
+
+ public function setUp() {
+ parent::setUp();
+ }
+
+ protected function getContentHandlerUseDB() {
+ return true;
+ }
+
+}