MCR RevisionStore, multi content mode..
authoraddshore <addshorewiki@gmail.com>
Tue, 17 Apr 2018 07:49:20 +0000 (08:49 +0100)
committerdaniel <daniel.kinzler@wikimedia.de>
Thu, 14 Jun 2018 15:30:33 +0000 (17:30 +0200)
Bug: T174024
Change-Id: Ifabf39e12ba843eb754ad0c029b7d16a311047a5

18 files changed:
includes/Storage/RevisionStore.php
includes/Storage/SlotRecord.php
includes/page/WikiPage.php
tests/common/TestsAutoLoader.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionMcrDbTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/McrSchemaOverride.php [new file with mode: 0644]
tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php
tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php
tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php
tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php
tests/phpunit/includes/Storage/RevisionStoreTest.php
tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql [new file with mode: 0644]
tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql [new file with mode: 0644]
tests/phpunit/includes/page/WikiPageMcrDbTest.php [new file with mode: 0644]

index b691288..6c30d62 100644 (file)
@@ -151,10 +151,6 @@ class RevisionStore
                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;
@@ -365,15 +361,32 @@ class RevisionStore
                // 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
@@ -382,46 +395,145 @@ class RevisionStore
                        ? $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
@@ -432,158 +544,218 @@ class RevisionStore
                // 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,
+                               ]
+                       )
+               );
        }
 
        /**
@@ -630,7 +802,7 @@ class RevisionStore
         * @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();
@@ -868,6 +1040,12 @@ class RevisionStore
                $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 );
@@ -963,6 +1141,9 @@ class RevisionStore
                }
 
                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 )
                        {
@@ -986,8 +1167,6 @@ class RevisionStore
                                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 );
        }
 
@@ -1182,6 +1361,86 @@ class RevisionStore
                );
        }
 
+       /**
+        * @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)
@@ -1248,14 +1507,13 @@ class RevisionStore
                        // 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
         *
@@ -1264,10 +1522,8 @@ class RevisionStore
         * @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 ) {
@@ -1299,27 +1555,11 @@ class RevisionStore
                        // 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.
@@ -1360,14 +1600,22 @@ class RevisionStore
 
                // 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"
                                );
                        }
                }
@@ -1392,11 +1640,17 @@ class RevisionStore
                        }
                }
 
-               $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;
        }
@@ -1745,7 +1999,7 @@ class RevisionStore
 
        /**
         * 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
         *
@@ -1757,7 +2011,9 @@ class RevisionStore
         * @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()`
@@ -1827,6 +2083,8 @@ class RevisionStore
                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';
@@ -1842,7 +2100,81 @@ class RevisionStore
 
        /**
         * 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
         *
index e63dd3c..dff4b03 100644 (file)
@@ -234,11 +234,6 @@ class SlotRecord {
                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',
index 65b3428..24a7059 100644 (file)
@@ -2555,13 +2555,17 @@ class WikiPage implements Page, IDBAccessObject {
                                         * 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
index f457a06..88c541e 100644 (file)
@@ -150,6 +150,7 @@ $wgAutoloadClasses += [
 
        # 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",
index b6c569c..e2f9834 100644 (file)
@@ -1387,8 +1387,10 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
        /**
         * @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.'
index 1ab78f4..73050e0 100644 (file)
@@ -266,7 +266,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        [
                                'rev_id',
                                'rev_page',
-                               'rev_text_id',
                                'rev_minor_edit',
                                'rev_deleted',
                                'rev_len',
@@ -277,7 +276,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        [ [
                                strval( $rev->getId() ),
                                strval( $this->testPage->getId() ),
-                               strval( $textId ),
                                '0',
                                '0',
                                '13',
@@ -287,19 +285,52 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                );
        }
 
+       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 ) );
        }
@@ -922,7 +953,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $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.
diff --git a/tests/phpunit/includes/RevisionMcrDbTest.php b/tests/phpunit/includes/RevisionMcrDbTest.php
new file mode 100644 (file)
index 0000000..3c30efe
--- /dev/null
@@ -0,0 +1,27 @@
+<?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;
+       }
+
+}
index 761548c..cf2c64a 100644 (file)
@@ -159,16 +159,15 @@ class RevisionTest extends MediaWikiTestCase {
                                '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',
diff --git a/tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php
new file mode 100644 (file)
index 0000000..5bf49d3
--- /dev/null
@@ -0,0 +1,248 @@
+<?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' ] ],
+                               ],
+                       ]
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/Storage/McrSchemaOverride.php b/tests/phpunit/includes/Storage/McrSchemaOverride.php
new file mode 100644 (file)
index 0000000..d2f58bf
--- /dev/null
@@ -0,0 +1,59 @@
+<?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;
+       }
+
+}
index 81c98c0..c984142 100644 (file)
@@ -1,8 +1,11 @@
 <?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.
@@ -18,18 +21,32 @@ class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase {
 
        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 );
        }
 
        /**
@@ -82,9 +99,9 @@ class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase {
                        ]
                ];
                yield [
-                       [ 'page', 'user', 'text' ],
+                       [ 'page', 'user' ],
                        [
-                               'tables' => [ 'revision', 'page', 'user', 'text' ],
+                               'tables' => [ 'revision', 'page', 'user' ],
                                'fields' => array_merge(
                                        $this->getDefaultQueryFields(),
                                        $this->getCommentQueryFields(),
@@ -98,17 +115,102 @@ class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase {
                                                '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',
+                       ]
+               ];
+       }
+
 }
index c77a94a..2337805 100644 (file)
@@ -1,6 +1,8 @@
 <?php
 namespace MediaWiki\Tests\Storage;
 
+use Revision;
+
 /**
  * Tests RevisionStore against the pre-MCR, pre-ContentHandler DB schema.
  *
@@ -19,6 +21,14 @@ class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase {
                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 [
                        [
@@ -111,4 +121,71 @@ class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase {
                ];
        }
 
+       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!',
+                       ]
+               ];
+       }
+
 }
index 4336691..a27d2bb 100644 (file)
@@ -1,6 +1,10 @@
 <?php
 namespace MediaWiki\Tests\Storage;
 
+use InvalidArgumentException;
+use Revision;
+use WikitextContent;
+
 /**
  * Tests RevisionStore against the pre-MCR DB schema.
  *
@@ -15,6 +19,16 @@ class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase {
 
        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 [
                        [
@@ -81,4 +95,92 @@ class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase {
                ];
        }
 
+       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',
+                       ]
+               ];
+       }
+
 }
index 7ea0627..763a3e7 100644 (file)
@@ -3,6 +3,7 @@
 namespace MediaWiki\Tests\Storage;
 
 use CommentStoreComment;
+use Content;
 use Exception;
 use HashBagOStuff;
 use InvalidArgumentException;
@@ -36,6 +37,16 @@ use WikitextContent;
  */
 abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
