Merge "[MCR] Fix SqlBlobStore using DB_REPLICA for writes"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 19 Dec 2017 15:06:02 +0000 (15:06 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 19 Dec 2017 15:06:03 +0000 (15:06 +0000)
14 files changed:
RELEASE-NOTES-1.31
docs/hooks.txt
includes/Revision.php
includes/ServiceWiring.php
includes/actions/HistoryAction.php
includes/cache/MessageCache.php
includes/page/WikiPage.php
includes/parser/Parser.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/specials/SpecialNewpages.php
resources/src/mediawiki.language/specialcharacters.json
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionTest.php

index 67026f4..1a1a9f7 100644 (file)
@@ -123,9 +123,6 @@ changes to languages because of Phabricator reports.
 * The Block class will no longer accept usable-but-missing usernames for
   'byText' or ->setBlocker(). Callers should either ensure the blocker exists
   locally or use a new interwiki-format username like "iw>Example".
-* The RevisionInsertComplete hook is now deprecated, use RevisionRecordInserted instead.
-  RevisionInsertComplete is still called, but the second and third parameter will always be null.
-  Hard deprecation is scheduled for 1.32.
 * The following methods that get and set ParserOutput state are deprecated.
   Callers should use the new stateless $options parameter to
   ParserOutput::getText() instead.
index 1f4a5f4..ee38ea9 100644 (file)
@@ -2810,14 +2810,14 @@ called after the addition of 'qunit' and MediaWiki testing resources.
   added to any module.
 &$ResourceLoader: object
 
-'RevisionRecordInserted': Called after a revision is inserted into the database.
-$revisionRecord: the RevisionRecord that has just been inserted.
-
-'RevisionInsertComplete': DEPRECATED! Use RevisionRecordInserted hook instead.
-Called after a revision is inserted into the database.
-$revision: the Revision
-$data: DEPRECATED! Always null!
-$flags: DEPRECATED! Always null!
+'RevisionInsertComplete': Called after a revision is inserted into the database.
+&$revision: the Revision
+$data: the data stored in old_text.  The meaning depends on $flags: if external
+  is set, it's the URL of the revision text in external storage; otherwise,
+  it's the revision text itself.  In either case, if gzip is set, the revision
+  text is gzipped.
+$flags: a comma-delimited list of strings representing the options used.  May
+  include: utf8 (this will always be set for new revisions); gzip; external.
 
 'SearchableNamespaces': An option to modify which namespaces are searchable.
 &$arr: Array of namespaces ($nsId => $name) which will be used.
index ea73a61..25c89c2 100644 (file)
  * @file
  */
 
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWiki\User\UserIdentityValue;
+use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
@@ -35,50 +28,78 @@ use Wikimedia\Rdbms\ResultWrapper;
 use Wikimedia\Rdbms\FakeResultWrapper;
 
 /**
- * @deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead.
+ * @todo document
  */
 class Revision implements IDBAccessObject {
-
-       /** @var RevisionRecord */
-       protected $mRecord;
+       /** @var int|null */
+       protected $mId;
+       /** @var int|null */
+       protected $mPage;
+       /** @var string */
+       protected $mUserText;
+       /** @var string */
+       protected $mOrigUserText;
+       /** @var int */
+       protected $mUser;
+       /** @var bool */
+       protected $mMinorEdit;
+       /** @var string */
+       protected $mTimestamp;
+       /** @var int */
+       protected $mDeleted;
+       /** @var int */
+       protected $mSize;
+       /** @var string */
+       protected $mSha1;
+       /** @var int */
+       protected $mParentId;
+       /** @var string */
+       protected $mComment;
+       /** @var string */
+       protected $mText;
+       /** @var int */
+       protected $mTextId;
+       /** @var int */
+       protected $mUnpatrolled;
+
+       /** @var stdClass|null */
+       protected $mTextRow;
+
+       /**  @var null|Title */
+       protected $mTitle;
+       /** @var bool */
+       protected $mCurrent;
+       /** @var string */
+       protected $mContentModel;
+       /** @var string */
+       protected $mContentFormat;
+
+       /** @var Content|null|bool */
+       protected $mContent;
+       /** @var null|ContentHandler */
+       protected $mContentHandler;
+
+       /** @var int */
+       protected $mQueryFlags = 0;
+       /** @var bool Used for cached values to reload user text and rev_deleted */
+       protected $mRefreshMutableFields = false;
+       /** @var string Wiki ID; false means the current wiki */
+       protected $mWiki = false;
 
        // Revision deletion constants
-       const DELETED_TEXT = RevisionRecord::DELETED_TEXT;
-       const DELETED_COMMENT = RevisionRecord::DELETED_COMMENT;
-       const DELETED_USER = RevisionRecord::DELETED_USER;
-       const DELETED_RESTRICTED = RevisionRecord::DELETED_RESTRICTED;
-       const SUPPRESSED_USER = RevisionRecord::SUPPRESSED_USER;
-       const SUPPRESSED_ALL = RevisionRecord::SUPPRESSED_ALL;
+       const DELETED_TEXT = 1;
+       const DELETED_COMMENT = 2;
+       const DELETED_USER = 4;
+       const DELETED_RESTRICTED = 8;
+       const SUPPRESSED_USER = 12; // convenience
+       const SUPPRESSED_ALL = 15; // convenience
 
        // Audience options for accessors
-       const FOR_PUBLIC = RevisionRecord::FOR_PUBLIC;
-       const FOR_THIS_USER = RevisionRecord::FOR_THIS_USER;
-       const RAW = RevisionRecord::RAW;
-
-       const TEXT_CACHE_GROUP = SqlBlobStore::TEXT_CACHE_GROUP;
+       const FOR_PUBLIC = 1;
+       const FOR_THIS_USER = 2;
+       const RAW = 3;
 
-       /**
-        * @return RevisionStore
-        */
-       protected static function getRevisionStore() {
-               return MediaWikiServices::getInstance()->getRevisionStore();
-       }
-
-       /**
-        * @return SqlBlobStore
-        */
-       protected static function getBlobStore() {
-               $store = MediaWikiServices::getInstance()->getBlobStore();
-
-               if ( !$store instanceof SqlBlobStore ) {
-                       throw new RuntimeException(
-                               'The backwards compatibility code in Revision currently requires the BlobStore '
-                               . 'service to be an SqlBlobStore instance, but it is a ' . get_class( $store )
-                       );
-               }
-
-               return $store;
-       }
+       const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count
 
        /**
         * Load a page revision from a given revision ID number.
@@ -93,8 +114,7 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public static function newFromId( $id, $flags = 0 ) {
-               $rec = self::getRevisionStore()->getRevisionById( $id, $flags );
-               return $rec === null ? null : new Revision( $rec, $flags );
+               return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
        }
 
        /**
@@ -112,8 +132,20 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
-               $rec = self::getRevisionStore()->getRevisionByTitle( $linkTarget, $id, $flags );
-               return $rec === null ? null : new Revision( $rec, $flags );
+               $conds = [
+                       'page_namespace' => $linkTarget->getNamespace(),
+                       'page_title' => $linkTarget->getDBkey()
+               ];
+               if ( $id ) {
+                       // Use the specified ID
+                       $conds['rev_id'] = $id;
+                       return self::newFromConds( $conds, $flags );
+               } else {
+                       // Use a join to get the latest revision
+                       $conds[] = 'rev_id=page_latest';
+                       $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+                       return self::loadFromConds( $db, $conds, $flags );
+               }
        }
 
        /**
@@ -131,13 +163,22 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
-               $rec = self::getRevisionStore()->getRevisionByPageId( $pageId, $revId, $flags );
-               return $rec === null ? null : new Revision( $rec, $flags );
+               $conds = [ 'page_id' => $pageId ];
+               if ( $revId ) {
+                       $conds['rev_id'] = $revId;
+                       return self::newFromConds( $conds, $flags );
+               } else {
+                       // Use a join to get the latest revision
+                       $conds[] = 'rev_id = page_latest';
+                       $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+                       return self::loadFromConds( $db, $conds, $flags );
+               }
        }
 
        /**
         * Make a fake revision object from an archive table row. This is queried
         * for permissions or even inserted (as in Special:Undelete)
+        * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
         *
         * @param object $row
         * @param array $overrides
@@ -146,45 +187,68 @@ class Revision implements IDBAccessObject {
         * @return Revision
         */
        public static function newFromArchiveRow( $row, $overrides = [] ) {
-               $rec = self::getRevisionStore()->newRevisionFromArchiveRow( $row, 0, null, $overrides );
-               return new Revision( $rec );
+               global $wgContentHandlerUseDB;
+
+               $attribs = $overrides + [
+                       'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
+                       'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
+                       'comment'    => CommentStore::newKey( 'ar_comment' )
+                               // Legacy because $row may have come from self::selectArchiveFields()
+                               ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text,
+                       'user'       => $row->ar_user,
+                       'user_text'  => $row->ar_user_text,
+                       'timestamp'  => $row->ar_timestamp,
+                       'minor_edit' => $row->ar_minor_edit,
+                       'text_id'    => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
+                       'deleted'    => $row->ar_deleted,
+                       'len'        => $row->ar_len,
+                       'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
+                       'content_model'   => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
+                       'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
+               ];
+
+               if ( !$wgContentHandlerUseDB ) {
+                       unset( $attribs['content_model'] );
+                       unset( $attribs['content_format'] );
+               }
+
+               if ( !isset( $attribs['title'] )
+                       && isset( $row->ar_namespace )
+                       && isset( $row->ar_title )
+               ) {
+                       $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+               }
+
+               if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
+                       // Pre-1.5 ar_text row
+                       $attribs['text'] = self::getRevisionText( $row, 'ar_' );
+                       if ( $attribs['text'] === false ) {
+                               throw new MWException( 'Unable to load text from archive row (possibly T24624)' );
+                       }
+               }
+               return new self( $attribs );
        }
 
        /**
         * @since 1.19
         *
-        * MCR migration note: replaced by RevisionStore::newRevisionFromRow(). Note that
-        * newFromRow() also accepts arrays, while newRevisionFromRow() does not. Instead,
-        * a MutableRevisionRecord should be constructed directly. RevisionStore::newRevisionFromArray()
-        * can be used as a temporary replacement, but should be avoided.
-        *
-        * @param object|array $row
+        * @param object $row
         * @return Revision
         */
        public static function newFromRow( $row ) {
-               if ( is_array( $row ) ) {
-                       $rec = self::getRevisionStore()->newMutableRevisionFromArray( $row );
-               } else {
-                       $rec = self::getRevisionStore()->newRevisionFromRow( $row );
-               }
-
-               return new Revision( $rec );
+               return new self( $row );
        }
 
        /**
         * Load a page revision from a given revision ID number.
         * Returns null if no such revision can be found.
         *
-        * @deprecated since 1.31, use RevisionStore::getRevisionById() instead.
-        *
         * @param IDatabase $db
         * @param int $id
         * @return Revision|null
         */
        public static function loadFromId( $db, $id ) {
-               wfDeprecated( __METHOD__, '1.31' ); // no known callers
-               $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id );
-               return $rec === null ? null : new Revision( $rec );
+               return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
        }
 
        /**
@@ -192,16 +256,19 @@ class Revision implements IDBAccessObject {
         * that's attached to a given page. If not attached
         * to that page, will return null.
         *
-        * @deprecated since 1.31, use RevisionStore::getRevisionByPageId() instead.
-        *
         * @param IDatabase $db
         * @param int $pageid
         * @param int $id
         * @return Revision|null
         */
        public static function loadFromPageId( $db, $pageid, $id = 0 ) {
-               $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id );
-               return $rec === null ? null : new Revision( $rec );
+               $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
+               if ( $id ) {
+                       $conds['rev_id'] = intval( $id );
+               } else {
+                       $conds[] = 'rev_id=page_latest';
+               }
+               return self::loadFromConds( $db, $conds );
        }
 
        /**
@@ -209,16 +276,24 @@ class Revision implements IDBAccessObject {
         * that's attached to a given page. If not attached
         * to that page, will return null.
         *
-        * @deprecated since 1.31, use RevisionStore::getRevisionByTitle() instead.
-        *
         * @param IDatabase $db
         * @param Title $title
         * @param int $id
         * @return Revision|null
         */
        public static function loadFromTitle( $db, $title, $id = 0 ) {
-               $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id );
-               return $rec === null ? null : new Revision( $rec );
+               if ( $id ) {
+                       $matchId = intval( $id );
+               } else {
+                       $matchId = 'page_latest';
+               }
+               return self::loadFromConds( $db,
+                       [
+                               "rev_id=$matchId",
+                               'page_namespace' => $title->getNamespace(),
+                               'page_title' => $title->getDBkey()
+                       ]
+               );
        }
 
        /**
@@ -226,17 +301,73 @@ class Revision implements IDBAccessObject {
         * WARNING: Timestamps may in some circumstances not be unique,
         * so this isn't the best key to use.
         *
-        * @deprecated since 1.31, use RevisionStore::loadRevisionFromTimestamp() instead.
-        *
         * @param IDatabase $db
         * @param Title $title
         * @param string $timestamp
         * @return Revision|null
         */
        public static function loadFromTimestamp( $db, $title, $timestamp ) {
-               // XXX: replace loadRevisionFromTimestamp by getRevisionByTimestamp?
-               $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp );
-               return $rec === null ? null : new Revision( $rec );
+               return self::loadFromConds( $db,
+                       [
+                               'rev_timestamp' => $db->timestamp( $timestamp ),
+                               'page_namespace' => $title->getNamespace(),
+                               'page_title' => $title->getDBkey()
+                       ]
+               );
+       }
+
+       /**
+        * Given a set of conditions, fetch a revision
+        *
+        * This method is used then a revision ID is qualified and
+        * will incorporate some basic replica DB/master fallback logic
+        *
+        * @param array $conditions
+        * @param int $flags (optional)
+        * @return Revision|null
+        */
+       private static function newFromConds( $conditions, $flags = 0 ) {
+               $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+
+               $rev = self::loadFromConds( $db, $conditions, $flags );
+               // Make sure new pending/committed revision are visibile later on
+               // within web requests to certain avoid bugs like T93866 and T94407.
+               if ( !$rev
+                       && !( $flags & self::READ_LATEST )
+                       && wfGetLB()->getServerCount() > 1
+                       && wfGetLB()->hasOrMadeRecentMasterChanges()
+               ) {
+                       $flags = self::READ_LATEST;
+                       $db = wfGetDB( DB_MASTER );
+                       $rev = self::loadFromConds( $db, $conditions, $flags );
+               }
+
+               if ( $rev ) {
+                       $rev->mQueryFlags = $flags;
+               }
+
+               return $rev;
+       }
+
+       /**
+        * Given a set of conditions, fetch a revision from
+        * the given database connection.
+        *
+        * @param IDatabase $db
+        * @param array $conditions
+        * @param int $flags (optional)
+        * @return Revision|null
+        */
+       private static function loadFromConds( $db, $conditions, $flags = 0 ) {
+               $row = self::fetchFromConds( $db, $conditions, $flags );
+               if ( $row ) {
+                       $rev = new Revision( $row );
+                       $rev->mWiki = $db->getDomainID();
+
+                       return $rev;
+               }
+
+               return null;
        }
 
        /**
@@ -246,18 +377,52 @@ class Revision implements IDBAccessObject {
         *
         * @param LinkTarget $title
         * @return ResultWrapper
-        * @deprecated Since 1.28, no callers in core nor in known extensions. No-op since 1.31.
+        * @deprecated Since 1.28
         */
        public static function fetchRevision( LinkTarget $title ) {
-               wfDeprecated( __METHOD__, '1.31' );
-               return new FakeResultWrapper( [] );
+               $row = self::fetchFromConds(
+                       wfGetDB( DB_REPLICA ),
+                       [
+                               'rev_id=page_latest',
+                               'page_namespace' => $title->getNamespace(),
+                               'page_title' => $title->getDBkey()
+                       ]
+               );
+
+               return new FakeResultWrapper( $row ? [ $row ] : [] );
+       }
+
+       /**
+        * Given a set of conditions, return a ResultWrapper
+        * which will return matching database rows with the
+        * fields necessary to build Revision objects.
+        *
+        * @param IDatabase $db
+        * @param array $conditions
+        * @param int $flags (optional)
+        * @return stdClass
+        */
+       private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
+               $revQuery = self::getQueryInfo( [ 'page', 'user' ] );
+               $options = [];
+               if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
+                       $options[] = 'FOR UPDATE';
+               }
+               return $db->selectRow(
+                       $revQuery['tables'],
+                       $revQuery['fields'],
+                       $conditions,
+                       __METHOD__,
+                       $options,
+                       $revQuery['joins']
+               );
        }
 
        /**
         * Return the value of a select() JOIN conds array for the user table.
         * This will get user table rows for logged-in users.
         * @since 1.19
-        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
+        * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
         * @return array
         */
        public static function userJoinCond() {
@@ -269,7 +434,7 @@ class Revision implements IDBAccessObject {
         * Return the value of a select() page conds array for the page table.
         * This will assure that the revision(s) are not orphaned from live pages.
         * @since 1.19
-        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
+        * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
         * @return array
         */
        public static function pageJoinCond() {
@@ -280,7 +445,7 @@ class Revision implements IDBAccessObject {
        /**
         * Return the list of revision fields that should be selected to create
         * a new revision.
-        * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
+        * @deprecated since 1.31, use self::getQueryInfo() instead.
         * @return array
         */
        public static function selectFields() {
@@ -315,7 +480,7 @@ class Revision implements IDBAccessObject {
        /**
         * Return the list of revision fields that should be selected to create
         * a new revision from an archive row.
-        * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
+        * @deprecated since 1.31, use self::getArchiveQueryInfo() instead.
         * @return array
         */
        public static function selectArchiveFields() {
@@ -351,7 +516,7 @@ class Revision implements IDBAccessObject {
        /**
         * Return the list of text fields that should be selected to read the
         * revision text
-        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'text' ] ) instead.
+        * @deprecated since 1.31, use self::getQueryInfo( [ 'text' ] ) instead.
         * @return array
         */
        public static function selectTextFields() {
@@ -364,7 +529,7 @@ class Revision implements IDBAccessObject {
 
        /**
         * Return the list of page fields that should be selected from page table
-        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
+        * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
         * @return array
         */
        public static function selectPageFields() {
@@ -381,7 +546,7 @@ class Revision implements IDBAccessObject {
 
        /**
         * Return the list of user fields that should be selected from user table
-        * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
+        * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
         * @return array
         */
        public static function selectUserFields() {
@@ -393,7 +558,6 @@ class Revision implements IDBAccessObject {
         * Return the tables, fields, and join conditions to be selected to create
         * a new revision object.
         * @since 1.31
-        * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
         * @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
@@ -404,21 +568,104 @@ class Revision implements IDBAccessObject {
         *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
         */
        public static function getQueryInfo( $options = [] ) {
-               return self::getRevisionStore()->getQueryInfo( $options );
+               global $wgContentHandlerUseDB;
+
+               $commentQuery = CommentStore::newKey( 'rev_comment' )->getJoin();
+               $ret = [
+                       'tables' => [ 'revision' ] + $commentQuery['tables'],
+                       'fields' => [
+                               'rev_id',
+                               'rev_page',
+                               'rev_text_id',
+                               'rev_timestamp',
+                               'rev_user_text',
+                               'rev_user',
+                               'rev_minor_edit',
+                               'rev_deleted',
+                               'rev_len',
+                               'rev_parent_id',
+                               'rev_sha1',
+                       ] + $commentQuery['fields'],
+                       'joins' => $commentQuery['joins'],
+               ];
+
+               if ( $wgContentHandlerUseDB ) {
+                       $ret['fields'][] = 'rev_content_format';
+                       $ret['fields'][] = 'rev_content_model';
+               }
+
+               if ( in_array( 'page', $options, true ) ) {
+                       $ret['tables'][] = 'page';
+                       $ret['fields'] = array_merge( $ret['fields'], [
+                               'page_namespace',
+                               'page_title',
+                               'page_id',
+                               'page_latest',
+                               'page_is_redirect',
+                               'page_len',
+                       ] );
+                       $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
+               }
+
+               if ( in_array( 'user', $options, true ) ) {
+                       $ret['tables'][] = 'user';
+                       $ret['fields'] = array_merge( $ret['fields'], [
+                               'user_name',
+                       ] );
+                       $ret['joins']['user'] = [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
+               }
+
+               if ( in_array( 'text', $options, true ) ) {
+                       $ret['tables'][] = 'text';
+                       $ret['fields'] = array_merge( $ret['fields'], [
+                               'old_text',
+                               'old_flags'
+                       ] );
+                       $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
+               }
+
+               return $ret;
        }
 
        /**
         * Return the tables, fields, and join conditions to be selected to create
         * a new archived revision object.
         * @since 1.31
-        * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
         * @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 static function getArchiveQueryInfo() {
-               return self::getRevisionStore()->getArchiveQueryInfo();
+               global $wgContentHandlerUseDB;
+
+               $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
+               $ret = [
+                       'tables' => [ 'archive' ] + $commentQuery['tables'],
+                       'fields' => [
+                               'ar_id',
+                               'ar_page_id',
+                               'ar_rev_id',
+                               'ar_text',
+                               'ar_text_id',
+                               'ar_timestamp',
+                               'ar_user_text',
+                               'ar_user',
+                               'ar_minor_edit',
+                               'ar_deleted',
+                               'ar_len',
+                               'ar_parent_id',
+                               'ar_sha1',
+                       ] + $commentQuery['fields'],
+                       'joins' => $commentQuery['joins'],
+               ];
+
+               if ( $wgContentHandlerUseDB ) {
+                       $ret['fields'][] = 'ar_content_format';
+                       $ret['fields'][] = 'ar_content_model';
+               }
+
+               return $ret;
        }
 
        /**
@@ -428,49 +675,203 @@ class Revision implements IDBAccessObject {
         * @return array
         */
        public static function getParentLengths( $db, array $revIds ) {
-               return self::getRevisionStore()->listRevisionSizes( $db, $revIds );
+               $revLens = [];
+               if ( !$revIds ) {
+                       return $revLens; // empty
+               }
+               $res = $db->select( 'revision',
+                       [ 'rev_id', 'rev_len' ],
+                       [ 'rev_id' => $revIds ],
+                       __METHOD__ );
+               foreach ( $res as $row ) {
+                       $revLens[$row->rev_id] = $row->rev_len;
+               }
+               return $revLens;
        }
 
        /**
-        * @param object|array|RevisionRecord $row Either a database row or an array
-        * @param int $queryFlags
-        * @param Title|null $title
-        *
+        * @param object|array $row Either a database row or an array
+        * @throws MWException
         * @access private
         */
-       function __construct( $row, $queryFlags = 0, Title $title = null ) {
-               global $wgUser;
-
-               if ( $row instanceof RevisionRecord ) {
-                       $this->mRecord = $row;
+       public function __construct( $row ) {
+               if ( is_object( $row ) ) {
+                       $this->constructFromDbRowObject( $row );
                } elseif ( is_array( $row ) ) {
-                       if ( !isset( $row['user'] ) && !isset( $row['user_text'] ) ) {
-                               $row['user'] = $wgUser;
-                       }
+                       $this->constructFromRowArray( $row );
+               } else {
+                       throw new MWException( 'Revision constructor passed invalid row format.' );
+               }
+               $this->mUnpatrolled = null;
+       }
 
-                       $this->mRecord = self::getRevisionStore()->newMutableRevisionFromArray(
-                               $row,
-                               $queryFlags,
-                               $title
-                       );
-               } elseif ( is_object( $row ) ) {
-                       $this->mRecord = self::getRevisionStore()->newRevisionFromRow(
-                               $row,
-                               $queryFlags,
-                               $title
-                       );
+       /**
+        * @param object $row
+        */
+       private function constructFromDbRowObject( $row ) {
+               $this->mId = intval( $row->rev_id );
+               $this->mPage = intval( $row->rev_page );
+               $this->mTextId = intval( $row->rev_text_id );
+               $this->mComment = CommentStore::newKey( 'rev_comment' )
+                       // Legacy because $row may have come from self::selectFields()
+                       ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
+               $this->mUser = intval( $row->rev_user );
+               $this->mMinorEdit = intval( $row->rev_minor_edit );
+               $this->mTimestamp = $row->rev_timestamp;
+               $this->mDeleted = intval( $row->rev_deleted );
+
+               if ( !isset( $row->rev_parent_id ) ) {
+                       $this->mParentId = null;
                } else {
-                       throw new InvalidArgumentException(
-                               '$row must be a row object, an associative array, or a RevisionRecord'
-                       );
+                       $this->mParentId = intval( $row->rev_parent_id );
                }
+
+               if ( !isset( $row->rev_len ) ) {
+                       $this->mSize = null;
+               } else {
+                       $this->mSize = intval( $row->rev_len );
+               }
+
+               if ( !isset( $row->rev_sha1 ) ) {
+                       $this->mSha1 = null;
+               } else {
+                       $this->mSha1 = $row->rev_sha1;
+               }
+
+               if ( isset( $row->page_latest ) ) {
+                       $this->mCurrent = ( $row->rev_id == $row->page_latest );
+                       $this->mTitle = Title::newFromRow( $row );
+               } else {
+                       $this->mCurrent = false;
+                       $this->mTitle = null;
+               }
+
+               if ( !isset( $row->rev_content_model ) ) {
+                       $this->mContentModel = null; # determine on demand if needed
+               } else {
+                       $this->mContentModel = strval( $row->rev_content_model );
+               }
+
+               if ( !isset( $row->rev_content_format ) ) {
+                       $this->mContentFormat = null; # determine on demand if needed
+               } else {
+                       $this->mContentFormat = strval( $row->rev_content_format );
+               }
+
+               // Lazy extraction...
+               $this->mText = null;
+               if ( isset( $row->old_text ) ) {
+                       $this->mTextRow = $row;
+               } else {
+                       // 'text' table row entry will be lazy-loaded
+                       $this->mTextRow = null;
+               }
+
+               // Use user_name for users and rev_user_text for IPs...
+               $this->mUserText = null; // lazy load if left null
+               if ( $this->mUser == 0 ) {
+                       $this->mUserText = $row->rev_user_text; // IP user
+               } elseif ( isset( $row->user_name ) ) {
+                       $this->mUserText = $row->user_name; // logged-in user
+               }
+               $this->mOrigUserText = $row->rev_user_text;
        }
 
        /**
-        * @return RevisionRecord
+        * @param array $row
+        *
+        * @throws MWException
         */
-       public function getRevisionRecord() {
-               return $this->mRecord;
+       private function constructFromRowArray( array $row ) {
+               // Build a new revision to be saved...
+               global $wgUser; // ugh
+
+               # if we have a content object, use it to set the model and type
+               if ( !empty( $row['content'] ) ) {
+                       if ( !( $row['content'] instanceof Content ) ) {
+                               throw new MWException( '`content` field must contain a Content object.' );
+                       }
+
+                       // @todo when is that set? test with external store setup! check out insertOn() [dk]
+                       if ( !empty( $row['text_id'] ) ) {
+                               throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
+                                       "can't serialize content object" );
+                       }
+
+                       $row['content_model'] = $row['content']->getModel();
+                       # note: mContentFormat is initializes later accordingly
+                       # note: content is serialized later in this method!
+                       # also set text to null?
+               }
+
+               $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
+               $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
+               $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
+               $this->mUserText = isset( $row['user_text'] )
+                       ? strval( $row['user_text'] ) : $wgUser->getName();
+               $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
+               $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
+               $this->mTimestamp = isset( $row['timestamp'] )
+                       ? strval( $row['timestamp'] ) : wfTimestampNow();
+               $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
+               $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
+               $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
+               $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
+
+               $this->mContentModel = isset( $row['content_model'] )
+                       ? strval( $row['content_model'] ) : null;
+               $this->mContentFormat = isset( $row['content_format'] )
+                       ? strval( $row['content_format'] ) : null;
+
+               // Enforce spacing trimming on supplied text
+               $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
+               $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
+               $this->mTextRow = null;
+
+               $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
+
+               // if we have a Content object, override mText and mContentModel
+               if ( !empty( $row['content'] ) ) {
+                       $handler = $this->getContentHandler();
+                       $this->mContent = $row['content'];
+
+                       $this->mContentModel = $this->mContent->getModel();
+                       $this->mContentHandler = null;
+
+                       $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
+               } elseif ( $this->mText !== null ) {
+                       $handler = $this->getContentHandler();
+                       $this->mContent = $handler->unserializeContent( $this->mText );
+               }
+
+               // If we have a Title object, make sure it is consistent with mPage.
+               if ( $this->mTitle && $this->mTitle->exists() ) {
+                       if ( $this->mPage === null ) {
+                               // if the page ID wasn't known, set it now
+                               $this->mPage = $this->mTitle->getArticleID();
+                       } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
+                               // Got different page IDs. This may be legit (e.g. during undeletion),
+                               // but it seems worth mentioning it in the log.
+                               wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
+                                       $this->mTitle->getArticleID() . " provided by the Title object." );
+                       }
+               }
+
+               $this->mCurrent = false;
+
+               // If we still have no length, see it we have the text to figure it out
+               if ( !$this->mSize && $this->mContent !== null ) {
+                       $this->mSize = $this->mContent->getSize();
+               }
+
+               // Same for sha1
+               if ( $this->mSha1 === null ) {
+                       $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
+               }
+
+               // force lazy init
+               $this->getContentModel();
+               $this->getContentFormat();
        }
 
        /**
@@ -479,27 +880,19 @@ class Revision implements IDBAccessObject {
         * @return int|null
         */
        public function getId() {
-               return $this->mRecord->getId();
+               return $this->mId;
        }
 
        /**
         * Set the revision ID
         *
-        * This should only be used for proposed revisions that turn out to be null edits.
-        *
-        * @note Only supported on Revisions that were constructed based on associative arrays,
-        *       since they are mutable.
+        * This should only be used for proposed revisions that turn out to be null edits
         *
         * @since 1.19
-        * @param int|string $id
-        * @throws MWException
+        * @param int $id
         */
        public function setId( $id ) {
-               if ( $this->mRecord instanceof MutableRevisionRecord ) {
-                       $this->mRecord->setId( intval( $id ) );
-               } else {
-                       throw new MWException( __METHOD__ . ' is not supported on this instance' );
-               }
+               $this->mId = (int)$id;
        }
 
        /**
@@ -507,107 +900,106 @@ class Revision implements IDBAccessObject {
         *
         * This should only be used for proposed revisions that turn out to be null edits
         *
-        * @note Only supported on Revisions that were constructed based on associative arrays,
-        *       since they are mutable.
-        *
         * @since 1.28
         * @deprecated since 1.31, please reuse old Revision object
         * @param int $id User ID
         * @param string $name User name
-        * @throws MWException
         */
        public function setUserIdAndName( $id, $name ) {
-               if ( $this->mRecord instanceof MutableRevisionRecord ) {
-                       $user = new UserIdentityValue( intval( $id ), $name );
-                       $this->mRecord->setUser( $user );
-               } else {
-                       throw new MWException( __METHOD__ . ' is not supported on this instance' );
-               }
+               $this->mUser = (int)$id;
+               $this->mUserText = $name;
+               $this->mOrigUserText = $name;
        }
 
        /**
-        * @return SlotRecord
-        */
-       private function getMainSlotRaw() {
-               return $this->mRecord->getSlot( 'main', RevisionRecord::RAW );
-       }
-
-       /**
-        * Get the ID of the row of the text table that contains the content of the
-        * revision's main slot, if that content is stored in the text table.
-        *
-        * If the content is stored elsewhere, this returns null.
-        *
-        * @deprecated since 1.31, use RevisionRecord()->getSlot()->getContentAddress() to
-        * get that actual address that can be used with BlobStore::getBlob(); or use
-        * RevisionRecord::hasSameContent() to check if two revisions have the same content.
+        * Get text row ID
         *
         * @return int|null
         */
        public function getTextId() {
-               $slot = $this->getMainSlotRaw();
-               return $slot->hasAddress()
-                       ? self::getBlobStore()->getTextIdFromAddress( $slot->getAddress() )
-                       : null;
+               return $this->mTextId;
        }
 
        /**
         * Get parent revision ID (the original previous page revision)
         *
-        * @return int|null The ID of the parent revision. 0 indicates that there is no
-        * parent revision. Null indicates that the parent revision is not known.
+        * @return int|null
         */
        public function getParentId() {
-               return $this->mRecord->getParentId();
+               return $this->mParentId;
        }
 
        /**
         * Returns the length of the text in this revision, or null if unknown.
         *
-        * @return int
+        * @return int|null
         */
        public function getSize() {
-               return $this->mRecord->getSize();
+               return $this->mSize;
        }
 
        /**
-        * Returns the base36 sha1 of the content in this revision, or null if unknown.
+        * Returns the base36 sha1 of the text in this revision, or null if unknown.
         *
-        * @return string
+        * @return string|null
         */
        public function getSha1() {
-               // XXX: we may want to drop all the hashing logic, it's not worth the overhead.
-               return $this->mRecord->getSha1();
+               return $this->mSha1;
        }
 
        /**
-        * Returns the title of the page associated with this entry.
-        * Since 1.31, this will never return null.
+        * Returns the title of the page associated with this entry or null.
         *
         * Will do a query, when title is not set and id is given.
         *
-        * @return Title
+        * @return Title|null
         */
        public function getTitle() {
-               $linkTarget = $this->mRecord->getPageAsLinkTarget();
-               return Title::newFromLinkTarget( $linkTarget );
+               if ( $this->mTitle !== null ) {
+                       return $this->mTitle;
+               }
+               // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
+               if ( $this->mId !== null ) {
+                       $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
+                       // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
+                       $row = $dbr->selectRow(
+                               [ 'revision', 'page' ],
+                               [
+                                       'page_namespace',
+                                       'page_title',
+                                       'page_id',
+                                       'page_latest',
+                                       'page_is_redirect',
+                                       'page_len',
+                               ],
+                               [ 'rev_id' => $this->mId ],
+                               __METHOD__,
+                               [],
+                               [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
+                       );
+                       if ( $row ) {
+                               // @TODO: better foreign title handling
+                               $this->mTitle = Title::newFromRow( $row );
+                       }
+               }
+
+               if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
+                       // Loading by ID is best, though not possible for foreign titles
+                       if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
+                               $this->mTitle = Title::newFromID( $this->mPage );
+                       }
+               }
+
+               return $this->mTitle;
        }
 
        /**
         * Set the title of the revision
         *
-        * @deprecated: since 1.31, this is now a noop. Pass the Title to the constructor instead.
-        *
         * @param Title $title
         */
        public function setTitle( $title ) {
-               if ( !$title->equals( $this->getTitle() ) ) {
-                       throw new InvalidArgumentException(
-                               $title->getPrefixedText()
-                                       . ' is not the same as '
-                                       . $this->mRecord->getPageAsLinkTarget()->__toString()
-                       );
-               }
+               $this->mTitle = $title;
        }
 
        /**
@@ -616,7 +1008,7 @@ class Revision implements IDBAccessObject {
         * @return int|null
         */
        public function getPage() {
-               return $this->mRecord->getPageId();
+               return $this->mPage;
        }
 
        /**
@@ -633,14 +1025,13 @@ class Revision implements IDBAccessObject {
         * @return int
         */
        public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
-               global $wgUser;
-
-               if ( $audience === self::FOR_THIS_USER && !$user ) {
-                       $user = $wgUser;
+               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
+                       return 0;
+               } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
+                       return 0;
+               } else {
+                       return $this->mUser;
                }
-
-               $user = $this->mRecord->getUser( $audience, $user );
-               return $user ? $user->getId() : 0;
        }
 
        /**
@@ -668,14 +1059,23 @@ class Revision implements IDBAccessObject {
         * @return string
         */
        public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
-               global $wgUser;
+               $this->loadMutableFields();
 
-               if ( $audience === self::FOR_THIS_USER && !$user ) {
-                       $user = $wgUser;
+               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
+                       return '';
+               } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
+                       return '';
+               } else {
+                       if ( $this->mUserText === null ) {
+                               $this->mUserText = User::whoIs( $this->mUser ); // load on demand
+                               if ( $this->mUserText === false ) {
+                                       # This shouldn't happen, but it can if the wiki was recovered
+                                       # via importing revs and there is no user table entry yet.
+                                       $this->mUserText = $this->mOrigUserText;
+                               }
+                       }
+                       return $this->mUserText;
                }
-
-               $user = $this->mRecord->getUser( $audience, $user );
-               return $user ? $user->getName() : '';
        }
 
        /**
@@ -703,14 +1103,13 @@ class Revision implements IDBAccessObject {
         * @return string
         */
        function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
-               global $wgUser;
-
-               if ( $audience === self::FOR_THIS_USER && !$user ) {
-                       $user = $wgUser;
+               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
+                       return '';
+               } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
+                       return '';
+               } else {
+                       return $this->mComment;
                }
-
-               $comment = $this->mRecord->getComment( $audience, $user );
-               return $comment === null ? null : $comment->text;
        }
 
        /**
@@ -728,14 +1127,23 @@ class Revision implements IDBAccessObject {
         * @return bool
         */
        public function isMinor() {
-               return $this->mRecord->isMinor();
+               return (bool)$this->mMinorEdit;
        }
 
        /**
         * @return int Rcid of the unpatrolled row, zero if there isn't one
         */
        public function isUnpatrolled() {
-               return self::getRevisionStore()->isUnpatrolled( $this->mRecord );
+               if ( $this->mUnpatrolled !== null ) {
+                       return $this->mUnpatrolled;
+               }
+               $rc = $this->getRecentChange();
+               if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
+                       $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
+               } else {
+                       $this->mUnpatrolled = 0;
+               }
+               return $this->mUnpatrolled;
        }
 
        /**
@@ -748,7 +1156,19 @@ class Revision implements IDBAccessObject {
         * @return RecentChange|null
         */
        public function getRecentChange( $flags = 0 ) {
-               return self::getRevisionStore()->getRecentChange( $this->mRecord, $flags );
+               $dbr = wfGetDB( DB_REPLICA );
+
+               list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
+
+               return RecentChange::newFromConds(
+                       [
+                               'rc_user_text' => $this->getUserText( self::RAW ),
+                               'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
+                               'rc_this_oldid' => $this->getId()
+                       ],
+                       __METHOD__,
+                       $dbType
+               );
        }
 
        /**
@@ -757,7 +1177,14 @@ class Revision implements IDBAccessObject {
         * @return bool
         */
        public function isDeleted( $field ) {
-               return $this->mRecord->isDeleted( $field );
+               if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
+                       // Current revisions of pages cannot have the content hidden. Skipping this
+                       // check is very useful for Parser as it fetches templates using newKnownCurrent().
+                       // Calling getVisibility() in that case triggers a verification database query.
+                       return false; // no need to check
+               }
+
+               return ( $this->getVisibility() & $field ) == $field;
        }
 
        /**
@@ -766,17 +1193,19 @@ class Revision implements IDBAccessObject {
         * @return int
         */
        public function getVisibility() {
-               return $this->mRecord->getVisibility();
+               $this->loadMutableFields();
+
+               return (int)$this->mDeleted;
        }
 
        /**
         * Fetch revision content if it's available to the specified audience.
         * If the specified audience does not have the ability to view this
-        * revision, or the content could not be loaded, null will be returned.
+        * revision, null will be returned.
         *
         * @param int $audience One of:
         *   Revision::FOR_PUBLIC       to be displayed to all users
-        *   Revision::FOR_THIS_USER    to be displayed to $user
+        *   Revision::FOR_THIS_USER    to be displayed to $wgUser
         *   Revision::RAW              get the text regardless of permissions
         * @param User $user User object to check for, only if FOR_THIS_USER is passed
         *   to the $audience parameter
@@ -784,17 +1213,12 @@ class Revision implements IDBAccessObject {
         * @return Content|null
         */
        public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
-               global $wgUser;
-
-               if ( $audience === self::FOR_THIS_USER && !$user ) {
-                       $user = $wgUser;
-               }
-
-               try {
-                       return $this->mRecord->getContent( 'main', $audience, $user );
-               }
-               catch ( RevisionAccessException $e ) {
+               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
+                       return null;
+               } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
                        return null;
+               } else {
+                       return $this->getContentInternal();
                }
        }
 
@@ -802,51 +1226,86 @@ class Revision implements IDBAccessObject {
         * Get original serialized data (without checking view restrictions)
         *
         * @since 1.21
-        * @deprecated since 1.31, use BlobStore::getBlob instead.
-        *
         * @return string
         */
        public function getSerializedData() {
-               $slot = $this->getMainSlotRaw();
-               return $slot->getContent()->serialize();
+               if ( $this->mText === null ) {
+                       // Revision is immutable. Load on demand.
+                       $this->mText = $this->loadText();
+               }
+
+               return $this->mText;
        }
 
        /**
-        * Returns the content model for the main slot of this revision.
+        * Gets the content object for the revision (or null on failure).
+        *
+        * Note that for mutable Content objects, each call to this method will return a
+        * fresh clone.
+        *
+        * @since 1.21
+        * @return Content|null The Revision's content, or null on failure.
+        */
+       protected function getContentInternal() {
+               if ( $this->mContent === null ) {
+                       $text = $this->getSerializedData();
+
+                       if ( $text !== null && $text !== false ) {
+                               // Unserialize content
+                               $handler = $this->getContentHandler();
+                               $format = $this->getContentFormat();
+
+                               $this->mContent = $handler->unserializeContent( $text, $format );
+                       }
+               }
+
+               // NOTE: copy() will return $this for immutable content objects
+               return $this->mContent ? $this->mContent->copy() : null;
+       }
+
+       /**
+        * Returns the content model for this revision.
         *
         * If no content model was stored in the database, the default content model for the title is
         * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
         * is used as a last resort.
         *
-        * @todo: drop this, with MCR, there no longer is a single model associated with a revision.
-        *
         * @return string The content model id associated with this revision,
         *     see the CONTENT_MODEL_XXX constants.
         */
        public function getContentModel() {
-               return $this->getMainSlotRaw()->getModel();
+               if ( !$this->mContentModel ) {
+                       $title = $this->getTitle();
+                       if ( $title ) {
+                               $this->mContentModel = ContentHandler::getDefaultModelFor( $title );
+                       } else {
+                               $this->mContentModel = CONTENT_MODEL_WIKITEXT;
+                       }
+
+                       assert( !empty( $this->mContentModel ) );
+               }
+
+               return $this->mContentModel;
        }
 
        /**
-        * Returns the content format for the main slot of this revision.
+        * Returns the content format for this revision.
         *
         * If no content format was stored in the database, the default format for this
         * revision's content model is returned.
         *
-        * @todo: drop this, the format is irrelevant to the revision!
-        *
         * @return string The content format id associated with this revision,
         *     see the CONTENT_FORMAT_XXX constants.
         */
        public function getContentFormat() {
-               $format = $this->getMainSlotRaw()->getFormat();
+               if ( !$this->mContentFormat ) {
+                       $handler = $this->getContentHandler();
+                       $this->mContentFormat = $handler->getDefaultFormat();
 
-               if ( $format === null ) {
-                       // if no format was stored along with the blob, fall back to default format
-                       $format = $this->getContentHandler()->getDefaultFormat();
+                       assert( !empty( $this->mContentFormat ) );
                }
 
-               return $format;
+               return $this->mContentFormat;
        }
 
        /**
@@ -856,21 +1315,33 @@ class Revision implements IDBAccessObject {
         * @return ContentHandler
         */
        public function getContentHandler() {
-               return ContentHandler::getForModelID( $this->getContentModel() );
+               if ( !$this->mContentHandler ) {
+                       $model = $this->getContentModel();
+                       $this->mContentHandler = ContentHandler::getForModelID( $model );
+
+                       $format = $this->getContentFormat();
+
+                       if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
+                               throw new MWException( "Oops, the content format $format is not supported for "
+                                       . "this content model, $model" );
+                       }
+               }
+
+               return $this->mContentHandler;
        }
 
        /**
         * @return string
         */
        public function getTimestamp() {
-               return $this->mRecord->getTimestamp();
+               return wfTimestamp( TS_MW, $this->mTimestamp );
        }
 
        /**
         * @return bool
         */
        public function isCurrent() {
-               return ( $this->mRecord instanceof RevisionStoreRecord ) && $this->mRecord->isCurrent();
+               return $this->mCurrent;
        }
 
        /**
@@ -879,8 +1350,13 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public function getPrevious() {
-               $rec = self::getRevisionStore()->getPreviousRevision( $this->mRecord );
-               return $rec === null ? null : new Revision( $rec );
+               if ( $this->getTitle() ) {
+                       $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
+                       if ( $prev ) {
+                               return self::newFromTitle( $this->getTitle(), $prev );
+                       }
+               }
+               return null;
        }
 
        /**
@@ -889,8 +1365,38 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public function getNext() {
-               $rec = self::getRevisionStore()->getNextRevision( $this->mRecord );
-               return $rec === null ? null : new Revision( $rec );
+               if ( $this->getTitle() ) {
+                       $next = $this->getTitle()->getNextRevisionID( $this->getId() );
+                       if ( $next ) {
+                               return self::newFromTitle( $this->getTitle(), $next );
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Get previous revision Id for this page_id
+        * This is used to populate rev_parent_id on save
+        *
+        * @param IDatabase $db
+        * @return int
+        */
+       private function getPreviousRevisionId( $db ) {
+               if ( $this->mPage === null ) {
+                       return 0;
+               }
+               # Use page_latest if ID is not given
+               if ( !$this->mId ) {
+                       $prevId = $db->selectField( 'page', 'page_latest',
+                               [ 'page_id' => $this->mPage ],
+                               __METHOD__ );
+               } else {
+                       $prevId = $db->selectField( 'revision', 'rev_id',
+                               [ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
+                               __METHOD__,
+                               [ 'ORDER BY' => 'rev_id DESC' ] );
+               }
+               return intval( $prevId );
        }
 
        /**
@@ -923,9 +1429,35 @@ class Revision implements IDBAccessObject {
                        return false;
                }
 
-               $cacheKey = isset( $row->old_id ) ? ( 'tt:' . $row->old_id ) : null;
+               // Use external methods for external objects, text in table is URL-only then
+               if ( in_array( 'external', $flags ) ) {
+                       $url = $text;
+                       $parts = explode( '://', $url, 2 );
+                       if ( count( $parts ) == 1 || $parts[1] == '' ) {
+                               return false;
+                       }
+
+                       if ( isset( $row->old_id ) && $wiki === false ) {
+                               // Make use of the wiki-local revision text cache
+                               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+                               // The cached value should be decompressed, so handle that and return here
+                               return $cache->getWithSetCallback(
+                                       $cache->makeKey( 'revisiontext', 'textid', $row->old_id ),
+                                       self::getCacheTTL( $cache ),
+                                       function () use ( $url, $wiki, $flags ) {
+                                               // No negative caching per Revision::loadText()
+                                               $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
+
+                                               return self::decompressRevisionText( $text, $flags );
+                                       },
+                                       [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
+                               );
+                       } else {
+                               $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
+                       }
+               }
 
-               return self::getBlobStore()->expandBlob( $text, $flags, $cacheKey );
+               return self::decompressRevisionText( $text, $flags );
        }
 
        /**
@@ -939,7 +1471,28 @@ class Revision implements IDBAccessObject {
         * @return string
         */
        public static function compressRevisionText( &$text ) {
-               return self::getBlobStore()->compressData( $text );
+               global $wgCompressRevisions;
+               $flags = [];
+
+               # Revisions not marked this way will be converted
+               # on load if $wgLegacyCharset is set in the future.
+               $flags[] = 'utf-8';
+
+               if ( $wgCompressRevisions ) {
+                       if ( function_exists( 'gzdeflate' ) ) {
+                               $deflated = gzdeflate( $text );
+
+                               if ( $deflated === false ) {
+                                       wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
+                               } else {
+                                       $text = $deflated;
+                                       $flags[] = 'gzip';
+                               }
+                       } else {
+                               wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
+                       }
+               }
+               return implode( ',', $flags );
        }
 
        /**
@@ -950,7 +1503,46 @@ class Revision implements IDBAccessObject {
         * @return string|bool Decompressed text, or false on failure
         */
        public static function decompressRevisionText( $text, $flags ) {
-               return self::getBlobStore()->decompressData( $text, $flags );
+               global $wgLegacyEncoding, $wgContLang;
+
+               if ( $text === false ) {
+                       // Text failed to be fetched; nothing to do
+                       return false;
+               }
+
+               if ( in_array( 'gzip', $flags ) ) {
+                       # Deal with optional compression of archived pages.
+                       # This can be done periodically via maintenance/compressOld.php, and
+                       # as pages are saved if $wgCompressRevisions is set.
+                       $text = gzinflate( $text );
+
+                       if ( $text === false ) {
+                               wfLogWarning( __METHOD__ . ': gzinflate() failed' );
+                               return false;
+                       }
+               }
+
+               if ( in_array( 'object', $flags ) ) {
+                       # Generic compressed storage
+                       $obj = unserialize( $text );
+                       if ( !is_object( $obj ) ) {
+                               // Invalid object
+                               return false;
+                       }
+                       $text = $obj->getText();
+               }
+
+               if ( $text !== false && $wgLegacyEncoding
+                       && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
+               ) {
+                       # Old revisions kept around in a legacy encoding?
+                       # Upconvert on demand.
+                       # ("utf8" checked for compatibility with some broken
+                       #  conversion scripts 2008-12-30)
+                       $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
+               }
+
+               return $text;
        }
 
        /**
@@ -962,27 +1554,192 @@ class Revision implements IDBAccessObject {
         * @return int The revision ID
         */
        public function insertOn( $dbw ) {
-               global $wgUser;
+               global $wgDefaultExternalStore, $wgContentHandlerUseDB;
+
+               // We're inserting a new revision, so we have to use master anyway.
+               // If it's a null revision, it may have references to rows that
+               // are not in the replica yet (the text row).
+               $this->mQueryFlags |= self::READ_LATEST;
+
+               // Not allowed to have rev_page equal to 0, false, etc.
+               if ( !$this->mPage ) {
+                       $title = $this->getTitle();
+                       if ( $title instanceof Title ) {
+                               $titleText = ' for page ' . $title->getPrefixedText();
+                       } else {
+                               $titleText = '';
+                       }
+                       throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
+               }
 
-               // Note that $this->mRecord->getId() will typically return null here, but not always,
-               // e.g. not when restoring a revision.
+               $this->checkContentModel();
 
-               if ( $this->mRecord->getUser( RevisionRecord::RAW ) === null ) {
-                       if ( $this->mRecord instanceof MutableRevisionRecord ) {
-                               $this->mRecord->setUser( $wgUser );
-                       } else {
-                               throw new MWException( 'Cannot insert revision with no associated user.' );
+               $data = $this->mText;
+               $flags = self::compressRevisionText( $data );
+
+               # Write to external storage if required
+               if ( $wgDefaultExternalStore ) {
+                       // Store and get the URL
+                       $data = ExternalStore::insertToDefault( $data );
+                       if ( !$data ) {
+                               throw new MWException( "Unable to store text to external storage" );
+                       }
+                       if ( $flags ) {
+                               $flags .= ',';
+                       }
+                       $flags .= 'external';
+               }
+
+               # Record the text (or external storage URL) to the text table
+               if ( $this->mTextId === null ) {
+                       $dbw->insert( 'text',
+                               [
+                                       'old_text' => $data,
+                                       'old_flags' => $flags,
+                               ], __METHOD__
+                       );
+                       $this->mTextId = $dbw->insertId();
+               }
+
+               if ( $this->mComment === null ) {
+                       $this->mComment = "";
+               }
+
+               # Record the edit in revisions
+               $row = [
+                       'rev_page'       => $this->mPage,
+                       'rev_text_id'    => $this->mTextId,
+                       'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
+                       'rev_user'       => $this->mUser,
+                       'rev_user_text'  => $this->mUserText,
+                       'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
+                       'rev_deleted'    => $this->mDeleted,
+                       'rev_len'        => $this->mSize,
+                       'rev_parent_id'  => $this->mParentId === null
+                               ? $this->getPreviousRevisionId( $dbw )
+                               : $this->mParentId,
+                       'rev_sha1'       => $this->mSha1 === null
+                               ? self::base36Sha1( $this->mText )
+                               : $this->mSha1,
+               ];
+               if ( $this->mId !== null ) {
+                       $row['rev_id'] = $this->mId;
+               }
+
+               list( $commentFields, $commentCallback ) =
+                       CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $this->mComment );
+               $row += $commentFields;
+
+               if ( $wgContentHandlerUseDB ) {
+                       // NOTE: Store null for the default model and format, to save space.
+                       // XXX: Makes the DB sensitive to changed defaults.
+                       // Make this behavior optional? Only in miser mode?
+
+                       $model = $this->getContentModel();
+                       $format = $this->getContentFormat();
+
+                       $title = $this->getTitle();
+
+                       if ( $title === null ) {
+                               throw new MWException( "Insufficient information to determine the title of the "
+                                       . "revision's page!" );
                        }
+
+                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
+                       $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+                       $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
+                       $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
                }
 
-               $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw );
+               $dbw->insert( 'revision', $row, __METHOD__ );
 
-               $this->mRecord = $rec;
+               if ( $this->mId === null ) {
+                       // Only if auto-increment was used
+                       $this->mId = $dbw->insertId();
+               }
+               $commentCallback( $this->mId );
 
-               // TODO: hard-deprecate in 1.32 (or even 1.31?)
-               Hooks::run( 'RevisionInsertComplete', [ $this, null, null ] );
+               // Assertion to try to catch T92046
+               if ( (int)$this->mId === 0 ) {
+                       throw new UnexpectedValueException(
+                               'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
+                                       var_export( $row, 1 )
+                       );
+               }
 
-               return $rec->getId();
+               // Insert IP revision into ip_changes for use when querying for a range.
+               if ( $this->mUser === 0 && IP::isValid( $this->mUserText ) ) {
+                       $ipcRow = [
+                               'ipc_rev_id'        => $this->mId,
+                               'ipc_rev_timestamp' => $row['rev_timestamp'],
+                               'ipc_hex'           => IP::toHex( $row['rev_user_text'] ),
+                       ];
+                       $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
+               }
+
+               // Avoid PHP 7.1 warning of passing $this by reference
+               $revision = $this;
+               Hooks::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] );
+
+               return $this->mId;
+       }
+
+       protected function checkContentModel() {
+               global $wgContentHandlerUseDB;
+
+               // Note: may return null for revisions that have not yet been inserted
+               $title = $this->getTitle();
+
+               $model = $this->getContentModel();
+               $format = $this->getContentFormat();
+               $handler = $this->getContentHandler();
+
+               if ( !$handler->isSupportedFormat( $format ) ) {
+                       $t = $title->getPrefixedDBkey();
+
+                       throw new MWException( "Can't use format $format with content model $model on $t" );
+               }
+
+               if ( !$wgContentHandlerUseDB && $title ) {
+                       // if $wgContentHandlerUseDB is not set,
+                       // all revisions must use the default content model and format.
+
+                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
+                       $defaultHandler = ContentHandler::getForModelID( $defaultModel );
+                       $defaultFormat = $defaultHandler->getDefaultFormat();
+
+                       if ( $this->getContentModel() != $defaultModel ) {
+                               $t = $title->getPrefixedDBkey();
+
+                               throw new MWException( "Can't save non-default content model with "
+                                       . "\$wgContentHandlerUseDB disabled: model is $model, "
+                                       . "default for $t is $defaultModel" );
+                       }
+
+                       if ( $this->getContentFormat() != $defaultFormat ) {
+                               $t = $title->getPrefixedDBkey();
+
+                               throw new MWException( "Can't use non-default content format with "
+                                       . "\$wgContentHandlerUseDB disabled: format is $format, "
+                                       . "default for $t is $defaultFormat" );
+                       }
+               }
+
+               $content = $this->getContent( self::RAW );
+               $prefixedDBkey = $title->getPrefixedDBkey();
+               $revId = $this->mId;
+
+               if ( !$content ) {
+                       throw new MWException(
+                               "Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
+                       );
+               }
+               if ( !$content->isValid() ) {
+                       throw new MWException(
+                               "Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
+                       );
+               }
        }
 
        /**
@@ -991,7 +1748,103 @@ class Revision implements IDBAccessObject {
         * @return string
         */
        public static function base36Sha1( $text ) {
-               return SlotRecord::base36Sha1( $text );
+               return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
+       }
+
+       /**
+        * Get the text cache TTL
+        *
+        * @param WANObjectCache $cache
+        * @return int
+        */
+       private static function getCacheTTL( WANObjectCache $cache ) {
+               global $wgRevisionCacheExpiry;
+
+               if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) {
+                       // Do not cache RDBMs blobs in...the RDBMs store
+                       $ttl = $cache::TTL_UNCACHEABLE;
+               } else {
+                       $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE;
+               }
+
+               return $ttl;
+       }
+
+       /**
+        * Lazy-load the revision's text.
+        * Currently hardcoded to the 'text' table storage engine.
+        *
+        * @return string|bool The revision's text, or false on failure
+        */
+       private function loadText() {
+               $cache = ObjectCache::getMainWANInstance();
+
+               // No negative caching; negative hits on text rows may be due to corrupted replica DBs
+               return $cache->getWithSetCallback(
+                       $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
+                       self::getCacheTTL( $cache ),
+                       function () {
+                               return $this->fetchText();
+                       },
+                       [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
+               );
+       }
+
+       private function fetchText() {
+               $textId = $this->getTextId();
+
+               // If we kept data for lazy extraction, use it now...
+               if ( $this->mTextRow !== null ) {
+                       $row = $this->mTextRow;
+                       $this->mTextRow = null;
+               } else {
+                       $row = null;
+               }
+
+               // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
+               // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
+               $flags = $this->mQueryFlags;
+               $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
+                       ? self::READ_LATEST_IMMUTABLE
+                       : 0;
+
+               list( $index, $options, $fallbackIndex, $fallbackOptions ) =
+                       DBAccessObjectUtils::getDBOptions( $flags );
+
+               if ( !$row ) {
+                       // Text data is immutable; check replica DBs first.
+                       $row = wfGetDB( $index )->selectRow(
+                               'text',
+                               [ 'old_text', 'old_flags' ],
+                               [ 'old_id' => $textId ],
+                               __METHOD__,
+                               $options
+                       );
+               }
+
+               // Fallback to DB_MASTER in some cases if the row was not found
+               if ( !$row && $fallbackIndex !== null ) {
+                       // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
+                       // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
+                       $row = wfGetDB( $fallbackIndex )->selectRow(
+                               'text',
+                               [ 'old_text', 'old_flags' ],
+                               [ 'old_id' => $textId ],
+                               __METHOD__,
+                               $fallbackOptions
+                       );
+               }
+
+               if ( !$row ) {
+                       wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
+               }
+
+               $text = self::getRevisionText( $row );
+               if ( $row && $text === false ) {
+                       wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
+               }
+
+               return is_string( $text ) ? $text : false;
        }
 
        /**
@@ -1010,17 +1863,58 @@ class Revision implements IDBAccessObject {
         * @return Revision|null Revision or null on error
         */
        public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
-               global $wgUser;
-               if ( !$user ) {
-                       $user = $wgUser;
+               global $wgContentHandlerUseDB;
+
+               $fields = [ 'page_latest', 'page_namespace', 'page_title',
+                                               'rev_text_id', 'rev_len', 'rev_sha1' ];
+
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'rev_content_model';
+                       $fields[] = 'rev_content_format';
                }
 
-               $comment = CommentStoreComment::newUnsavedComment( $summary, null );
+               $current = $dbw->selectRow(
+                       [ 'page', 'revision' ],
+                       $fields,
+                       [
+                               'page_id' => $pageId,
+                               'page_latest=rev_id',
+                       ],
+                       __METHOD__,
+                       [ 'FOR UPDATE' ] // T51581
+               );
+
+               if ( $current ) {
+                       if ( !$user ) {
+                               global $wgUser;
+                               $user = $wgUser;
+                       }
+
+                       $row = [
+                               'page'       => $pageId,
+                               'user_text'  => $user->getName(),
+                               'user'       => $user->getId(),
+                               'comment'    => $summary,
+                               'minor_edit' => $minor,
+                               'text_id'    => $current->rev_text_id,
+                               'parent_id'  => $current->page_latest,
+                               'len'        => $current->rev_len,
+                               'sha1'       => $current->rev_sha1
+                       ];
+
+                       if ( $wgContentHandlerUseDB ) {
+                               $row['content_model'] = $current->rev_content_model;
+                               $row['content_format'] = $current->rev_content_format;
+                       }
+
+                       $row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
 
-               $title = Title::newFromID( $pageId );
-               $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, $comment, $minor, $user );
+                       $revision = new Revision( $row );
+               } else {
+                       $revision = null;
+               }
 
-               return new Revision( $rec );
+               return $revision;
        }
 
        /**
@@ -1054,13 +1948,35 @@ class Revision implements IDBAccessObject {
        public static function userCanBitfield( $bitfield, $field, User $user = null,
                Title $title = null
        ) {
-               global $wgUser;
-
-               if ( !$user ) {
-                       $user = $wgUser;
+               if ( $bitfield & $field ) { // aspect is deleted
+                       if ( $user === null ) {
+                               global $wgUser;
+                               $user = $wgUser;
+                       }
+                       if ( $bitfield & self::DELETED_RESTRICTED ) {
+                               $permissions = [ 'suppressrevision', 'viewsuppressed' ];
+                       } elseif ( $field & self::DELETED_TEXT ) {
+                               $permissions = [ 'deletedtext' ];
+                       } else {
+                               $permissions = [ 'deletedhistory' ];
+                       }
+                       $permissionlist = implode( ', ', $permissions );
+                       if ( $title === null ) {
+                               wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
+                               return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
+                       } else {
+                               $text = $title->getPrefixedText();
+                               wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
+                               foreach ( $permissions as $perm ) {
+                                       if ( $title->userCan( $perm, $user ) ) {
+                                               return true;
+                                       }
+                               }
+                               return false;
+                       }
+               } else {
+                       return true;
                }
-
-               return RevisionRecord::userCanBitfield( $bitfield, $field, $user, $title );
        }
 
        /**
@@ -1072,7 +1988,18 @@ class Revision implements IDBAccessObject {
         * @return string|bool False if not found
         */
        static function getTimestampFromId( $title, $id, $flags = 0 ) {
-               return self::getRevisionStore()->getTimestampFromId( $title, $id, $flags );
+               $db = ( $flags & self::READ_LATEST )
+                       ? wfGetDB( DB_MASTER )
+                       : wfGetDB( DB_REPLICA );
+               // Casting fix for databases that can't take '' for rev_id
+               if ( $id == '' ) {
+                       $id = 0;
+               }
+               $conds = [ 'rev_id' => $id ];
+               $conds['rev_page'] = $title->getArticleID();
+               $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
+
+               return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
        }
 
        /**
@@ -1083,7 +2010,12 @@ class Revision implements IDBAccessObject {
         * @return int
         */
        static function countByPageId( $db, $id ) {
-               return self::getRevisionStore()->countRevisionsByPageId( $db, $id );
+               $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
+                       [ 'rev_page' => $id ], __METHOD__ );
+               if ( $row ) {
+                       return $row->revCount;
+               }
+               return 0;
        }
 
        /**
@@ -1094,7 +2026,11 @@ class Revision implements IDBAccessObject {
         * @return int
         */
        static function countByTitle( $db, $title ) {
-               return self::getRevisionStore()->countRevisionsByTitle( $db, $title );
+               $id = $title->getArticleID();
+               if ( $id ) {
+                       return self::countByPageId( $db, $id );
+               }
+               return 0;
        }
 
        /**
@@ -1114,11 +2050,28 @@ class Revision implements IDBAccessObject {
         * @return bool True if the given user was the only one to edit since the given timestamp
         */
        public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
+               if ( !$userId ) {
+                       return false;
+               }
+
                if ( is_int( $db ) ) {
                        $db = wfGetDB( $db );
                }
 
-               return self::getRevisionStore()->userWasLastToEdit( $db, $pageId, $userId, $since );
+               $res = $db->select( 'revision',
+                       'rev_user',
+                       [
+                               'rev_page' => $pageId,
+                               'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
+                       ],
+                       __METHOD__,
+                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
+               foreach ( $res as $row ) {
+                       if ( $row->rev_user != $userId ) {
+                               return false;
+                       }
+               }
+               return true;
        }
 
        /**
@@ -1126,20 +2079,54 @@ class Revision implements IDBAccessObject {
         *
         * This method allows for the use of caching, though accessing anything that normally
         * requires permission checks (aside from the text) will trigger a small DB lookup.
-        * The title will also be loaded if $pageIdOrTitle is an integer ID.
+        * The title will also be lazy loaded, though setTitle() can be used to preload it.
         *
-        * @param IDatabase $db ignored!
-        * @param int|Title $pageIdOrTitle Page ID or Title object
-        * @param int $revId Known current revision of this page. Determined automatically if not given.
+        * @param IDatabase $db
+        * @param int $pageId Page ID
+        * @param int $revId Known current revision of this page
         * @return Revision|bool Returns false if missing
         * @since 1.28
         */
-       public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, $revId = 0 ) {
-               $title = $pageIdOrTitle instanceof Title
-                       ? $pageIdOrTitle
-                       : Title::newFromID( $pageIdOrTitle );
+       public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
+               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+               return $cache->getWithSetCallback(
+                       // Page/rev IDs passed in from DB to reflect history merges
+                       $cache->makeGlobalKey( 'revision', $db->getDomainID(), $pageId, $revId ),
+                       $cache::TTL_WEEK,
+                       function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
+                               $setOpts += Database::getCacheSetOptions( $db );
+
+                               $rev = Revision::loadFromPageId( $db, $pageId, $revId );
+                               // Reflect revision deletion and user renames
+                               if ( $rev ) {
+                                       $rev->mTitle = null; // mutable; lazy-load
+                                       $rev->mRefreshMutableFields = true;
+                               }
+
+                               return $rev ?: false; // don't cache negatives
+                       }
+               );
+       }
+
+       /**
+        * For cached revisions, make sure the user name and rev_deleted is up-to-date
+        */
+       private function loadMutableFields() {
+               if ( !$this->mRefreshMutableFields ) {
+                       return; // not needed
+               }
 
-               $record = self::getRevisionStore()->getKnownCurrentRevision( $title, $revId );
-               return $record ? new Revision( $record ) : false;
+               $this->mRefreshMutableFields = false;
+               $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
+               $row = $dbr->selectRow(
+                       [ 'revision', 'user' ],
+                       [ 'rev_deleted', 'user_name' ],
+                       [ 'rev_id' => $this->mId, 'user_id = rev_user' ],
+                       __METHOD__
+               );
+               if ( $row ) { // update values
+                       $this->mDeleted = (int)$row->rev_deleted;
+                       $this->mUserText = $row->user_name;
+               }
        }
 }
