From 0ef66de3cfc61557f234d608bd9aa75d0d708337 Mon Sep 17 00:00:00 2001 From: addshore Date: Tue, 17 Apr 2018 08:49:20 +0100 Subject: [PATCH] MCR RevisionStore, multi content mode.. Bug: T174024 Change-Id: Ifabf39e12ba843eb754ad0c029b7d16a311047a5 --- includes/Storage/RevisionStore.php | 720 +++++++++++++----- includes/Storage/SlotRecord.php | 5 - includes/page/WikiPage.php | 6 +- tests/common/TestsAutoLoader.php | 1 + tests/phpunit/MediaWikiTestCase.php | 4 +- tests/phpunit/includes/RevisionDbTestBase.php | 49 +- tests/phpunit/includes/RevisionMcrDbTest.php | 27 + tests/phpunit/includes/RevisionTest.php | 7 +- .../Storage/McrRevisionStoreDbTest.php | 248 ++++++ .../includes/Storage/McrSchemaOverride.php | 59 ++ .../McrWriteBothRevisionStoreDbTest.php | 118 ++- .../NoContentModelRevisionStoreDbTest.php | 77 ++ .../Storage/PreMcrRevisionStoreDbTest.php | 102 +++ .../Storage/RevisionStoreDbTestBase.php | 400 ++++++---- .../includes/Storage/RevisionStoreTest.php | 2 - .../includes/Storage/drop-pre-mcr-fields.sql | 3 + .../Storage/drop-pre-mcr-fields.sqlite.sql | 15 + .../includes/page/WikiPageMcrDbTest.php | 28 + 18 files changed, 1519 insertions(+), 352 deletions(-) create mode 100644 tests/phpunit/includes/RevisionMcrDbTest.php create mode 100644 tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php create mode 100644 tests/phpunit/includes/Storage/McrSchemaOverride.php create mode 100644 tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql create mode 100644 tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql create mode 100644 tests/phpunit/includes/page/WikiPageMcrDbTest.php diff --git a/includes/Storage/RevisionStore.php b/includes/Storage/RevisionStore.php index b691288625..6c30d62e7a 100644 --- a/includes/Storage/RevisionStore.php +++ b/includes/Storage/RevisionStore.php @@ -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 + } 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 * diff --git a/includes/Storage/SlotRecord.php b/includes/Storage/SlotRecord.php index e63dd3c355..dff4b031d4 100644 --- a/includes/Storage/SlotRecord.php +++ b/includes/Storage/SlotRecord.php @@ -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', diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 65b3428df6..24a70595a1 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -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 diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index f457a0657a..88c541e533 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -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", diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index b6c569cf6b..e2f9834ca5 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -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.' diff --git a/tests/phpunit/includes/RevisionDbTestBase.php b/tests/phpunit/includes/RevisionDbTestBase.php index 1ab78f41f5..73050e0d3b 100644 --- a/tests/phpunit/includes/RevisionDbTestBase.php +++ b/tests/phpunit/includes/RevisionDbTestBase.php @@ -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 index 0000000000..3c30efe32d --- /dev/null +++ b/tests/phpunit/includes/RevisionMcrDbTest.php @@ -0,0 +1,27 @@ + 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 index 0000000000..5bf49d39b5 --- /dev/null +++ b/tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php @@ -0,0 +1,248 @@ +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 index 0000000000..d2f58bfa33 --- /dev/null +++ b/tests/phpunit/includes/Storage/McrSchemaOverride.php @@ -0,0 +1,59 @@ + [], + '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; + } + +} diff --git a/tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php index 81c98c04eb..c98414258a 100644 --- a/tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php @@ -1,8 +1,11 @@ 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', + ] + ]; + } + } diff --git a/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php index c77a94a2c9..2337805733 100644 --- a/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php @@ -1,6 +1,8 @@ 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!', + ] + ]; + } + } diff --git a/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php index 4336691185..a27d2bb29e 100644 --- a/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php +++ b/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php @@ -1,6 +1,10 @@ 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', + ] + ]; + } + } diff --git a/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php index 7ea0627e7b..763a3e763d 100644 --- a/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php +++ b/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php @@ -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. diff --git a/tests/phpunit/includes/Storage/RevisionStoreTest.php b/tests/phpunit/includes/Storage/RevisionStoreTest.php index a877f875c3..727697cfde 100644 --- a/tests/phpunit/includes/Storage/RevisionStoreTest.php +++ b/tests/phpunit/includes/Storage/RevisionStoreTest.php @@ -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 index 0000000000..ddfe756f8e --- /dev/null +++ b/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql @@ -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 index 0000000000..ce7a61861d --- /dev/null +++ b/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql @@ -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 index 0000000000..02567f8184 --- /dev/null +++ b/tests/phpunit/includes/page/WikiPageMcrDbTest.php @@ -0,0 +1,28 @@ +