+       /**
+        * @var Title
+        */
+       private $testPageTitle;
+
+       /**
+        * @var WikiPage
+        */
+       private $testPage;
+
        /**
         * @return int
         */
@@ -83,6 +94,46 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                // 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
         */
@@ -201,14 +252,26 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        }
 
        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() );
@@ -241,6 +304,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        }
 
        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 ) );
                }
@@ -249,6 +316,8 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        private function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) {
                $this->assertTrue( $slot->hasAddress() );
                $this->assertSame( $r->getId(), $slot->getRevision() );
+
+               $this->assertInstanceOf( Content::class, $slot->getContent() );
        }
 
        /**
@@ -256,21 +325,20 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         *
         * @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;
@@ -283,22 +351,26 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                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,
@@ -312,7 +384,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                ];
        }
 
-       private function getRandomCommentStoreComment() {
+       protected function getRandomCommentStoreComment() {
                return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) );
        }
 
@@ -323,25 +395,53 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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']
                );
        }
 
@@ -358,8 +458,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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,
@@ -372,14 +471,14 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $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 );
@@ -399,26 +498,23 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
        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(),
@@ -427,7 +523,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        new IncompleteRevisionException( 'timestamp field must not be NULL!' )
                ];
                yield 'no comment' => [
-                       Title::newFromText( 'UTPage' ),
                        [
                                'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
                                'timestamp' => '20171117010101',
@@ -436,7 +531,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        new IncompleteRevisionException( 'comment must not be NULL!' )
                ];
                yield 'no user' => [
-                       Title::newFromText( 'UTPage' ),
                        [
                                'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
                                'comment' => $this->getRandomCommentStoreComment(),
@@ -451,13 +545,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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();
 
@@ -471,12 +562,14 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
        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,
                ];
@@ -487,16 +580,28 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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,
@@ -510,14 +615,21 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $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 );
+               }
        }
 
        /**
@@ -539,7 +651,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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'];
@@ -550,7 +662,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
                $this->assertGreaterThan( 0, $result );
                $this->assertSame(
-                       $page->getRevision()->getRecentChange()->getAttribute( 'rc_id' ),
+                       $store->getRecentChange( $revisionRecord )->getAttribute( 'rc_id' ),
                        $result
                );
        }
@@ -561,7 +673,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        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__,
@@ -583,7 +695,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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 */
@@ -601,7 +713,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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 */
@@ -619,7 +731,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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 */
@@ -637,7 +749,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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 */
@@ -658,7 +770,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                // 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__ );
@@ -676,13 +788,13 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $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(),
@@ -691,19 +803,40 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        '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(
@@ -756,11 +889,9 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
        /**
         * @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(
@@ -780,13 +911,11 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
        /**
         * @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(
@@ -806,11 +935,9 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
        /**
         * @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(
@@ -838,7 +965,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $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'];
@@ -869,7 +996,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $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'];
@@ -948,8 +1075,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         */
        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() );