index 575970d..d21bcef 100644 (file)
@@ -450,46 +450,6 @@ return [
                return $factory;
        },
 
-       'RevisionStore' => function ( MediaWikiServices $services ) {
-               /** @var SqlBlobStore $blobStore */
-               $blobStore = $services->getService( '_SqlBlobStore' );
-
-               $store = new RevisionStore(
-                       $services->getDBLoadBalancer(),
-                       $blobStore,
-                       $services->getMainWANObjectCache()
-               );
-
-               $config = $services->getMainConfig();
-               $store->setContentHandlerUseDB( $config->get( 'ContentHandlerUseDB' ) );
-
-               return $store;
-       },
-
-       'BlobStore' => function ( MediaWikiServices $services ) {
-               return $services->getService( '_SqlBlobStore' );
-       },
-
-       '_SqlBlobStore' => function ( MediaWikiServices $services ) {
-               global $wgContLang; // TODO: manage $wgContLang as a service
-
-               $store = new SqlBlobStore(
-                       $services->getDBLoadBalancer(),
-                       $services->getMainWANObjectCache()
-               );
-
-               $config = $services->getMainConfig();
-               $store->setCompressRevisions( $config->get( 'CompressRevisions' ) );
-               $store->setCacheExpiry( $config->get( 'RevisionCacheExpiry' ) );
-               $store->setUseExternalStore( $config->get( 'DefaultExternalStore' ) !== false );
-
-               if ( $config->get( 'LegacyEncoding' ) ) {
-                       $store->setLegacyEncoding( $config->get( 'LegacyEncoding' ), $wgContLang );
-               }
-
-               return $store;
-       },
-
        'ExternalStoreFactory' => function ( MediaWikiServices $services ) {
                $config = $services->getMainConfig();
 
index 85e8db6..0e964bf 100644 (file)
@@ -335,8 +335,8 @@ class HistoryAction extends FormlessAction {
         * @return FeedItem
         */
        function feedItem( $row ) {
-               $rev = new Revision( $row, 0, $this->getTitle() );
-
+               $rev = new Revision( $row );
+               $rev->setTitle( $this->getTitle() );
                $text = FeedUtils::formatDiffRow(
                        $this->getTitle(),
                        $this->getTitle()->getPreviousRevisionID( $rev->getId() ),
@@ -639,10 +639,12 @@ class HistoryPager extends ReverseChronologicalPager {
         */
        function historyLine( $row, $next, $notificationtimestamp = false,
                $latest = false, $firstInList = false ) {
-               $rev = new Revision( $row, 0, $this->getTitle() );
+               $rev = new Revision( $row );
+               $rev->setTitle( $this->getTitle() );
 
                if ( is_object( $next ) ) {
-                       $prevRev = new Revision( $next, 0, $this->getTitle() );
+                       $prevRev = new Revision( $next );
+                       $prevRev->setTitle( $this->getTitle() );
                } else {
                        $prevRev = null;
                }
index d6e9b74..768f980 100644 (file)
@@ -1048,7 +1048,8 @@ class MessageCache {
                if ( $titleObj->getLatestRevID() ) {
                        $revision = Revision::newKnownCurrent(
                                $dbr,
-                               $titleObj
+                               $titleObj->getArticleID(),
+                               $titleObj->getLatestRevID()
                        );
                } else {
                        $revision = false;
index c37566b..ac9cd84 100644 (file)
@@ -23,7 +23,6 @@
 use MediaWiki\Edit\PreparedEdit;
 use \MediaWiki\Logger\LoggerFactory;
 use \MediaWiki\MediaWikiServices;
-use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\FakeResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\DBError;
@@ -672,7 +671,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
                } else {
                        $dbr = wfGetDB( DB_REPLICA );
-                       $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
+                       $revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
                }
 
                if ( $revision ) { // sanity
@@ -1265,11 +1264,8 @@ class WikiPage implements Page, IDBAccessObject {
                        $conditions['page_latest'] = $lastRevision;
                }
 
-               $revId = $revision->getId();
-               Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
-
                $row = [ /* SET */
-                       'page_latest'      => $revId,
+                       'page_latest'      => $revision->getId(),
                        'page_touched'     => $dbw->timestamp( $revision->getTimestamp() ),
                        'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
                        'page_is_redirect' => $rt !== null ? 1 : 0,
index 10a338e..13d8a3a 100644 (file)
@@ -3498,7 +3498,13 @@ class Parser {
         * @return Revision|bool False if missing
         */
        public static function statelessFetchRevision( Title $title, $parser = false ) {
-               $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
+               $pageId = $title->getArticleID();
+               $revId = $title->getLatestRevID();
+
+               $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $pageId, $revId );
+               if ( $rev ) {
+                       $rev->setTitle( $title );
+               }
 
                return $rev;
        }
index 6eddfc0..bebc188 100644 (file)
@@ -183,10 +183,12 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
         * @return Content|null
         */
        protected function getContentObj( Title $title ) {
-               $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
+               $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title->getArticleID(),
+                       $title->getLatestRevID() );
                if ( !$revision ) {
                        return null;
                }
+               $revision->setTitle( $title );
                $content = $revision->getContent( Revision::RAW );
                if ( !$content ) {
                        wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' );
index 1639386..671ab6f 100644 (file)
@@ -290,16 +290,15 @@ class SpecialNewpages extends IncludableSpecialPage {
 
        /**
         * @param stdClass $result Result row from recent changes
-        * @param Title $title
-        * @return bool|Revision
+        * @return Revision|bool
         */
-       protected function revisionFromRcResult( stdClass $result, Title $title ) {
+       protected function revisionFromRcResult( stdClass $result ) {
                return new Revision( [
                        'comment' => CommentStore::newKey( 'rc_comment' )->getComment( $result )->text,
                        'deleted' => $result->rc_deleted,
                        'user_text' => $result->rc_user_text,
                        'user' => $result->rc_user,
-               ], 0, $title );
+               ] );
        }
 
        /**
@@ -314,7 +313,8 @@ class SpecialNewpages extends IncludableSpecialPage {
 
                // Revision deletion works on revisions,
                // so cast our recent change row to a revision row.
-               $rev = $this->revisionFromRcResult( $result, $title );
+               $rev = $this->revisionFromRcResult( $result );
+               $rev->setTitle( $title );
 
                $classes = [];
                $attribs = [ 'data-mw-revid' => $result->rev_id ];
index f0a86ff..1c9294c 100644 (file)
                        "◌ֿ",
                        "Ö¿"
                ],
+               [
+                       "◌ֽ",
+                       "Ö½"
+               ],
                [
                        "◌׀",
                        "×€"
index 4d39f7b..a5c4688 100644 (file)
@@ -7,8 +7,6 @@ use MediaWiki\Services\DestructibleService;
 use MediaWiki\Services\SalvageableService;
 use MediaWiki\Services\ServiceDisabledException;
 use MediaWiki\Shell\CommandFactory;
-use MediaWiki\Storage\BlobStore;
-use MediaWiki\Storage\RevisionStore;
 
 /**
  * @covers MediaWiki\MediaWikiServices
@@ -333,8 +331,6 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
                        'LocalServerObjectCache' => [ 'LocalServerObjectCache', BagOStuff::class ],
                        'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ],
                        'ShellCommandFactory' => [ 'ShellCommandFactory', CommandFactory::class ],
-                       'BlobStore' => [ 'BlobStore', BlobStore::class ],
-                       'RevisionStore' => [ 'RevisionStore', RevisionStore::class ],
                ];
        }
 
index 9ab76c8..91dbf2c 100644 (file)
@@ -1,8 +1,4 @@
 <?php
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\IncompleteRevisionException;
-use MediaWiki\Storage\RevisionRecord;
 
 /**
  * RevisionDbTestBase contains test cases for the Revision class that have Database interactions.
@@ -76,7 +72,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                MWNamespace::clearCaches();
                // Reset namespace cache
                $wgContLang->resetNamespaces();
-
                if ( !$this->testPage ) {
                        /**
                         * We have to create a new page for each subclass as the page creation may result
@@ -107,14 +102,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        $props['text'] = 'Lorem Ipsum';
                }
 
-               if ( !isset( $props['user_text'] ) ) {
-                       $props['user_text'] = 'Tester';
-               }
-
-               if ( !isset( $props['user'] ) ) {
-                       $props['user'] = 0;
-               }
-
                if ( !isset( $props['comment'] ) ) {
                        $props['comment'] = 'just a test';
                }
@@ -123,10 +110,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        $props['page'] = $this->testPage->getId();
                }
 
-               if ( !isset( $props['content_model'] ) ) {
-                       $props['content_model'] = CONTENT_MODEL_WIKITEXT;
-               }
-
                $rev = new Revision( $props );
 
                $dbw = wfGetDB( DB_MASTER );
@@ -219,23 +202,14 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $revId = $rev->insertOn( wfGetDB( DB_MASTER ) );
 
                $this->assertInternalType( 'integer', $revId );
-               $this->assertSame( $revId, $rev->getId() );
-
-               // getTextId() must be an int!
                $this->assertInternalType( 'integer', $rev->getTextId() );
-
-               $mainSlot = $rev->getRevisionRecord()->getSlot( 'main', RevisionRecord::RAW );
-
-               // we currently only support storage in the text table
-               $textId = MediaWikiServices::getInstance()
-                       ->getBlobStore()
-                       ->getTextIdFromAddress( $mainSlot->getAddress() );
+               $this->assertSame( $revId, $rev->getId() );
 
                $this->assertSelect(
                        'text',
                        [ 'old_id', 'old_text' ],
-                       "old_id = $textId",
-                       [ [ strval( $textId ), 'Revision Text' ] ]
+                       "old_id = {$rev->getTextId()}",
+                       [ [ strval( $rev->getTextId() ), 'Revision Text' ] ]
                );
                $this->assertSelect(
                        'revision',
@@ -254,7 +228,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        [ [
                                strval( $rev->getId() ),
                                strval( $this->testPage->getId() ),
-                               strval( $textId ),
+                               strval( $rev->getTextId() ),
                                '0',
                                '0',
                                '0',
@@ -272,12 +246,11 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                // If an ExternalStore is set don't use it.
                $this->setMwGlobals( 'wgDefaultExternalStore', false );
                $this->setExpectedException(
-                       IncompleteRevisionException::class,
-                       "rev_page field must not be 0!"
+                       MWException::class,
+                       "Cannot insert revision: page ID must be nonzero"
                );
 
-               $title = Title::newFromText( 'Nonexistant-' . __METHOD__ );
-               $rev = new Revision( [], 0, $title );
+               $rev = new Revision( [] );
 
                $rev->insertOn( wfGetDB( DB_MASTER ) );
        }
@@ -348,42 +321,12 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                                return $f + [ 'ar_namespace', 'ar_title' ];
                        },
                ];
-               yield [
-                       function ( $f ) {
-                               unset( $f['ar_text'] );
-                               return $f;
-                       },
-               ];
                yield [
                        function ( $f ) {
                                unset( $f['ar_text_id'] );
                                return $f;
                        },
                ];
-               yield [
-                       function ( $f ) {
-                               unset( $f['ar_page_id'] );
-                               return $f;
-                       },
-               ];
-               yield [
-                       function ( $f ) {
-                               unset( $f['ar_parent_id'] );
-                               return $f;
-                       },
-               ];
-               yield [
-                       function ( $f ) {
-                               unset( $f['ar_rev_id'] );
-                               return $f;
-                       },
-               ];
-               yield [
-                       function ( $f ) {
-                               unset( $f['ar_sha1'] );
-                               return $f;
-                       },
-               ];
        }
 
        /**
@@ -391,17 +334,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
         * @covers Revision::newFromArchiveRow
         */
        public function testNewFromArchiveRow( $selectModifier ) {
-               $services = MediaWikiServices::getInstance();
-
-               $store = new RevisionStore(
-                       $services->getDBLoadBalancer(),
-                       $services->getService( '_SqlBlobStore' ),
-                       $services->getMainWANObjectCache()
-               );
-
-               $store->setContentHandlerUseDB( $this->getContentHandlerUseDB() );
-               $this->setService( 'RevisionStore', $store );
-
                $page = $this->createPage(
                        'RevisionStorageTest_testNewFromArchiveRow',
                        'Lorem Ipsum',
@@ -422,8 +354,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $row = $res->fetchObject();
                $res->free();
 
-               // MCR migration note: $row is now required to contain ar_title and ar_namespace.
-               // Alternatively, a Title object can be passed to RevisionStore::newRevisionFromArchiveRow
                $rev = Revision::newFromArchiveRow( $row );
 
                $this->assertRevEquals( $orig, $rev );
@@ -452,7 +382,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $row = $res->fetchObject();
                $res->free();
 
-               $rev = Revision::newFromArchiveRow( $row, [ 'comment_text' => 'SOMEOVERRIDE' ] );
+               $rev = Revision::newFromArchiveRow( $row, [ 'comment' => 'SOMEOVERRIDE' ] );
 
                $this->assertNotEquals( $orig->getComment(), $rev->getComment() );
                $this->assertEquals( 'SOMEOVERRIDE', $rev->getComment() );
@@ -496,8 +426,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
         * @covers Revision::newFromPageId
         */
        public function testNewFromPageIdWithNotLatestId() {
-               $content = new WikitextContent( __METHOD__ );
-               $this->testPage->doEditContent( $content, __METHOD__ );
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
                $rev = Revision::newFromPageId(
                        $this->testPage->getId(),
                        $this->testPage->getRevision()->getPrevious()->getId()
@@ -518,7 +447,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
                $id = $this->testPage->getRevision()->getId();
 
-               $this->hideDeprecated( 'Revision::fetchRevision' );
                $res = Revision::fetchRevision( $this->testPage->getTitle() );
 
                # note: order is unspecified
@@ -527,7 +455,8 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        $rows[$row->rev_id] = $row;
                }
 
-               $this->assertEmpty( $rows, 'expected empty set' );
+               $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' );
+               $this->assertArrayHasKey( $id, $rows, 'missing revision with id ' . $id );
        }
 
        /**
@@ -612,10 +541,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        'new null revision should have a different id from the original revision' );
                $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
                        'new null revision should have the same text id as the original revision' );
-               $this->assertEquals( $orig->getSha1(), $rev->getSha1(),
-                       'new null revision should have the same SHA1 as the original revision' );
-               $this->assertTrue( $orig->getRevisionRecord()->hasSameContent( $rev->getRevisionRecord() ),
-                       'new null revision should have the same content as the original revision' );
                $this->assertEquals( __METHOD__, $rev->getContent()->getNativeData() );
        }
 
@@ -681,7 +606,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        'user' => $userA->getId(),
                        'text' => 'zero',
                        'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'comment' => 'edit zero'
+                       'summary' => 'edit zero'
                ] );
                $revisions[0]->insertOn( $dbw );
 
@@ -693,7 +618,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        'user' => $userA->getId(),
                        'text' => 'one',
                        'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'comment' => 'edit one'
+                       'summary' => 'edit one'
                ] );
                $revisions[1]->insertOn( $dbw );
 
@@ -704,7 +629,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        'user' => $userB->getId(),
                        'text' => 'two',
                        'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'comment' => 'edit two'
+                       'summary' => 'edit two'
                ] );
                $revisions[2]->insertOn( $dbw );
 
@@ -715,7 +640,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        'user' => $userA->getId(),
                        'text' => 'three',
                        'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'comment' => 'edit three'
+                       'summary' => 'edit three'
                ] );
                $revisions[3]->insertOn( $dbw );
 
@@ -726,24 +651,13 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        'user' => $userA->getId(),
                        'text' => 'zero',
                        'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'comment' => 'edit four'
+                       'summary' => 'edit four'
                ] );
                $revisions[4]->insertOn( $dbw );
 
                // test it ---------------------------------
                $since = $revisions[$sinceIdx]->getTimestamp();
 
-               $allRows = iterator_to_array( $dbw->select(
-                       'revision',
-                       [ 'rev_id', 'rev_timestamp', 'rev_user' ],
-                       [
-                               'rev_page' => $page->getId(),
-                               //'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $since ) )
-                       ],
-                       __METHOD__,
-                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ]
-               ) );
-
                $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
 
                $this->assertEquals( $expectedLast, $wasLast );
@@ -891,16 +805,12 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        'text_id' => 123456789, // not in the test DB
                ] );
 
-               MediaWiki\suppressWarnings(); // bad text_id will trigger a warning.
-
                $this->assertNull( $rev->getContent(),
                        "getContent() should return null if the revision's text blob could not be loaded." );
 
                // NOTE: check this twice, once for lazy initialization, and once with the cached value.
                $this->assertNull( $rev->getContent(),
                        "getContent() should return null if the revision's text blob could not be loaded." );
-
-               MediaWiki\suppressWarnings( 'end' );
        }
 
        public function provideGetSize() {
@@ -994,7 +904,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
         */
        public function testLoadFromId() {
                $rev = $this->testPage->getRevision();
-               $this->hideDeprecated( 'Revision::loadFromId' );
                $this->assertRevEquals(
                        $rev,
                        Revision::loadFromId( wfGetDB( DB_MASTER ), $rev->getId() )
@@ -1117,7 +1026,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $rev[1] = $this->testPage->getLatest();
 
                $this->assertSame(
-                       [ $rev[1] => $textLength ],
+                       [ $rev[1] => strval( $textLength ) ],
                        Revision::getParentLengths(
                                wfGetDB( DB_MASTER ),
                                [ $rev[1] ]
@@ -1140,7 +1049,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $rev[2] = $this->testPage->getLatest();
 
                $this->assertSame(
-                       [ $rev[1] => $textOneLength, $rev[2] => $textTwoLength ],
+                       [ $rev[1] => strval( $textOneLength ), $rev[2] => strval( $textTwoLength ) ],
                        Revision::getParentLengths(
                                wfGetDB( DB_MASTER ),
                                [ $rev[1], $rev[2] ]
@@ -1171,6 +1080,14 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                );
        }
 
+       /**
+        * @covers Revision::getTitle
+        */
+       public function testGetTitle_forBadRevision() {
+               $rev = new Revision( [] );
+               $this->assertNull( $rev->getTitle() );
+       }
+
        /**
         * @covers Revision::isMinor
         */
@@ -1346,21 +1263,14 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $rev = $this->testPage->getRevision();
 
                // Clear any previous cache for the revision during creation
-               $key = $cache->makeGlobalKey( 'revision-row-1.29',
-                       $db->getDomainID(),
-                       $rev->getPage(),
-                       $rev->getId()
-               );
+               $key = $cache->makeGlobalKey( 'revision', $db->getDomainID(), $rev->getPage(), $rev->getId() );
                $cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
                $this->assertFalse( $cache->get( $key ) );
 
                // Get the new revision and make sure it is in the cache and correct
                $newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() );
                $this->assertRevEquals( $rev, $newRev );
-
-               $cachedRow = $cache->get( $key );
-               $this->assertNotFalse( $cachedRow );
-               $this->assertEquals( $rev->getId(), $cachedRow->rev_id );
+               $this->assertRevEquals( $rev, $cache->get( $key ) );
        }
 
        public function provideUserCanBitfield() {
@@ -1467,7 +1377,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        ]
                );
                $user = $this->getTestUser( $userGroups )->getUser();
-               $revision = new Revision( [ 'deleted' => $bitField ], 0, $this->testPage->getTitle() );
+               $revision = new Revision( [ 'deleted' => $bitField ] );
 
                $this->assertSame(
                        $expected,
index ef55e72..e4ea40f 100644 (file)
@@ -1,9 +1,6 @@
 <?php
 
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SqlBlobStore;
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\TestingAccessWrapper;
 
 /**
  * Test cases in RevisionTest should not interact with the Database.
@@ -57,10 +54,10 @@ class RevisionTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideConstructFromArray
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers Revision::constructFromRowArray
         */
-       public function testConstructFromArray( $rowArray ) {
-               $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+       public function testConstructFromArray( array $rowArray ) {
+               $rev = new Revision( $rowArray );
                $this->assertNotNull( $rev->getContent(), 'no content object available' );
                $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
                $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
@@ -68,7 +65,7 @@ class RevisionTest extends MediaWikiTestCase {
 
        /**
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers Revision::constructFromRowArray
         */
        public function testConstructFromEmptyArray() {
                $rev = new Revision( [], 0, $this->getMockTitle() );
@@ -93,20 +90,30 @@ class RevisionTest extends MediaWikiTestCase {
                        99,
                        'SomeTextUserName',
                ];
-               yield 'user text only' => [
+               // Note: the below XXX test cases are odd and probably result in unexpected behaviour if used
+               // in production code.
+               yield 'XXX: user text only' => [
                        [
                                'content' => new JavaScriptContent( 'hello world.' ),
                                'user_text' => '111.111.111.111',
                        ],
-                       0,
+                       null,
                        '111.111.111.111',
                ];
+               yield 'XXX: user id only' => [
+                       [
+                               'content' => new JavaScriptContent( 'hello world.' ),
+                               'user' => 9989,
+                       ],
+                       9989,
+                       null,
+               ];
        }
 
        /**
         * @dataProvider provideConstructFromArray_userSetAsExpected
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers Revision::constructFromRowArray
         *
         * @param array $rowArray
         * @param mixed $expectedUserId null to expect the current wgUser ID
@@ -126,7 +133,7 @@ class RevisionTest extends MediaWikiTestCase {
                        $expectedUserName = $testUser->getName();
                }
 
-               $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+               $rev = new Revision( $rowArray );
                $this->assertEquals( $expectedUserId, $rev->getUser() );
                $this->assertEquals( $expectedUserName, $rev->getUserText() );
        }
@@ -136,37 +143,28 @@ 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" )
                ];
-               yield 'unknown user id and no user name' => [
-                       [
-                               'content' => new JavaScriptContent( 'hello world.' ),
-                               'user' => 9989,
-                       ],
-                       new MWException( 'user_text not given, and unknown user ID 9989' )
-               ];
                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',
-                       new InvalidArgumentException(
-                               '$row must be a row object, an associative array, or a RevisionRecord'
-                       )
+                       new MWException( 'Revision constructor passed invalid row format.' )
                ];
        }
 
        /**
         * @dataProvider provideConstructFromArrayThrowsExceptions
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers Revision::constructFromRowArray
         */
        public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) {
                $this->setExpectedException(
@@ -174,25 +172,23 @@ class RevisionTest extends MediaWikiTestCase {
                        $expectedException->getMessage(),
                        $expectedException->getCode()
                );
-               new Revision( $rowArray, 0, $this->getMockTitle() );
+               new Revision( $rowArray );
        }
 
        /**
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers Revision::constructFromRowArray
         */
        public function testConstructFromNothing() {
-               $this->setExpectedException(
-                       InvalidArgumentException::class
-               );
-               new Revision( [] );
+               $rev = new Revision( [] );
+               $this->assertNull( $rev->getId(), 'getId()' );
        }
 
        public function provideConstructFromRow() {
                yield 'Full construction' => [
                        [
-                               'rev_id' => '42',
-                               'rev_page' => '23',
+                               'rev_id' => '2',
+                               'rev_page' => '1',
                                'rev_text_id' => '2',
                                'rev_timestamp' => '20171017114835',
                                'rev_user_text' => '127.0.0.1',
@@ -209,8 +205,8 @@ class RevisionTest extends MediaWikiTestCase {
                                'rev_content_model' => 'GOATMODEL',
                        ],
                        function ( RevisionTest $testCase, Revision $rev ) {
-                               $testCase->assertSame( 42, $rev->getId() );
-                               $testCase->assertSame( 23, $rev->getPage() );
+                               $testCase->assertSame( 2, $rev->getId() );
+                               $testCase->assertSame( 1, $rev->getPage() );
                                $testCase->assertSame( 2, $rev->getTextId() );
                                $testCase->assertSame( '20171017114835', $rev->getTimestamp() );
                                $testCase->assertSame( '127.0.0.1', $rev->getUserText() );
@@ -225,10 +221,10 @@ class RevisionTest extends MediaWikiTestCase {
                                $testCase->assertSame( 'GOATMODEL', $rev->getContentModel() );
                        }
                ];
-               yield 'default field values' => [
+               yield 'null fields' => [
                        [
-                               'rev_id' => '42',
-                               'rev_page' => '23',
+                               'rev_id' => '2',
+                               'rev_page' => '1',
                                'rev_text_id' => '2',
                                'rev_timestamp' => '20171017114835',
                                'rev_user_text' => '127.0.0.1',
@@ -240,24 +236,11 @@ class RevisionTest extends MediaWikiTestCase {
                                'rev_comment_cid' => null,
                        ],
                        function ( RevisionTest $testCase, Revision $rev ) {
-                               // parent ID may be null
-                               $testCase->assertSame( null, $rev->getParentId(), 'revision id' );
-
-                               // given fields
-                               $testCase->assertSame( $rev->getTimestamp(), '20171017114835', 'timestamp' );
-                               $testCase->assertSame( $rev->getUserText(), '127.0.0.1', 'user name' );
-                               $testCase->assertSame( $rev->getUser(), 0, 'user id' );
-                               $testCase->assertSame( $rev->getComment(), 'Goat Comment!' );
-                               $testCase->assertSame( false, $rev->isMinor(), 'minor edit' );
-                               $testCase->assertSame( 0, $rev->getVisibility(), 'visibility flags' );
-
-                               // computed fields
-                               $testCase->assertNotNull( $rev->getSize(), 'size' );
-                               $testCase->assertNotNull( $rev->getSha1(), 'hash' );
-
-                               // NOTE: model and format will be detected based on the namespace of the (mock) title
-                               $testCase->assertSame( 'text/x-wiki', $rev->getContentFormat(), 'format' );
-                               $testCase->assertSame( 'wikitext', $rev->getContentModel(), 'model' );
+                               $testCase->assertNull( $rev->getSize() );
+                               $testCase->assertNull( $rev->getParentId() );
+                               $testCase->assertNull( $rev->getSha1() );
+                               $testCase->assertSame( 'text/x-wiki', $rev->getContentFormat() );
+                               $testCase->assertSame( 'wikitext', $rev->getContentModel() );
                        }
                ];
        }
@@ -265,34 +248,11 @@ class RevisionTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideConstructFromRow
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
+        * @covers Revision::constructFromRowArray
         */
        public function testConstructFromRow( array $arrayData, $assertions ) {
-               $data = 'Hello goat.'; // needs to match model and format
-
-               $blobStore = $this->getMockBuilder( SqlBlobStore::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $blobStore->method( 'getBlob' )
-                       ->will( $this->returnValue( $data ) );
-
-               $blobStore->method( 'getTextIdFromAddress' )
-                       ->will( $this->returnCallback(
-                               function ( $address ) {
-                                       // Turn "tt:1234" into 12345.
-                                       // Note that this must be functional so we can test getTextId().
-                                       // Ideally, we'd un-mock getTextIdFromAddress and use its actual implementation.
-                                       $parts = explode( ':', $address );
-                                       return (int)array_pop( $parts );
-                               }
-                       ) );
-
-               // Note override internal service, so RevisionStore uses it as well.
-               $this->setService( '_SqlBlobStore', $blobStore );
-
                $row = (object)$arrayData;
-               $rev = new Revision( $row, 0, $this->getMockTitle() );
+               $rev = new Revision( $row );
                $assertions( $this, $rev );
        }
 
@@ -322,7 +282,7 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers Revision::getId
         */
        public function testGetId( $rowArray, $expectedId ) {
-               $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+               $rev = new Revision( $rowArray );
                $this->assertEquals( $expectedId, $rev->getId() );
        }
 
@@ -336,7 +296,7 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers Revision::setId
         */
        public function testSetId( $input, $expected ) {
-               $rev = new Revision( [], 0, $this->getMockTitle() );
+               $rev = new Revision( [] );
                $rev->setId( $input );
                $this->assertSame( $expected, $rev->getId() );
        }
@@ -351,7 +311,7 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers Revision::setUserIdAndName
         */
        public function testSetUserIdAndName( $inputId, $expectedId, $name ) {
-               $rev = new Revision( [], 0, $this->getMockTitle() );
+               $rev = new Revision( [] );
                $rev->setUserIdAndName( $inputId, $name );
                $this->assertSame( $expectedId, $rev->getUser( Revision::RAW ) );
                $this->assertEquals( $name, $rev->getUserText( Revision::RAW ) );
@@ -368,7 +328,7 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers Revision::getTextId()
         */
        public function testGetTextId( $rowArray, $expected ) {
-               $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+               $rev = new Revision( $rowArray );
                $this->assertSame( $expected, $rev->getTextId() );
        }
 
@@ -383,7 +343,7 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers Revision::getParentId()
         */
        public function testGetParentId( $rowArray, $expected ) {
-               $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+               $rev = new Revision( $rowArray );
                $this->assertSame( $expected, $rev->getParentId() );
        }
 
@@ -416,44 +376,9 @@ class RevisionTest extends MediaWikiTestCase {
                $this->testGetRevisionText( $expected, $rowData );
        }
 
-       private function getWANObjectCache() {
-               return new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
-       }
-
-       /**
-        * @return SqlBlobStore
-        */
-       private function getBlobStore() {
-               /** @var LoadBalancer $lb */
-               $lb = $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $cache = $this->getWANObjectCache();
-
-               $blobStore = new SqlBlobStore( $lb, $cache );
-               return $blobStore;
-       }
-
-       /**
-        * @return RevisionStore
-        */
-       private function getRevisionStore() {
-               /** @var LoadBalancer $lb */
-               $lb = $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $cache = $this->getWANObjectCache();
-
-               $blobStore = new RevisionStore( $lb, $this->getBlobStore(), $cache );
-               return $blobStore;
-       }
-
        public function provideGetRevisionTextWithLegacyEncoding() {
                yield 'Utf8Native' => [
                        "Wiki est l'\xc3\xa9cole superieur !",
-                       'fr',
                        'iso-8859-1',
                        [
                                'old_flags' => 'utf-8',
@@ -462,7 +387,6 @@ class RevisionTest extends MediaWikiTestCase {
                ];
                yield 'Utf8Legacy' => [
                        "Wiki est l'\xc3\xa9cole superieur !",
-                       'fr',
                        'iso-8859-1',
                        [
                                'old_flags' => '',
@@ -475,11 +399,8 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers Revision::getRevisionText
         * @dataProvider provideGetRevisionTextWithLegacyEncoding
         */
-       public function testGetRevisionWithLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
-               $blobStore = $this->getBlobStore();
-               $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
-               $this->setService( 'BlobStore', $blobStore );
-
+       public function testGetRevisionWithLegacyEncoding( $expected, $encoding, $rowData ) {
+               $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
                $this->testGetRevisionText( $expected, $rowData );
        }
 
@@ -491,7 +412,6 @@ class RevisionTest extends MediaWikiTestCase {
                 */
                yield 'Utf8NativeGzip' => [
                        "Wiki est l'\xc3\xa9cole superieur !",
-                       'fr',
                        'iso-8859-1',
                        [
                                'old_flags' => 'gzip,utf-8',
@@ -500,7 +420,6 @@ class RevisionTest extends MediaWikiTestCase {
                ];
                yield 'Utf8LegacyGzip' => [
                        "Wiki est l'\xc3\xa9cole superieur !",
-                       'fr',
                        'iso-8859-1',
                        [
                                'old_flags' => 'gzip',
@@ -513,13 +432,9 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers Revision::getRevisionText
         * @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding
         */
-       public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
+       public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $encoding, $rowData ) {
                $this->checkPHPExtension( 'zlib' );
-
-               $blobStore = $this->getBlobStore();
-               $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
-               $this->setService( 'BlobStore', $blobStore );
-
+               $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
                $this->testGetRevisionText( $expected, $rowData );
        }
 
@@ -545,10 +460,7 @@ class RevisionTest extends MediaWikiTestCase {
         */
        public function testCompressRevisionTextUtf8Gzip() {
                $this->checkPHPExtension( 'zlib' );
-
-               $blobStore = $this->getBlobStore();
-               $blobStore->setCompressBlobs( true );
-               $this->setService( 'BlobStore', $blobStore );
+               $this->setMwGlobals( 'wgCompressRevisions', true );
 
                $row = new stdClass;
                $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
@@ -563,41 +475,20 @@ class RevisionTest extends MediaWikiTestCase {
                        Revision::getRevisionText( $row ), "getRevisionText" );
        }
 
+       public function provideFetchFromConds() {
+               yield [ 0, [] ];
+               yield [ Revision::READ_LOCKING, [ 'FOR UPDATE' ] ];
+       }
+
        /**
-        * @covers Revision::loadFromTitle
+        * @dataProvider provideFetchFromConds
+        * @covers Revision::fetchFromConds
         */
-       public function testLoadFromTitle() {
-               $title = $this->getMockTitle();
-
-               $conditions = [
-                       'rev_id=page_latest',
-                       'page_namespace' => $title->getNamespace(),
-                       'page_title' => $title->getDBkey()
-               ];
-
-               $row = (object)[
-                       'rev_id' => '42',
-                       'rev_page' => $title->getArticleID(),
-                       'rev_text_id' => '2',
-                       'rev_timestamp' => '20171017114835',
-                       'rev_user_text' => '127.0.0.1',
-                       'rev_user' => '0',
-                       'rev_minor_edit' => '0',
-                       'rev_deleted' => '0',
-                       'rev_len' => '46',
-                       'rev_parent_id' => '1',
-                       'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                       'rev_comment_text' => 'Goat Comment!',
-                       'rev_comment_data' => null,
-                       'rev_comment_cid' => null,
-                       'rev_content_format' => 'GOATFORMAT',
-                       'rev_content_model' => 'GOATMODEL',
-               ];
+       public function testFetchFromConds( $flags, array $options ) {
+               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $conditions = [ 'conditionsArray' ];
 
                $db = $this->getMock( IDatabase::class );
-               $db->expects( $this->any() )
-                       ->method( 'getDomainId' )
-                       ->will( $this->returnValue( wfWikiID() ) );
                $db->expects( $this->once() )
                        ->method( 'selectRow' )
                        ->with(
@@ -606,24 +497,17 @@ class RevisionTest extends MediaWikiTestCase {
                                $this->isType( 'array' ),
                                $this->equalTo( $conditions ),
                                // Method name
-                               $this->stringContains( 'fetchRevisionRowFromConds' ),
-                               // We don't really care about the options here
-                               $this->isType( 'array' ),
+                               $this->equalTo( 'Revision::fetchFromConds' ),
+                               $this->equalTo( $options ),
                                // We don't really care about the join conds are they come from the joinCond methods
                                $this->isType( 'array' )
                        )
-                       ->willReturn( $row );
-
-               $revision = Revision::loadFromTitle( $db, $title );
-
-               $this->assertEquals( $title->getArticleID(), $revision->getTitle()->getArticleID() );
-               $this->assertEquals( $row->rev_id, $revision->getId() );
-               $this->assertEquals( $row->rev_len, $revision->getSize() );
-               $this->assertEquals( $row->rev_sha1, $revision->getSha1() );
-               $this->assertEquals( $row->rev_parent_id, $revision->getParentId() );
-               $this->assertEquals( $row->rev_timestamp, $revision->getTimestamp() );
-               $this->assertEquals( $row->rev_comment_text, $revision->getComment() );
-               $this->assertEquals( $row->rev_user_text, $revision->getUserText() );
+                       ->willReturn( 'RETURNVALUE' );
+
+               $wrapper = TestingAccessWrapper::newFromClass( Revision::class );
+               $result = $wrapper->fetchFromConds( $db, $conditions, $flags );
+
+               $this->assertEquals( 'RETURNVALUE', $result );
        }
 
        public function provideDecompressRevisionText() {
@@ -688,12 +572,8 @@ class RevisionTest extends MediaWikiTestCase {
         * @param mixed $expected
         */
        public function testDecompressRevisionText( $legacyEncoding, $text, $flags, $expected ) {
-               $blobStore = $this->getBlobStore();
-               if ( $legacyEncoding ) {
-                       $blobStore->setLegacyEncoding( $legacyEncoding, Language::factory( 'en' ) );
-               }
-
-               $this->setService( 'BlobStore', $blobStore );
+               $this->setMwGlobals( 'wgLegacyEncoding', $legacyEncoding );
+               $this->setMwGlobals( 'wgLanguageCode', 'en' );
                $this->assertSame(
                        $expected,
                        Revision::decompressRevisionText( $text, $flags )
@@ -789,20 +669,14 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers Revision::getRevisionText
         */
        public function testGetRevisionText_external_oldId() {
-               $cache = $this->getWANObjectCache();
+               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
                $this->setService( 'MainWANObjectCache', $cache );
-
                $this->setService(
                        'ExternalStoreFactory',
                        new ExternalStoreFactory( [ 'ForTesting' ] )
                );
 
-               $lb = $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $blobStore = new SqlBlobStore( $lb, $cache );
-               $this->setService( 'BlobStore', $blobStore );
+               $cacheKey = $cache->makeKey( 'revisiontext', 'textid', '7777' );
 
                $this->assertSame(
                        'AAAABBAAA',
@@ -814,8 +688,6 @@ class RevisionTest extends MediaWikiTestCase {
                                ]
                        )
                );
-
-               $cacheKey = $cache->makeKey( 'revisiontext', 'textid', 'tt:7777' );
                $this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) );
        }
 
@@ -1011,8 +883,6 @@ class RevisionTest extends MediaWikiTestCase {
                                'fields' => [
                                        'ar_id',
                                        'ar_page_id',
-                                       'ar_namespace',
-                                       'ar_title',
                                        'ar_rev_id',
                                        'ar_text',
                                        'ar_text_id',
@@ -1041,8 +911,6 @@ class RevisionTest extends MediaWikiTestCase {
                                'fields' => [
                                        'ar_id',
                                        'ar_page_id',
-                                       'ar_namespace',
-                                       'ar_title',
                                        'ar_rev_id',
                                        'ar_text',
                                        'ar_text_id',
@@ -1076,8 +944,6 @@ class RevisionTest extends MediaWikiTestCase {
                                'fields' => [
                                        'ar_id',
                                        'ar_page_id',
-                                       'ar_namespace',
-                                       'ar_title',
                                        'ar_rev_id',
                                        'ar_text',
                                        'ar_text_id',
@@ -1114,8 +980,6 @@ class RevisionTest extends MediaWikiTestCase {
                                'fields' => [
                                        'ar_id',
                                        'ar_page_id',
-                                       'ar_namespace',
-                                       'ar_title',
                                        'ar_rev_id',
                                        'ar_text',
                                        'ar_text_id',
@@ -1152,8 +1016,6 @@ class RevisionTest extends MediaWikiTestCase {
                                'fields' => [
                                        'ar_id',
                                        'ar_page_id',
-                                       'ar_namespace',
-                                       'ar_title',
                                        'ar_rev_id',
                                        'ar_text',
                                        'ar_text_id',
@@ -1185,11 +1047,6 @@ class RevisionTest extends MediaWikiTestCase {
         */
        public function testGetArchiveQueryInfo( $globals, $expected ) {
                $this->setMwGlobals( $globals );
-
-               $revisionStore = $this->getRevisionStore();
-               $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
-               $this->setService( 'RevisionStore', $revisionStore );
-
                $this->assertEquals(
                        $expected,
                        Revision::getArchiveQueryInfo()
@@ -1541,11 +1398,6 @@ class RevisionTest extends MediaWikiTestCase {
         */
        public function testGetQueryInfo( $globals, $options, $expected ) {
                $this->setMwGlobals( $globals );
-
-               $revisionStore = $this->getRevisionStore();
-               $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
-               $this->setService( 'RevisionStore', $revisionStore );
-
                $this->assertEquals(
                        $expected,
                        Revision::getQueryInfo( $options )