@@ -961,8 +1090,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         */
        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() );
@@ -974,8 +1105,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         */
        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 );
@@ -986,8 +1119,8 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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'];
@@ -1023,7 +1156,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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__
@@ -1059,7 +1192,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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__
@@ -1083,7 +1216,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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__
@@ -1107,8 +1240,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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(
@@ -1123,8 +1258,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @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(
@@ -1140,7 +1277,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         */
        public function testCountRevisionsByPageId() {
                $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $page = $this->getNonexistingTestPage( __METHOD__ );
+               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
 
                $this->assertSame(
                        0,
@@ -1163,7 +1300,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         */
        public function testCountRevisionsByTitle() {
                $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $page = $this->getNonexistingTestPage( __METHOD__ );
+               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
 
                $this->assertSame(
                        0,
@@ -1186,7 +1323,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         */
        public function testUserWasLastToEdit_false() {
                $sysop = $this->getTestSysop()->getUser();
-               $page = $this->getExistingTestPage();
+               $page = $this->getTestPage();
                $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
 
                $store = MediaWikiServices::getInstance()->getRevisionStore();
@@ -1205,7 +1342,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        public function testUserWasLastToEdit_true() {
                $startTime = wfTimestampNow();
                $sysop = $this->getTestSysop()->getUser();
-               $page = $this->getExistingTestPage();
+               $page = $this->getTestPage();
                $page->doEditContent(
                        new WikitextContent( __METHOD__ ),
                        __METHOD__,
@@ -1228,7 +1365,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision
         */
        public function testGetKnownCurrentRevision() {
-               $page = $this->getExistingTestPage();
+               $page = $this->getTestPage();
                /** @var Revision $rev */
                $rev = $page->doEditContent(
                        new WikitextContent( __METHOD__ . 'b' ),
@@ -1248,24 +1385,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        }
 
        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,
@@ -1318,7 +1437,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                yield 'Basic array, with title' => [
                        [
                                'title' => Title::newFromText( 'SomeText' ),
-                               'text_id' => 2,
                                'timestamp' => '20171017114835',
                                'user_text' => '111.0.1.2',
                                'user' => 0,
@@ -1328,15 +1446,13 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                                '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,
@@ -1345,8 +1461,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                                'parent_id' => 1,
                                'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
                                'comment' => 'Goat Comment!',
-                               'content_format' => 'text/x-wiki',
-                               'content_model' => 'wikitext',
+                               'content' => new WikitextContent( 'Some Content' ),
                        ]
                ];
        }
@@ -1388,12 +1503,14 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $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()
@@ -1540,6 +1657,33 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                );
        }
 
+       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.
index a877f87..727697c 100644 (file)
@@ -403,7 +403,6 @@ class RevisionStoreTest extends MediaWikiTestCase {
         * @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() ] );
@@ -425,7 +424,6 @@ class RevisionStoreTest extends MediaWikiTestCase {
 
        /**
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
         */
        public function testNewRevisionFromRow_legacyEncoding_ignored() {
                $row = [
diff --git a/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql b/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql
new file mode 100644 (file)
index 0000000..ddfe756
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TABLE /*_*/revision DROP COLUMN rev_text_id;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_model;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_format;
diff --git a/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql b/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql
new file mode 100644 (file)
index 0000000..ce7a618
--- /dev/null
@@ -0,0 +1,15 @@
+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*/;
diff --git a/tests/phpunit/includes/page/WikiPageMcrDbTest.php b/tests/phpunit/includes/page/WikiPageMcrDbTest.php
new file mode 100644 (file)
index 0000000..02567f8
--- /dev/null
@@ -0,0 +1,28 @@
+<?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;
+       }
+
+}