Merge "install.php: Allow extensions and skins to be specified"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 11 Oct 2018 05:34:33 +0000 (05:34 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 11 Oct 2018 05:34:33 +0000 (05:34 +0000)
244 files changed:
RELEASE-NOTES-1.32
autoload.php
composer.json
includes/ActorMigration.php
includes/AutoLoader.php
includes/Block.php
includes/DefaultSettings.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/MediaWiki.php
includes/MediaWikiServices.php
includes/OrderedStreamingForkController.php
includes/OutputPage.php
includes/Revision.php
includes/Revision/IncompleteRevisionException.php [new file with mode: 0644]
includes/Revision/MutableRevisionRecord.php [new file with mode: 0644]
includes/Revision/MutableRevisionSlots.php [new file with mode: 0644]
includes/Revision/RenderedRevision.php
includes/Revision/RevisionAccessException.php [new file with mode: 0644]
includes/Revision/RevisionArchiveRecord.php [new file with mode: 0644]
includes/Revision/RevisionFactory.php [new file with mode: 0644]
includes/Revision/RevisionLookup.php [new file with mode: 0644]
includes/Revision/RevisionRecord.php [new file with mode: 0644]
includes/Revision/RevisionRenderer.php
includes/Revision/RevisionSlots.php [new file with mode: 0644]
includes/Revision/RevisionStore.php [new file with mode: 0644]
includes/Revision/RevisionStoreFactory.php [new file with mode: 0644]
includes/Revision/RevisionStoreRecord.php [new file with mode: 0644]
includes/Revision/SlotRecord.php [new file with mode: 0644]
includes/Revision/SlotRenderingProvider.php
includes/Revision/SuppressedDataException.php [new file with mode: 0644]
includes/ServiceWiring.php
includes/Storage/DerivedPageDataUpdater.php
includes/Storage/IncompleteRevisionException.php [deleted file]
includes/Storage/MutableRevisionRecord.php [deleted file]
includes/Storage/MutableRevisionSlots.php [deleted file]
includes/Storage/NameTableStoreFactory.php
includes/Storage/PageUpdater.php
includes/Storage/RevisionAccessException.php [deleted file]
includes/Storage/RevisionArchiveRecord.php [deleted file]
includes/Storage/RevisionFactory.php [deleted file]
includes/Storage/RevisionLookup.php [deleted file]
includes/Storage/RevisionRecord.php [deleted file]
includes/Storage/RevisionSlots.php [deleted file]
includes/Storage/RevisionSlotsUpdate.php
includes/Storage/RevisionStore.php [deleted file]
includes/Storage/RevisionStoreFactory.php [deleted file]
includes/Storage/RevisionStoreRecord.php [deleted file]
includes/Storage/SlotRecord.php [deleted file]
includes/Storage/SuppressedDataException.php [deleted file]
includes/actions/InfoAction.php
includes/actions/McrUndoAction.php
includes/api/ApiBase.php
includes/api/ApiComparePages.php
includes/api/ApiFeedContributions.php
includes/api/ApiLogin.php
includes/api/ApiQueryAllDeletedRevisions.php
includes/api/ApiQueryAllRevisions.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQueryContributors.php
includes/api/ApiQueryDeletedRevisions.php
includes/api/ApiQueryFilearchive.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryRevisions.php
includes/api/ApiQueryRevisionsBase.php
includes/api/ApiQueryUserContribs.php
includes/api/ApiQueryWatchlist.php
includes/api/ApiRevisionDelete.php
includes/api/ApiTag.php
includes/auth/LegacyHookPreAuthenticationProvider.php
includes/cache/UserCache.php
includes/changes/RecentChange.php
includes/db/DatabaseOracle.php
includes/diff/DifferenceEngine.php
includes/filerepo/file/ArchivedFile.php
includes/filerepo/file/LocalFile.php
includes/filerepo/file/OldLocalFile.php
includes/htmlform/fields/HTMLInfoField.php
includes/installer/DatabaseUpdater.php
includes/jobqueue/jobs/DeletePageJob.php [new file with mode: 0644]
includes/jobqueue/jobs/RefreshLinksJob.php
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMssql.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabaseMysqli.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancerSingle.php
includes/logging/LogEntry.php
includes/page/Article.php
includes/page/PageArchive.php
includes/page/WikiPage.php
includes/poolcounter/PoolWorkArticleView.php
includes/revisiondelete/RevDelList.php
includes/revisiondelete/RevisionDeleteUser.php
includes/specialpage/LoginSignupSpecialPage.php
includes/specials/SpecialLog.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialNewimages.php
includes/specials/SpecialUndelete.php
includes/specials/pagers/ContribsPager.php
includes/specials/pagers/NewFilesPager.php
includes/user/BotPassword.php
includes/user/User.php
languages/i18n/ace.json
languages/i18n/af.json
languages/i18n/ar.json
languages/i18n/be-tarask.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/egl.json
languages/i18n/en.json
languages/i18n/fr.json
languages/i18n/he.json
languages/i18n/lb.json
languages/i18n/lt.json
languages/i18n/mk.json
languages/i18n/nl.json
languages/i18n/qqq.json
languages/i18n/roa-tara.json
languages/i18n/ru.json
languages/i18n/si.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/deleteBatch.php
maintenance/edit.php
maintenance/initEditCount.php
maintenance/migrateActors.php
maintenance/populateContentTables.php
maintenance/populateLogSearch.php
maintenance/reassignEdits.php
maintenance/removeUnusedAccounts.php
maintenance/resources/foreign-resources.yaml
maintenance/rollbackEdits.php
maintenance/storage/dumpRev.php
resources/lib/CLDRPluralRuleParser/CLDRPluralRuleParser.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.mixins.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.highlightCircles.seenunseen.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.TagItemWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.variables.less
tests/common/TestsAutoLoader.php
tests/parser/ParserTestRunner.php
tests/phpunit/includes/ActorMigrationTest.php
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/McrReadNewSchemaOverride.php [new file with mode: 0644]
tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/McrSchemaDetection.php [new file with mode: 0644]
tests/phpunit/includes/Revision/McrSchemaOverride.php [new file with mode: 0644]
tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/McrWriteBothSchemaOverride.php [new file with mode: 0644]
tests/phpunit/includes/Revision/MutableRevisionRecordTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/MutableRevisionSlotsTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/PreMcrSchemaOverride.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RenderedRevisionTest.php
tests/phpunit/includes/Revision/RevisionArchiveRecordTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RevisionQueryInfoTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RevisionRecordTests.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RevisionRendererTest.php
tests/phpunit/includes/Revision/RevisionSlotsTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RevisionStoreRecordTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/RevisionStoreTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/SlotRecordTest.php [new file with mode: 0644]
tests/phpunit/includes/Revision/create-pre-mcr-fields.sql [new file with mode: 0644]
tests/phpunit/includes/Revision/drop-mcr-tables.sql [new file with mode: 0644]
tests/phpunit/includes/Revision/drop-pre-mcr-fields.sql [new file with mode: 0644]
tests/phpunit/includes/Revision/drop-pre-mcr-fields.sqlite.sql [new file with mode: 0644]
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionMcrDbTest.php
tests/phpunit/includes/RevisionMcrReadNewDbTest.php
tests/phpunit/includes/RevisionMcrWriteBothDbTest.php
tests/phpunit/includes/RevisionNoContentModelDbTest.php
tests/phpunit/includes/RevisionPreMcrDbTest.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php
tests/phpunit/includes/Storage/McrReadNewRevisionStoreDbTest.php [deleted file]
tests/phpunit/includes/Storage/McrReadNewSchemaOverride.php [deleted file]
tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php [deleted file]
tests/phpunit/includes/Storage/McrSchemaDetection.php [deleted file]
tests/phpunit/includes/Storage/McrSchemaOverride.php [deleted file]
tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php [deleted file]
tests/phpunit/includes/Storage/McrWriteBothSchemaOverride.php [deleted file]
tests/phpunit/includes/Storage/MutableRevisionRecordTest.php [deleted file]
tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php [deleted file]
tests/phpunit/includes/Storage/NameTableStoreFactoryTest.php
tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php [deleted file]
tests/phpunit/includes/Storage/PageUpdaterTest.php
tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php [deleted file]
tests/phpunit/includes/Storage/PreMcrSchemaOverride.php [deleted file]
tests/phpunit/includes/Storage/RevisionArchiveRecordTest.php [deleted file]
tests/phpunit/includes/Storage/RevisionQueryInfoTest.php [deleted file]
tests/phpunit/includes/Storage/RevisionRecordTests.php [deleted file]
tests/phpunit/includes/Storage/RevisionSlotsTest.php [deleted file]
tests/phpunit/includes/Storage/RevisionSlotsUpdateTest.php
tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php [deleted file]
tests/phpunit/includes/Storage/RevisionStoreFactoryTest.php [deleted file]
tests/phpunit/includes/Storage/RevisionStoreRecordTest.php [deleted file]
tests/phpunit/includes/Storage/RevisionStoreTest.php [deleted file]
tests/phpunit/includes/Storage/SlotRecordTest.php [deleted file]
tests/phpunit/includes/Storage/create-pre-mcr-fields.sql [deleted file]
tests/phpunit/includes/Storage/drop-mcr-tables.sql [deleted file]
tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql [deleted file]
tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql [deleted file]
tests/phpunit/includes/TitlePermissionTest.php
tests/phpunit/includes/api/ApiLoginTest.php
tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php
tests/phpunit/includes/api/query/ApiQueryUserContribsTest.php
tests/phpunit/includes/auth/LegacyHookPreAuthenticationProviderTest.php
tests/phpunit/includes/changetags/ChangeTagsTest.php
tests/phpunit/includes/content/WikitextContentHandlerTest.php
tests/phpunit/includes/db/DatabaseTestHelper.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/diff/DifferenceEngineTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php
tests/phpunit/includes/logging/DatabaseLogEntryTest.php
tests/phpunit/includes/page/ArticleViewTest.php
tests/phpunit/includes/page/PageArchiveMcrTest.php
tests/phpunit/includes/page/PageArchivePreMcrTest.php
tests/phpunit/includes/page/PageArchiveTestBase.php
tests/phpunit/includes/page/WikiPageDbTestBase.php
tests/phpunit/includes/page/WikiPageMcrDbTest.php
tests/phpunit/includes/page/WikiPageMcrReadNewDbTest.php
tests/phpunit/includes/page/WikiPageMcrWriteBothDbTest.php
tests/phpunit/includes/page/WikiPageNoContentModelDbTest.php
tests/phpunit/includes/page/WikiPagePreMcrDbTest.php
tests/phpunit/includes/parser/ParserMethodsTest.php
tests/phpunit/includes/poolcounter/PoolWorkArticleViewTest.php
tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php
tests/phpunit/includes/user/BotPasswordTest.php
tests/phpunit/includes/user/UserTest.php

index 63d0894..efc3a2e 100644 (file)
@@ -40,6 +40,10 @@ production.
   old and the new schema, but reading the new schema, so Multi-Content Revisions
   (MCR) are now functional per default. The new default value of the setting is
   SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW.
+* $wgActorTableSchemaMigrationStage no longer accepts MIGRATION_WRITE_BOTH or
+  MIGRATION_WRITE_NEW. It instead uses SCHEMA_COMPAT_WRITE_BOTH |
+  SCHEMA_COMPAT_READ_OLD and SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
+  for intermediate stages of migration.
 
 ==== Removed configuration ====
 * $wgEnableAPI and $wgEnableWriteAPI – These settings, deprecated in 1.31,
@@ -100,7 +104,8 @@ production.
 === External library changes in 1.32 ===
 
 ==== New external libraries ====
-* Added wikimedia/xmp-reader 0.6.0
+* Added wikimedia/xmp-reader 0.6.0.
+* Added Add pear/Net_SMTP 1.8.0.
 * …
 
 ==== Changed external libraries ====
@@ -116,6 +121,7 @@ production.
 * Updated jquery from v3.2.1 to v3.3.1.
 
 ==== Removed external libraries ====
+* pear/mail_mime-decode was removed.
 * …
 
 === Bug fixes in 1.32 ===
@@ -329,9 +335,11 @@ because of Phabricator reports.
   phase.
 * The global function wfErrorLog, deprecated since 1.25, has now been removed.
   Use MWLoggerLegacyLogger::emit or UDPTransport.
- The hooks 'SpecialRecentChangesQuery' & 'SpecialWatchlistQuery', deprecated in
+* The hooks 'SpecialRecentChangesQuery' & 'SpecialWatchlistQuery', deprecated in
   1.23, were removed. Instead, use ChangesListSpecialPageStructuredFilters or
   ChangesListSpecialPageQuery.
+* The global function wfUsePHP, deprecated since 1.30, has now been removed. To
+  assert a newer version of PHP than MediaWiki does, use extension registration.
 
 === Deprecations in 1.32 ===
 * HTMLForm::setSubmitProgressive() is deprecated. No need to call it. Submit
@@ -472,6 +480,14 @@ because of Phabricator reports.
   $wgTidyConfig instead.
 * All Tidy configurations other than Remex have been hard deprecated;
   future parsers will not emit compatible output for these configurations.
+* (T198214) OutputPage::addWikiText(), OutputPage::addWikiTextWithTitle(),
+  and OutputPage::addWikiTextTitle() have been deprecated, since they
+  can result in untidy output.  In addition OutputPage::addWikiTextTidy()
+  and OutputPage::addWikiTextTitleTidy() was deprecated to make naming new
+  methods consistent.  Use OutputPage::addWikiTextAsInterface() or
+  OutputPage::addWikiTextAsContent() instead, which ensures the output is
+  tidy and clarifies whether content-language specific postprocessing should
+  be done on the text.
 * QuickTemplate::msgHtml() and BaseTemplate::msgHtml() have been deprecated
   as they promote bad practises. I18n messages should always be properly
   escaped.
@@ -482,6 +498,18 @@ because of Phabricator reports.
   deprecated. Use CommentStore::insert() instead.
 * Language::setCode is deprecated as public function. Use Language::factory
   to create a new Language object with a different language code.
+* Several classes have been moved from the MediaWiki\Storage\ namespace to the
+  MediaWiki\Revision\ namespace. The old class names are aliased for
+  compatibility, but are deprecated. Classes are IncompleteRevisionException,
+  MutableRevisionRecord, MutableRevisionSlots, RevisionAccessException,
+  RevisionArchiveRecord, RevisionFactory, RevisionLookup, RevisionRecord,
+  RevisionSlots, RevisionStore, RevisionStoreRecord, SlotRecord, and
+  SuppressedDataException.
+* When using OOUI HTMLForm containing an 'info' field which uses the 'rawrow'
+  option, it is now deprecated to give its contents (the 'default' option)
+  as a string. They should be given as a OOUI\FieldLayout object instead.
+  Notably, this affects fields defined in the 'GetPreferences' hook, because
+  Special:Preferences uses an OOUI form now. (If possible, don't use 'rawrow'.)
 
 === Other changes in 1.32 ===
 * (T198811) The following tables have had their UNIQUE indexes turned into
index ac47093..0f92ccb 100644 (file)
@@ -384,6 +384,7 @@ $wgAutoloadLocalClasses = [
        'DeleteLogFormatter' => __DIR__ . '/includes/logging/DeleteLogFormatter.php',
        'DeleteOldRevisions' => __DIR__ . '/maintenance/deleteOldRevisions.php',
        'DeleteOrphanedRevisions' => __DIR__ . '/maintenance/deleteOrphanedRevisions.php',
+       'DeletePageJob' => __DIR__ . '/includes/jobqueue/jobs/DeletePageJob.php',
        'DeleteSelfExternals' => __DIR__ . '/maintenance/deleteSelfExternals.php',
        'DeletedContribsPager' => __DIR__ . '/includes/specials/pagers/DeletedContribsPager.php',
        'DeletedContributionsPage' => __DIR__ . '/includes/specials/SpecialDeletedContributions.php',
@@ -896,13 +897,23 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php',
        'MediaWiki\\OutputHandler' => __DIR__ . '/includes/OutputHandler.php',
        'MediaWiki\\ProcOpenError' => __DIR__ . '/includes/exception/ProcOpenError.php',
-       'MediaWiki\\Revision\\RenderedRevision' => __DIR__ . '/includes/Revision/RenderedRevision.php',
-       'MediaWiki\\Revision\\RevisionRenderer' => __DIR__ . '/includes/Revision/RevisionRenderer.php',
-       'MediaWiki\\Revision\\SlotRenderingProvider' => __DIR__ . '/includes/Revision/SlotRenderingProvider.php',
        'MediaWiki\\Search\\ParserOutputSearchDataExtractor' => __DIR__ . '/includes/search/ParserOutputSearchDataExtractor.php',
        'MediaWiki\\ShellDisabledError' => __DIR__ . '/includes/exception/ShellDisabledError.php',
        'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php',
        'MediaWiki\\Special\\SpecialPageFactory' => __DIR__ . '/includes/specialpage/SpecialPageFactory.php',
+       'MediaWiki\\Storage\\IncompleteRevisionException' => __DIR__ . '/includes/Revision/IncompleteRevisionException.php',
+       'MediaWiki\\Storage\\MutableRevisionRecord' => __DIR__ . '/includes/Revision/MutableRevisionRecord.php',
+       'MediaWiki\\Storage\\MutableRevisionSlots' => __DIR__ . '/includes/Revision/MutableRevisionSlots.php',
+       'MediaWiki\\Storage\\RevisionAccessException' => __DIR__ . '/includes/Revision/RevisionAccessException.php',
+       'MediaWiki\\Storage\\RevisionArchiveRecord' => __DIR__ . '/includes/Revision/RevisionArchiveRecord.php',
+       'MediaWiki\\Storage\\RevisionFactory' => __DIR__ . '/includes/Revision/RevisionFactory.php',
+       'MediaWiki\\Storage\\RevisionLookup' => __DIR__ . '/includes/Revision/RevisionLookup.php',
+       'MediaWiki\\Storage\\RevisionRecord' => __DIR__ . '/includes/Revision/RevisionRecord.php',
+       'MediaWiki\\Storage\\RevisionSlots' => __DIR__ . '/includes/Revision/RevisionSlots.php',
+       'MediaWiki\\Storage\\RevisionStore' => __DIR__ . '/includes/Revision/RevisionStore.php',
+       'MediaWiki\\Storage\\RevisionStoreRecord' => __DIR__ . '/includes/Revision/RevisionStoreRecord.php',
+       'MediaWiki\\Storage\\SlotRecord' => __DIR__ . '/includes/Revision/SlotRecord.php',
+       'MediaWiki\\Storage\\SuppressedDataException' => __DIR__ . '/includes/Revision/SuppressedDataException.php',
        'MediaWiki\\User\\UserIdentity' => __DIR__ . '/includes/user/UserIdentity.php',
        'MediaWiki\\User\\UserIdentityValue' => __DIR__ . '/includes/user/UserIdentityValue.php',
        'MediaWiki\\Widget\\CheckMatrixWidget' => __DIR__ . '/includes/widget/CheckMatrixWidget.php',
index 934943e..2e13f90 100644 (file)
@@ -29,7 +29,7 @@
                "oyejorge/less.php": "1.7.0.14",
                "pear/mail": "1.4.1",
                "pear/mail_mime": "1.10.2",
-               "pear/mail_mime-decode": "1.5.5.2",
+               "pear/net_smtp": "1.8.0",
                "php": ">=5.6.99",
                "psr/log": "1.0.2",
                "wikimedia/assert": "0.2.2",
index 51dfa60..0c33eb9 100644 (file)
@@ -34,6 +34,12 @@ use Wikimedia\Rdbms\IDatabase;
  */
 class ActorMigration {
 
+       /**
+        * Constant for extensions to feature-test whether $wgActorTableSchemaMigrationStage
+        * expects MIGRATION_* or SCHEMA_COMPAT_*
+        */
+       const MIGRATION_STAGE_SCHEMA_COMPAT = 1;
+
        /**
         * Define fields that use temporary tables for transitional purposes
         * @var array Keys are '$key', values are arrays with four fields:
@@ -74,11 +80,27 @@ class ActorMigration {
        /** @var array|null Cache for `self::getJoin()` */
        private $joinCache = null;
 
-       /** @var int One of the MIGRATION_* constants */
+       /** @var int Combination of SCHEMA_COMPAT_* constants */
        private $stage;
 
        /** @private */
        public function __construct( $stage ) {
+               if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) {
+                       throw new InvalidArgumentException( '$stage must include a write mode' );
+               }
+               if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) {
+                       throw new InvalidArgumentException( '$stage must include a read mode' );
+               }
+               if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_BOTH ) {
+                       throw new InvalidArgumentException( 'Cannot read both schemas' );
+               }
+               if ( ( $stage & SCHEMA_COMPAT_READ_OLD ) && !( $stage & SCHEMA_COMPAT_WRITE_OLD ) ) {
+                       throw new InvalidArgumentException( 'Cannot read the old schema without also writing it' );
+               }
+               if ( ( $stage & SCHEMA_COMPAT_READ_NEW ) && !( $stage & SCHEMA_COMPAT_WRITE_NEW ) ) {
+                       throw new InvalidArgumentException( 'Cannot read the new schema without also writing it' );
+               }
+
                $this->stage = $stage;
        }
 
@@ -96,7 +118,7 @@ class ActorMigration {
         * @return string
         */
        public function isAnon( $field ) {
-               return $this->stage === MIGRATION_NEW ? "$field IS NULL" : "$field = 0";
+               return ( $this->stage & SCHEMA_COMPAT_READ_NEW ) ? "$field IS NULL" : "$field = 0";
        }
 
        /**
@@ -105,7 +127,7 @@ class ActorMigration {
         * @return string
         */
        public function isNotAnon( $field ) {
-               return $this->stage === MIGRATION_NEW ? "$field IS NOT NULL" : "$field != 0";
+               return ( $this->stage & SCHEMA_COMPAT_READ_NEW ) ? "$field IS NOT NULL" : "$field != 0";
        }
 
        /**
@@ -140,18 +162,16 @@ class ActorMigration {
 
                        list( $text, $actor ) = self::getFieldNames( $key );
 
-                       if ( $this->stage === MIGRATION_OLD ) {
+                       if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) {
                                $fields[$key] = $key;
                                $fields[$text] = $text;
                                $fields[$actor] = 'NULL';
                        } else {
-                               $join = $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN';
-
                                if ( isset( self::$tempTables[$key] ) ) {
                                        $t = self::$tempTables[$key];
                                        $alias = "temp_$key";
                                        $tables[$alias] = $t['table'];
-                                       $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
+                                       $joins[$alias] = [ 'JOIN', "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
                                        $joinField = "{$alias}.{$t['field']}";
                                } else {
                                        $joinField = $actor;
@@ -159,15 +179,10 @@ class ActorMigration {
 
                                $alias = "actor_$key";
                                $tables[$alias] = 'actor';
-                               $joins[$alias] = [ $join, "{$alias}.actor_id = {$joinField}" ];
+                               $joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$joinField}" ];
 
-                               if ( $this->stage === MIGRATION_NEW ) {
-                                       $fields[$key] = "{$alias}.actor_user";
-                                       $fields[$text] = "{$alias}.actor_name";
-                               } else {
-                                       $fields[$key] = "COALESCE( {$alias}.actor_user, $key )";
-                                       $fields[$text] = "COALESCE( {$alias}.actor_name, $text )";
-                               }
+                               $fields[$key] = "{$alias}.actor_user";
+                               $fields[$text] = "{$alias}.actor_name";
                                $fields[$actor] = $joinField;
                        }
 
@@ -197,11 +212,11 @@ class ActorMigration {
 
                list( $text, $actor ) = self::getFieldNames( $key );
                $ret = [];
-               if ( $this->stage <= MIGRATION_WRITE_BOTH ) {
+               if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
                        $ret[$key] = $user->getId();
                        $ret[$text] = $user->getName();
                }
-               if ( $this->stage >= MIGRATION_WRITE_BOTH ) {
+               if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
                        // We need to be able to assign an actor ID if none exists
                        if ( !$user instanceof User && !$user->getActorId() ) {
                                $user = User::newFromAnyId( $user->getId(), $user->getName(), null );
@@ -233,11 +248,11 @@ class ActorMigration {
                list( $text, $actor ) = self::getFieldNames( $key );
                $ret = [];
                $callback = null;
-               if ( $this->stage <= MIGRATION_WRITE_BOTH ) {
+               if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
                        $ret[$key] = $user->getId();
                        $ret[$text] = $user->getName();
                }
-               if ( $this->stage >= MIGRATION_WRITE_BOTH ) {
+               if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
                        // We need to be able to assign an actor ID if none exists
                        if ( !$user instanceof User && !$user->getActorId() ) {
                                $user = User::newFromAnyId( $user->getId(), $user->getName(), null );
@@ -301,6 +316,8 @@ class ActorMigration {
         *   - orconds: (array[]) array of alternatives in case a union of multiple
         *     queries would be more efficient than a query with OR. May have keys
         *     'actor', 'userid', 'username'.
+        *     Since 1.32, this is guaranteed to contain just one alternative if
+        *     $users contains a single user.
         *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
         *  All tables and joins are aliased, so `+` is safe to use.
         */
@@ -332,44 +349,26 @@ class ActorMigration {
                list( $text, $actor ) = self::getFieldNames( $key );
 
                // Combine data into conditions to be ORed together
-               $actorNotEmpty = [];
-               if ( $this->stage === MIGRATION_OLD ) {
-                       $actors = [];
-                       $actorEmpty = [];
-               } elseif ( isset( self::$tempTables[$key] ) ) {
-                       $t = self::$tempTables[$key];
-                       $alias = "temp_$key";
-                       $tables[$alias] = $t['table'];
-                       $joins[$alias] = [
-                               $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
-                               "{$alias}.{$t['pk']} = {$t['joinPK']}"
-                       ];
-                       $joinField = "{$alias}.{$t['field']}";
-                       $actorEmpty = [ $joinField => null ];
-                       if ( $this->stage !== MIGRATION_NEW ) {
-                               // Otherwise the resulting test can evaluate to NULL, and
-                               // NOT(NULL) is NULL rather than true.
-                               $actorNotEmpty = [ "$joinField IS NOT NULL" ];
+               if ( $this->stage & SCHEMA_COMPAT_READ_NEW ) {
+                       if ( $actors ) {
+                               if ( isset( self::$tempTables[$key] ) ) {
+                                       $t = self::$tempTables[$key];
+                                       $alias = "temp_$key";
+                                       $tables[$alias] = $t['table'];
+                                       $joins[$alias] = [ 'JOIN', "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
+                                       $joinField = "{$alias}.{$t['field']}";
+                               } else {
+                                       $joinField = $actor;
+                               }
+                               $conds['actor'] = $db->makeList( [ $joinField => $actors ], IDatabase::LIST_AND );
                        }
                } else {
-                       $joinField = $actor;
-                       $actorEmpty = [ $joinField => 0 ];
-               }
-
-               if ( $actors ) {
-                       $conds['actor'] = $db->makeList(
-                               $actorNotEmpty + [ $joinField => $actors ], IDatabase::LIST_AND
-                       );
-               }
-               if ( $this->stage < MIGRATION_NEW && $ids ) {
-                       $conds['userid'] = $db->makeList(
-                               $actorEmpty + [ $key => $ids ], IDatabase::LIST_AND
-                       );
-               }
-               if ( $this->stage < MIGRATION_NEW && $names ) {
-                       $conds['username'] = $db->makeList(
-                               $actorEmpty + [ $text => $names ], IDatabase::LIST_AND
-                       );
+                       if ( $ids ) {
+                               $conds['userid'] = $db->makeList( [ $key => $ids ], IDatabase::LIST_AND );
+                       }
+                       if ( $names ) {
+                               $conds['username'] = $db->makeList( [ $text => $names ], IDatabase::LIST_AND );
+                       }
                }
 
                return [
index 5482f6a..e4e59da 100644 (file)
@@ -134,6 +134,7 @@ class AutoLoader {
                        'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/',
                        'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
                        'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/',
+                       'MediaWiki\\Revision\\' => __DIR__ . '/Revision/',
                        'MediaWiki\\Services\\' => __DIR__ . '/services/',
                        'MediaWiki\\Session\\' => __DIR__ . '/session/',
                        'MediaWiki\\Shell\\' => __DIR__ . '/shell/',
index 913aeb9..bf8bad1 100644 (file)
@@ -208,13 +208,14 @@ class Block {
        public static function selectFields() {
                global $wgActorTableSchemaMigrationStage;
 
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                        // If code is using this instead of self::getQueryInfo(), there's a
                        // decent chance it's going to try to directly access
                        // $row->ipb_by or $row->ipb_by_text and we can't give it
-                       // useful values here once those aren't being written anymore.
+                       // useful values here once those aren't being used anymore.
                        throw new BadMethodCallException(
-                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                               'Cannot use ' . __METHOD__
+                                       . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW'
                        );
                }
 
@@ -224,7 +225,7 @@ class Block {
                        'ipb_address',
                        'ipb_by',
                        'ipb_by_text',
-                       'ipb_by_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'ipb_by_actor' : 'NULL',
+                       'ipb_by_actor' => 'NULL',
                        'ipb_timestamp',
                        'ipb_auto',
                        'ipb_anon_only',
index 4fee5d7..4ed1707 100644 (file)
@@ -5534,6 +5534,12 @@ $wgAvailableRights = [];
  */
 $wgDeleteRevisionsLimit = 0;
 
+/**
+ * Page deletions with > this number of revisions will use the job queue.
+ * Revisions will be archived in batches of (at most) this size, one batch per job.
+ */
+$wgDeleteRevisionsBatchSize = 1000;
+
 /**
  * The maximum number of edits a user can have and
  * can still be hidden by users with the hideuser permission.
@@ -7518,6 +7524,7 @@ $wgServiceWiringFiles = [
  * or (since 1.30) a callback to use for creating the job object.
  */
 $wgJobClasses = [
+       'deletePage' => DeletePageJob::class,
        'refreshLinks' => RefreshLinksJob::class,
        'deleteLinks' => DeleteLinksJob::class,
        'htmlCacheUpdate' => HTMLCacheUpdateJob::class,
@@ -8977,10 +8984,21 @@ $wgMultiContentRevisionSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_
 
 /**
  * Actor table schema migration stage.
+ *
+ * Use the SCHEMA_COMPAT_XXX flags. Supported values:
+ * - SCHEMA_COMPAT_OLD
+ * - SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+ * - SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
+ * - SCHEMA_COMPAT_NEW
+ *
+ * Note that reading the old and new schema at the same time is not supported
+ * in 1.32, but was (with significant query performance issues) in 1.31.
+ *
  * @since 1.31
- * @var int One of the MIGRATION_* constants
+ * @since 1.32 changed allowed flags
+ * @var int An appropriate combination of SCHEMA_COMPAT_XXX flags.
  */
-$wgActorTableSchemaMigrationStage = MIGRATION_OLD;
+$wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_OLD;
 
 /**
  * Temporary option to disable the date picker from the Expiry Widget.
index 29abfb1..6b79538 100644 (file)
@@ -2666,8 +2666,9 @@ ERROR;
                        $title = Title::newFromText( $this->editintro );
                        if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
                                // Added using template syntax, to take <noinclude>'s into account.
-                               $this->context->getOutput()->addWikiTextTitleTidy(
+                               $this->context->getOutput()->addWikiTextAsContent(
                                        '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
+                                       /*linestart*/true,
                                        $this->mTitle
                                );
                                return true;
index 868fda3..49a2612 100644 (file)
@@ -2455,33 +2455,6 @@ function wfDiff( $before, $after, $params = '-u' ) {
        return $diff;
 }
 
-/**
- * This function works like "use VERSION" in Perl, the program will die with a
- * backtrace if the current version of PHP is less than the version provided
- *
- * This is useful for extensions which due to their nature are not kept in sync
- * with releases, and might depend on other versions of PHP than the main code
- *
- * Note: PHP might die due to parsing errors in some cases before it ever
- *       manages to call this function, such is life
- *
- * @see perldoc -f use
- *
- * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float
- *
- * @deprecated since 1.30
- *
- * @throws MWException
- */
-function wfUsePHP( $req_ver ) {
-       wfDeprecated( __FUNCTION__, '1.30' );
-       $php_ver = PHP_VERSION;
-
-       if ( version_compare( $php_ver, (string)$req_ver, '<' ) ) {
-               throw new MWException( "PHP $req_ver required--this is only $php_ver" );
-       }
-}
-
 /**
  * Return the final portion of a pathname.
  * Reimplemented because PHP5's "basename()" is buggy with multibyte text.
index e10a530..4636ba3 100644 (file)
@@ -369,11 +369,6 @@ class MediaWiki {
                        }
                        throw new HttpError( 500, $message );
                }
-               // Protect against redirects to NS_MEDIA namespace
-               // when the user probably wants NS_FILE
-               if ( $title->inNamespace( NS_MEDIA ) ) {
-                       $title->mNamespace = NS_FILE;
-               }
                $output->setCdnMaxage( 1200 );
                $output->redirect( $targetUrl, '301' );
                return true;
index b236ca1..0acd55a 100644 (file)
@@ -22,11 +22,11 @@ use MediaWiki\Storage\BlobStore;
 use MediaWiki\Storage\BlobStoreFactory;
 use MediaWiki\Storage\NameTableStore;
 use MediaWiki\Storage\NameTableStoreFactory;
-use MediaWiki\Storage\RevisionFactory;
-use MediaWiki\Storage\RevisionLookup;
-use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Revision\RevisionFactory;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionStore;
 use OldRevisionImporter;
-use MediaWiki\Storage\RevisionStoreFactory;
+use MediaWiki\Revision\RevisionStoreFactory;
 use UploadRevisionImporter;
 use Wikimedia\Rdbms\LBFactory;
 use LinkCache;
index 11abc81..5424b13 100644 (file)
@@ -135,13 +135,16 @@ class OrderedStreamingForkController extends ForkController {
        protected function consumeNoFork() {
                while ( !feof( $this->input ) ) {
                        $data = fgets( $this->input );
-                       if ( $data[ strlen( $data ) - 1 ] == "\n" ) {
+                       if ( substr( $data, -1 ) === "\n" ) {
+                               // Strip any final new line used to delimit lines of input.
+                               // The last line of input might not have it, though.
                                $data = substr( $data, 0, -1 );
                        }
-                       if ( strlen( $data ) !== 0 ) {
-                               $result = call_user_func( $this->workCallback, $data );
-                               fwrite( $this->output, "$result\n" );
+                       if ( $data === '' ) {
+                               continue;
                        }
+                       $result = call_user_func( $this->workCallback, $data );
+                       fwrite( $this->output, "$result\n" );
                }
        }
 
@@ -163,12 +166,12 @@ class OrderedStreamingForkController extends ForkController {
                                        $this->updateAvailableSockets( $sockets, $used, $sockets ? 0 : 5 );
                                } while ( !$sockets );
                        }
-                       // Strip the trailing \n. The last line of a file might not have a trailing
-                       // \n though
-                       if ( $data[ strlen( $data ) - 1 ] == "\n" ) {
+                       if ( substr( $data, - 1 ) === "\n" ) {
+                               // Strip any final new line used to delimit lines of input.
+                               // The last line of input might not have it, though.
                                $data = substr( $data, 0, -1 );
                        }
-                       if ( strlen( $data ) === 0 ) {
+                       if ( $data === '' ) {
                                continue;
                        }
                        $socket = array_pop( $sockets );
index dd2f5ac..b1874b9 100644 (file)
@@ -1748,13 +1748,71 @@ class OutputPage extends ContextSource {
         * @param bool $linestart Is this the start of a line?
         * @param bool $interface Is this text in the user interface language?
         * @throws MWException
+        * @deprecated since 1.32 due to untidy output; use
+        *    addWikiTextAsInterface() if $interface is default value or true,
+        *    or else addWikiTextAsContent() if $interface is false.
         */
        public function addWikiText( $text, $linestart = true, $interface = true ) {
-               $title = $this->getTitle(); // Work around E_STRICT
+               $title = $this->getTitle();
+               if ( !$title ) {
+                       throw new MWException( 'Title is null' );
+               }
+               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/false, $interface );
+       }
+
+       /**
+        * Convert wikitext *in the user interface language* to HTML and
+        * add it to the buffer. The result will not be
+        * language-converted, as user interface messages are already
+        * localized into a specific variant.  Assumes that the current
+        * page title will be used if optional $title is not
+        * provided. Output will be tidy.
+        *
+        * @param string $text Wikitext in the user interface language
+        * @param bool $linestart Is this the start of a line? (Defaults to true)
+        * @param Title|null $title Optional title to use; default of `null`
+        *   means use current page title.
+        * @throws MWException if $title is not provided and OutputPage::getTitle()
+        *   is null
+        * @since 1.32
+        */
+       public function addWikiTextAsInterface(
+               $text, $linestart = true, Title $title = null
+       ) {
+               if ( $title === null ) {
+                       $title = $this->getTitle();
+               }
+               if ( !$title ) {
+                       throw new MWException( 'Title is null' );
+               }
+               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/true );
+       }
+
+       /**
+        * Convert wikitext *in the page content language* to HTML and add
+        * it to the buffer.  The result with be language-converted to the
+        * user's preferred variant.  Assumes that the current page title
+        * will be used if optional $title is not provided. Output will be
+        * tidy.
+        *
+        * @param string $text Wikitext in the page content language
+        * @param bool $linestart Is this the start of a line? (Defaults to true)
+        * @param Title|null $title Optional title to use; default of `null`
+        *   means use current page title.
+        * @throws MWException if $title is not provided and OutputPage::getTitle()
+        *   is null
+        * @since 1.32
+        */
+       public function addWikiTextAsContent(
+               $text, $linestart = true, Title $title = null
+       ) {
+               if ( $title === null ) {
+                       $title = $this->getTitle();
+               }
                if ( !$title ) {
                        throw new MWException( 'Title is null' );
                }
-               $this->addWikiTextTitle( $text, $title, $linestart, /*tidy*/false, $interface );
+               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/false );
        }
 
        /**
@@ -1763,31 +1821,44 @@ class OutputPage extends ContextSource {
         * @param string $text Wikitext
         * @param Title $title
         * @param bool $linestart Is this the start of a line?
+        * @deprecated since 1.32 due to untidy output; use
+        *   addWikiTextAsInterface()
         */
        public function addWikiTextWithTitle( $text, Title $title, $linestart = true ) {
-               $this->addWikiTextTitle( $text, $title, $linestart );
+               wfDeprecated( __METHOD__, '1.32' );
+               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/false, /*interface*/false );
        }
 
        /**
-        * Add wikitext with a custom Title object and tidy enabled.
+        * Add wikitext *in content language* with a custom Title object.
+        * Output will be tidy.
         *
-        * @param string $text Wikitext
+        * @param string $text Wikitext in content language
         * @param Title $title
         * @param bool $linestart Is this the start of a line?
+        * @deprecated since 1.32 to rename methods consistently; use
+        *   addWikiTextAsContent()
         */
        function addWikiTextTitleTidy( $text, Title $title, $linestart = true ) {
-               $this->addWikiTextTitle( $text, $title, $linestart, true );
+               wfDeprecated( __METHOD__, '1.32' );
+               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/false );
        }
 
        /**
-        * Add wikitext with tidy enabled
+        * Add wikitext *in content language*. Output will be tidy.
         *
-        * @param string $text Wikitext
+        * @param string $text Wikitext in content language
         * @param bool $linestart Is this the start of a line?
+        * @deprecated since 1.32 to rename methods consistently; use
+        *   addWikiTextAsContent()
         */
        public function addWikiTextTidy( $text, $linestart = true ) {
+               wfDeprecated( __METHOD__, '1.32' );
                $title = $this->getTitle();
-               $this->addWikiTextTitleTidy( $text, $title, $linestart );
+               if ( !$title ) {
+                       throw new MWException( 'Title is null' );
+               }
+               $this->addWikiTextTitleInternal( $text, $title, $linestart, /*tidy*/true, /*interface*/false );
        }
 
        /**
@@ -1797,12 +1868,41 @@ class OutputPage extends ContextSource {
         * @param string $text Wikitext
         * @param Title $title
         * @param bool $linestart Is this the start of a line?
-        * @param bool $tidy Whether to use tidy
+        * @param bool $tidy Whether to use tidy.
+        *             Setting this to false (or omitting it) is deprecated
+        *             since 1.32; all wikitext should be tidied.
+        *             For backwards-compatibility with prior MW releases,
+        *             you may wish to invoke this method but set $tidy=true;
+        *             this will result in equivalent output to the non-deprecated
+        *             addWikiTextAsContent()/addWikiTextAsInterface() methods.
         * @param bool $interface Whether it is an interface message
         *   (for example disables conversion)
+        * @deprecated since 1.32, use addWikiTextAsContent() or
+        *   addWikiTextAsInterface() (depending on $interface)
         */
        public function addWikiTextTitle( $text, Title $title, $linestart,
                $tidy = false, $interface = false
+       ) {
+               wfDeprecated( __METHOD__, '1.32' );
+               return $this->addWikiTextTitleInternal( $text, $title, $linestart, $tidy, $interface );
+       }
+
+       /**
+        * Add wikitext with a custom Title object.
+        * Output is unwrapped.
+        *
+        * @param string $text Wikitext
+        * @param Title $title
+        * @param bool $linestart Is this the start of a line?
+        * @param bool $tidy Whether to use tidy.
+        *             Setting this to false (or omitting it) is deprecated
+        *             since 1.32; all wikitext should be tidied.
+        * @param bool $interface Whether it is an interface message
+        *   (for example disables conversion)
+        * @private
+        */
+       private function addWikiTextTitleInternal(
+               $text, Title $title, $linestart, $tidy, $interface
        ) {
                global $wgParser;
 
index e45c6dc..e8fe8bd 100644 (file)
  * @file
  */
 
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionFactory;
-use MediaWiki\Storage\RevisionLookup;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionFactory;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreRecord;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\SqlBlobStore;
 use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\Linker\LinkTarget;
@@ -318,12 +318,13 @@ class Revision implements IDBAccessObject {
                global $wgActorTableSchemaMigrationStage;
 
                wfDeprecated( __METHOD__, '1.31' );
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                        // If code is using this instead of self::getQueryInfo(), there's
                        // no way the join it's trying to do can work once the old fields
-                       // aren't being written anymore.
+                       // aren't being used anymore.
                        throw new BadMethodCallException(
-                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                               'Cannot use ' . __METHOD__
+                                       . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW'
                        );
                }
 
@@ -352,13 +353,14 @@ class Revision implements IDBAccessObject {
                global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage;
                global $wgMultiContentRevisionSchemaMigrationStage;
 
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                        // If code is using this instead of self::getQueryInfo(), there's a
                        // decent chance it's going to try to directly access
                        // $row->rev_user or $row->rev_user_text and we can't give it
-                       // useful values here once those aren't being written anymore.
+                       // useful values here once those aren't being used anymore.
                        throw new BadMethodCallException(
-                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                               'Cannot use ' . __METHOD__
+                                       . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW'
                        );
                }
 
@@ -411,13 +413,14 @@ class Revision implements IDBAccessObject {
                global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage;
                global $wgMultiContentRevisionSchemaMigrationStage;
 
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                        // If code is using this instead of self::getQueryInfo(), there's a
                        // decent chance it's going to try to directly access
                        // $row->ar_user or $row->ar_user_text and we can't give it
-                       // useful values here once those aren't being written anymore.
+                       // useful values here once those aren't being used anymore.
                        throw new BadMethodCallException(
-                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                               'Cannot use ' . __METHOD__
+                                       . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW'
                        );
                }
 
diff --git a/includes/Revision/IncompleteRevisionException.php b/includes/Revision/IncompleteRevisionException.php
new file mode 100644 (file)
index 0000000..808b0d2
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+/**
+ * Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\IncompleteRevisionException
+ */
+class IncompleteRevisionException extends RevisionAccessException {
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( IncompleteRevisionException::class, 'MediaWiki\Storage\IncompleteRevisionException' );
diff --git a/includes/Revision/MutableRevisionRecord.php b/includes/Revision/MutableRevisionRecord.php
new file mode 100644 (file)
index 0000000..f287c05
--- /dev/null
@@ -0,0 +1,344 @@
+<?php
+/**
+ * Mutable RevisionRecord implementation, for building new revision entries programmatically.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use CommentStoreComment;
+use Content;
+use InvalidArgumentException;
+use MediaWiki\Storage\RevisionSlotsUpdate;
+use MediaWiki\User\UserIdentity;
+use MWException;
+use Title;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Mutable RevisionRecord implementation, for building new revision entries programmatically.
+ * Provides setters for all fields.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\MutableRevisionRecord
+ */
+class MutableRevisionRecord extends RevisionRecord {
+
+       /**
+        * Returns an incomplete MutableRevisionRecord which uses $parent as its
+        * parent revision, and inherits all slots form it. If saved unchanged,
+        * the new revision will act as a null-revision.
+        *
+        * @param RevisionRecord $parent
+        *
+        * @return MutableRevisionRecord
+        */
+       public static function newFromParentRevision( RevisionRecord $parent ) {
+               // TODO: ideally, we wouldn't need a Title here
+               $title = Title::newFromLinkTarget( $parent->getPageAsLinkTarget() );
+               $rev = new MutableRevisionRecord( $title, $parent->getWikiId() );
+
+               foreach ( $parent->getSlotRoles() as $role ) {
+                       $slot = $parent->getSlot( $role, self::RAW );
+                       $rev->inheritSlot( $slot );
+               }
+
+               $rev->setPageId( $parent->getPageId() );
+               $rev->setParentId( $parent->getId() );
+
+               return $rev;
+       }
+
+       /**
+        * @note Avoid calling this constructor directly. Use the appropriate methods
+        * in RevisionStore instead.
+        *
+        * @param Title $title The title of the page this Revision is associated with.
+        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+        *        or false for the local site.
+        *
+        * @throws MWException
+        */
+       function __construct( Title $title, $wikiId = false ) {
+               $slots = new MutableRevisionSlots();
+
+               parent::__construct( $title, $slots, $wikiId );
+
+               $this->mSlots = $slots; // redundant, but nice for static analysis
+       }
+
+       /**
+        * @param int $parentId
+        */
+       public function setParentId( $parentId ) {
+               Assert::parameterType( 'integer', $parentId, '$parentId' );
+
+               $this->mParentId = $parentId;
+       }
+
+       /**
+        * Sets the given slot. If a slot with the same role is already present in the revision,
+        * it is replaced.
+        *
+        * @note This can only be used with a fresh "unattached" SlotRecord. Calling code that has a
+        * SlotRecord from another revision should use inheritSlot(). Calling code that has access to
+        * a Content object can use setContent().
+        *
+        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+        *
+        * @note Calling this method will cause the revision size and hash to be re-calculated upon
+        *       the next call to getSize() and getSha1(), respectively.
+        *
+        * @param SlotRecord $slot
+        */
+       public function setSlot( SlotRecord $slot ) {
+               if ( $slot->hasRevision() && $slot->getRevision() !== $this->getId() ) {
+                       throw new InvalidArgumentException(
+                               'The given slot must be an unsaved, unattached one. '
+                               . 'This slot is already attached to revision ' . $slot->getRevision() . '. '
+                               . 'Use inheritSlot() instead to preserve a slot from a previous revision.'
+                       );
+               }
+
+               $this->mSlots->setSlot( $slot );
+               $this->resetAggregateValues();
+       }
+
+       /**
+        * "Inherits" the given slot's content.
+        *
+        * If a slot with the same role is already present in the revision, it is replaced.
+        *
+        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+        *
+        * @param SlotRecord $parentSlot
+        */
+       public function inheritSlot( SlotRecord $parentSlot ) {
+               $this->mSlots->inheritSlot( $parentSlot );
+               $this->resetAggregateValues();
+       }
+
+       /**
+        * Sets the content for the slot with the given role.
+        *
+        * If a slot with the same role is already present in the revision, it is replaced.
+        * Calling code that has access to a SlotRecord can use inheritSlot() instead.
+        *
+        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+        *
+        * @note Calling this method will cause the revision size and hash to be re-calculated upon
+        *       the next call to getSize() and getSha1(), respectively.
+        *
+        * @param string $role
+        * @param Content $content
+        */
+       public function setContent( $role, Content $content ) {
+               $this->mSlots->setContent( $role, $content );
+               $this->resetAggregateValues();
+       }
+
+       /**
+        * Removes the slot with the given role from this revision.
+        * This effectively ends the "stream" with that role on the revision's page.
+        * Future revisions will no longer inherit this slot, unless it is added back explicitly.
+        *
+        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+        *
+        * @note Calling this method will cause the revision size and hash to be re-calculated upon
+        *       the next call to getSize() and getSha1(), respectively.
+        *
+        * @param string $role
+        */
+       public function removeSlot( $role ) {
+               $this->mSlots->removeSlot( $role );
+               $this->resetAggregateValues();
+       }
+
+       /**
+        * Applies the given update to the slots of this revision.
+        *
+        * @param RevisionSlotsUpdate $update
+        */
+       public function applyUpdate( RevisionSlotsUpdate $update ) {
+               $update->apply( $this->mSlots );
+       }
+
+       /**
+        * @param CommentStoreComment $comment
+        */
+       public function setComment( CommentStoreComment $comment ) {
+               $this->mComment = $comment;
+       }
+
+       /**
+        * Set revision hash, for optimization. Prevents getSha1() from re-calculating the hash.
+        *
+        * @note This should only be used if the calling code is sure that the given hash is correct
+        * for the revision's content, and there is no chance of the content being manipulated
+        * later. When in doubt, this method should not be called.
+        *
+        * @param string $sha1 SHA1 hash as a base36 string.
+        */
+       public function setSha1( $sha1 ) {
+               Assert::parameterType( 'string', $sha1, '$sha1' );
+
+               $this->mSha1 = $sha1;
+       }
+
+       /**
+        * Set nominal revision size, for optimization. Prevents getSize() from re-calculating the size.
+        *
+        * @note This should only be used if the calling code is sure that the given size is correct
+        * for the revision's content, and there is no chance of the content being manipulated
+        * later. When in doubt, this method should not be called.
+        *
+        * @param int $size nominal size in bogo-bytes
+        */
+       public function setSize( $size ) {
+               Assert::parameterType( 'integer', $size, '$size' );
+
+               $this->mSize = $size;
+       }
+
+       /**
+        * @param int $visibility
+        */
+       public function setVisibility( $visibility ) {
+               Assert::parameterType( 'integer', $visibility, '$visibility' );
+
+               $this->mDeleted = $visibility;
+       }
+
+       /**
+        * @param string $timestamp A timestamp understood by wfTimestamp
+        */
+       public function setTimestamp( $timestamp ) {
+               Assert::parameterType( 'string', $timestamp, '$timestamp' );
+
+               $this->mTimestamp = wfTimestamp( TS_MW, $timestamp );
+       }
+
+       /**
+        * @param bool $minorEdit
+        */
+       public function setMinorEdit( $minorEdit ) {
+               Assert::parameterType( 'boolean', $minorEdit, '$minorEdit' );
+
+               $this->mMinorEdit = $minorEdit;
+       }
+
+       /**
+        * Set the revision ID.
+        *
+        * MCR migration note: this replaces Revision::setId()
+        *
+        * @warning Use this with care, especially when preparing a revision for insertion
+        *          into the database! The revision ID should only be fixed in special cases
+        *          like preserving the original ID when restoring a revision.
+        *
+        * @param int $id
+        */
+       public function setId( $id ) {
+               Assert::parameterType( 'integer', $id, '$id' );
+
+               $this->mId = $id;
+       }
+
+       /**
+        * Sets the user identity associated with the revision
+        *
+        * @param UserIdentity $user
+        */
+       public function setUser( UserIdentity $user ) {
+               $this->mUser = $user;
+       }
+
+       /**
+        * @param int $pageId
+        */
+       public function setPageId( $pageId ) {
+               Assert::parameterType( 'integer', $pageId, '$pageId' );
+
+               if ( $this->mTitle->exists() && $pageId !== $this->mTitle->getArticleID() ) {
+                       throw new InvalidArgumentException(
+                               'The given Title does not belong to page ID ' . $this->mPageId
+                       );
+               }
+
+               $this->mPageId = $pageId;
+       }
+
+       /**
+        * Returns the nominal size of this revision.
+        *
+        * MCR migration note: this replaces Revision::getSize
+        *
+        * @return int The nominal size, may be computed on the fly if not yet known.
+        */
+       public function getSize() {
+               // If not known, re-calculate and remember. Will be reset when slots change.
+               if ( $this->mSize === null ) {
+                       $this->mSize = $this->mSlots->computeSize();
+               }
+
+               return $this->mSize;
+       }
+
+       /**
+        * Returns the base36 sha1 of this revision.
+        *
+        * MCR migration note: this replaces Revision::getSha1
+        *
+        * @return string The revision hash, may be computed on the fly if not yet known.
+        */
+       public function getSha1() {
+               // If not known, re-calculate and remember. Will be reset when slots change.
+               if ( $this->mSha1 === null ) {
+                       $this->mSha1 = $this->mSlots->computeSha1();
+               }
+
+               return $this->mSha1;
+       }
+
+       /**
+        * Returns the slots defined for this revision as a MutableRevisionSlots instance,
+        * which can be modified to defined the slots for this revision.
+        *
+        * @return MutableRevisionSlots
+        */
+       public function getSlots() {
+               // Overwritten just guarantee the more narrow return type.
+               return parent::getSlots();
+       }
+
+       /**
+        * Invalidate cached aggregate values such as hash and size.
+        */
+       private function resetAggregateValues() {
+               $this->mSize = null;
+               $this->mSha1 = null;
+       }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( MutableRevisionRecord::class, 'MediaWiki\Storage\MutableRevisionRecord' );
diff --git a/includes/Revision/MutableRevisionSlots.php b/includes/Revision/MutableRevisionSlots.php
new file mode 100644 (file)
index 0000000..cd4bc2e
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Mutable version of RevisionSlots, for constructing a new revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use Content;
+
+/**
+ * Mutable version of RevisionSlots, for constructing a new revision.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\MutableRevisionSlots
+ */
+class MutableRevisionSlots extends RevisionSlots {
+
+       /**
+        * Constructs a MutableRevisionSlots that inherits from the given
+        * list of slots.
+        *
+        * @param SlotRecord[] $slots
+        *
+        * @return MutableRevisionSlots
+        */
+       public static function newFromParentRevisionSlots( array $slots ) {
+               $inherited = [];
+               foreach ( $slots as $slot ) {
+                       $role = $slot->getRole();
+                       $inherited[$role] = SlotRecord::newInherited( $slot );
+               }
+
+               return new MutableRevisionSlots( $inherited );
+       }
+
+       /**
+        * @param SlotRecord[] $slots An array of SlotRecords.
+        */
+       public function __construct( array $slots = [] ) {
+               parent::__construct( $slots );
+       }
+
+       /**
+        * Sets the given slot.
+        * If a slot with the same role is already present, it is replaced.
+        *
+        * @param SlotRecord $slot
+        */
+       public function setSlot( SlotRecord $slot ) {
+               if ( !is_array( $this->slots ) ) {
+                       $this->getSlots(); // initialize $this->slots
+               }
+
+               $role = $slot->getRole();
+               $this->slots[$role] = $slot;
+       }
+
+       /**
+        * Sets the given slot to an inherited version of $slot.
+        * If a slot with the same role is already present, it is replaced.
+        *
+        * @param SlotRecord $slot
+        */
+       public function inheritSlot( SlotRecord $slot ) {
+               $this->setSlot( SlotRecord::newInherited( $slot ) );
+       }
+
+       /**
+        * Sets the content for the slot with the given role.
+        * If a slot with the same role is already present, it is replaced.
+        *
+        * @param string $role
+        * @param Content $content
+        */
+       public function setContent( $role, Content $content ) {
+               $slot = SlotRecord::newUnsaved( $role, $content );
+               $this->setSlot( $slot );
+       }
+
+       /**
+        * Remove the slot for the given role, discontinue the corresponding stream.
+        *
+        * @param string $role
+        */
+       public function removeSlot( $role ) {
+               if ( !is_array( $this->slots ) ) {
+                       $this->getSlots();  // initialize $this->slots
+               }
+
+               unset( $this->slots[$role] );
+       }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( MutableRevisionSlots::class, 'MediaWiki\Storage\MutableRevisionSlots' );
index fa16c61..9cb20e0 100644 (file)
@@ -24,8 +24,6 @@ namespace MediaWiki\Revision;
 
 use InvalidArgumentException;
 use LogicException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SuppressedDataException;
 use ParserOptions;
 use ParserOutput;
 use Psr\Log\LoggerInterface;
diff --git a/includes/Revision/RevisionAccessException.php b/includes/Revision/RevisionAccessException.php
new file mode 100644 (file)
index 0000000..290991e
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use RuntimeException;
+
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionAccessException
+ */
+class RevisionAccessException extends RuntimeException {
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionAccessException::class, 'MediaWiki\Storage\RevisionAccessException' );
diff --git a/includes/Revision/RevisionArchiveRecord.php b/includes/Revision/RevisionArchiveRecord.php
new file mode 100644 (file)
index 0000000..67dc9b2
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+/**
+ * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use CommentStoreComment;
+use MediaWiki\User\UserIdentity;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
+ * Most getters on RevisionArchiveRecord will never return null. However, getId() and
+ * getParentId() may indeed return null if this information was not stored when the archive entry
+ * was created.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionArchiveRecord
+ */
+class RevisionArchiveRecord extends RevisionRecord {
+
+       /**
+        * @var int
+        */
+       protected $mArchiveId;
+
+       /**
+        * @note Avoid calling this constructor directly. Use the appropriate methods
+        * in RevisionStore instead.
+        *
+        * @param Title $title The title of the page this Revision is associated with.
+        * @param UserIdentity $user
+        * @param CommentStoreComment $comment
+        * @param object $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build
+        *        a query that yields the required fields.
+        * @param RevisionSlots $slots The slots of this revision.
+        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+        *        or false for the local site.
+        */
+       function __construct(
+               Title $title,
+               UserIdentity $user,
+               CommentStoreComment $comment,
+               $row,
+               RevisionSlots $slots,
+               $wikiId = false
+       ) {
+               parent::__construct( $title, $slots, $wikiId );
+               Assert::parameterType( 'object', $row, '$row' );
+
+               $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
+               Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
+
+               $this->mArchiveId = intval( $row->ar_id );
+
+               // NOTE: ar_page_id may be different from $this->mTitle->getArticleID() in some cases,
+               // notably when a partially restored page has been moved, and a new page has been created
+               // with the same title. Archive rows for that title will then have the wrong page id.
+               $this->mPageId = isset( $row->ar_page_id ) ? intval( $row->ar_page_id ) : $title->getArticleID();
+
+               // NOTE: ar_parent_id = 0 indicates that there is no parent revision, while null
+               // indicates that the parent revision is unknown. As per MW 1.31, the database schema
+               // allows ar_parent_id to be NULL.
+               $this->mParentId = isset( $row->ar_parent_id ) ? intval( $row->ar_parent_id ) : null;
+               $this->mId = isset( $row->ar_rev_id ) ? intval( $row->ar_rev_id ) : null;
+               $this->mComment = $comment;
+               $this->mUser = $user;
+               $this->mTimestamp = $timestamp;
+               $this->mMinorEdit = boolval( $row->ar_minor_edit );
+               $this->mDeleted = intval( $row->ar_deleted );
+               $this->mSize = isset( $row->ar_len ) ? intval( $row->ar_len ) : null;
+               $this->mSha1 = !empty( $row->ar_sha1 ) ? $row->ar_sha1 : null;
+       }
+
+       /**
+        * Get archive row ID
+        *
+        * @return int
+        */
+       public function getArchiveId() {
+               return $this->mArchiveId;
+       }
+
+       /**
+        * @return int|null The revision id, or null if the original revision ID
+        *         was not recorded in the archive table.
+        */
+       public function getId() {
+               // overwritten just to refine the contract specification.
+               return parent::getId();
+       }
+
+       /**
+        * @throws RevisionAccessException if the size was unknown and could not be calculated.
+        * @return int The nominal revision size, never null. May be computed on the fly.
+        */
+       public function getSize() {
+               // If length is null, calculate and remember it (potentially SLOW!).
+               // This is for compatibility with old database rows that don't have the field set.
+               if ( $this->mSize === null ) {
+                       $this->mSize = $this->mSlots->computeSize();
+               }
+
+               return $this->mSize;
+       }
+
+       /**
+        * @throws RevisionAccessException if the hash was unknown and could not be calculated.
+        * @return string The revision hash, never null. May be computed on the fly.
+        */
+       public function getSha1() {
+               // If hash is null, calculate it and remember (potentially SLOW!)
+               // This is for compatibility with old database rows that don't have the field set.
+               if ( $this->mSha1 === null ) {
+                       $this->mSha1 = $this->mSlots->computeSha1();
+               }
+
+               return $this->mSha1;
+       }
+
+       /**
+        * @param int $audience
+        * @param User|null $user
+        *
+        * @return UserIdentity The identity of the revision author, null if access is forbidden.
+        */
+       public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+               // overwritten just to add a guarantee to the contract
+               return parent::getUser( $audience, $user );
+       }
+
+       /**
+        * @param int $audience
+        * @param User|null $user
+        *
+        * @return CommentStoreComment The revision comment, null if access is forbidden.
+        */
+       public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+               // overwritten just to add a guarantee to the contract
+               return parent::getComment( $audience, $user );
+       }
+
+       /**
+        * @return string never null
+        */
+       public function getTimestamp() {
+               // overwritten just to add a guarantee to the contract
+               return parent::getTimestamp();
+       }
+
+       /**
+        * @see RevisionStore::isComplete
+        *
+        * @return bool always true.
+        */
+       public function isReadyForInsertion() {
+               return true;
+       }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionArchiveRecord::class, 'MediaWiki\Storage\RevisionArchiveRecord' );
diff --git a/includes/Revision/RevisionFactory.php b/includes/Revision/RevisionFactory.php
new file mode 100644 (file)
index 0000000..44f1350
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * Service for constructing revision objects.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use MWException;
+use Title;
+
+/**
+ * Service for constructing revision objects.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionFactory
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ *       static methods in Revision.
+ */
+interface RevisionFactory {
+
+       /**
+        * Constructs a new RevisionRecord based on the given associative array following the MW1.29
+        * database convention for the Revision constructor.
+        *
+        * MCR migration note: this replaces Revision::newFromRow
+        *
+        * @deprecated since 1.31. Use a MutableRevisionRecord instead.
+        *
+        * @param array $fields
+        * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+        * @param Title|null $title
+        *
+        * @return MutableRevisionRecord
+        * @throws MWException
+        */
+       public function newMutableRevisionFromArray( array $fields, $queryFlags = 0, Title $title = null );
+
+       /**
+        * Constructs a RevisionRecord given a database row and content slots.
+        *
+        * MCR migration note: this replaces Revision::newFromRow for rows based on the
+        * revision, slot, and content tables defined for MCR since MW1.31.
+        *
+        * @param object $row A query result row as a raw object.
+        *        Use RevisionStore::getQueryInfo() to build a query that yields the required fields.
+        * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+        * @param Title|null $title
+        *
+        * @return RevisionRecord
+        */
+       public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null );
+
+       /**
+        * Make a fake revision object from an archive table row. This is queried
+        * for permissions or even inserted (as in Special:Undelete)
+        *
+        * MCR migration note: this replaces Revision::newFromArchiveRow
+        *
+        * @param object $row A query result row as a raw object.
+        *        Use RevisionStore::getArchiveQueryInfo() to build a query that yields the
+        *        required fields.
+        * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+        * @param Title|null $title
+        * @param array $overrides An associative array that allows fields in $row to be overwritten.
+        *        Keys in this array correspond to field names in $row without the "ar_" prefix, so
+        *        $overrides['user'] will override $row->ar_user, etc.
+        *
+        * @return RevisionRecord
+        */
+       public function newRevisionFromArchiveRow(
+               $row,
+               $queryFlags = 0,
+               Title $title = null,
+               array $overrides = []
+       );
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionFactory::class, 'MediaWiki\Storage\RevisionFactory' );
diff --git a/includes/Revision/RevisionLookup.php b/includes/Revision/RevisionLookup.php
new file mode 100644 (file)
index 0000000..db6c7c3
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+/**
+ *  Service for looking up page revisions.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use IDBAccessObject;
+use MediaWiki\Linker\LinkTarget;
+use Title;
+
+/**
+ * Service for looking up page revisions.
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ *       static methods in Revision.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionLookup
+ */
+interface RevisionLookup extends IDBAccessObject {
+
+       /**
+        * Load a page revision from a given revision ID number.
+        * Returns null if no such revision can be found.
+        *
+        * MCR migration note: this replaces Revision::newFromId
+        *
+        * $flags include:
+        *
+        * @param int $id
+        * @param int $flags bit field, see IDBAccessObject::READ_XXX
+        * @return RevisionRecord|null
+        */
+       public function getRevisionById( $id, $flags = 0 );
+
+       /**
+        * Load either the current, or a specified, revision
+        * that's attached to a given link target. If not attached
+        * to that link target, will return null.
+        *
+        * MCR migration note: this replaces Revision::newFromTitle
+        *
+        * @param LinkTarget $linkTarget
+        * @param int $revId (optional)
+        * @param int $flags bit field, see IDBAccessObject::READ_XXX
+        * @return RevisionRecord|null
+        */
+       public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 );
+
+       /**
+        * Load either the current, or a specified, revision
+        * that's attached to a given page ID.
+        * Returns null if no such revision can be found.
+        *
+        * MCR migration note: this replaces Revision::newFromPageId
+        *
+        * @param int $pageId
+        * @param int $revId (optional)
+        * @param int $flags bit field, see IDBAccessObject::READ_XXX
+        * @return RevisionRecord|null
+        */
+       public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 );
+
+       /**
+        * Get previous revision for this title
+        *
+        * MCR migration note: this replaces Revision::getPrevious
+        *
+        * @param RevisionRecord $rev
+        * @param Title|null $title if known (optional)
+        *
+        * @return RevisionRecord|null
+        */
+       public function getPreviousRevision( RevisionRecord $rev, Title $title = null );
+
+       /**
+        * Get next revision for this title
+        *
+        * MCR migration note: this replaces Revision::getNext
+        *
+        * @param RevisionRecord $rev
+        * @param Title|null $title if known (optional)
+        *
+        * @return RevisionRecord|null
+        */
+       public function getNextRevision( RevisionRecord $rev, Title $title = null );
+
+       /**
+        * Load a revision based on a known page ID and current revision ID from the DB
+        *
+        * 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.
+        *
+        * MCR migration note: this replaces Revision::newKnownCurrent
+        *
+        * @param Title $title the associated page title
+        * @param int $revId current revision of this page
+        *
+        * @return RevisionRecord|bool Returns false if missing
+        */
+       public function getKnownCurrentRevision( Title $title, $revId );
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionLookup::class, 'MediaWiki\Storage\RevisionLookup' );
diff --git a/includes/Revision/RevisionRecord.php b/includes/Revision/RevisionRecord.php
new file mode 100644 (file)
index 0000000..1a7831b
--- /dev/null
@@ -0,0 +1,567 @@
+<?php
+/**
+ * Page revision base class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use CommentStoreComment;
+use Content;
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
+use MWException;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Page revision base class.
+ *
+ * RevisionRecords are considered value objects, but they may use callbacks for lazy loading.
+ * Note that while the base class has no setters, subclasses may offer a mutable interface.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionRecord
+ */
+abstract class RevisionRecord {
+
+       // RevisionRecord deletion constants
+       const DELETED_TEXT = 1;
+       const DELETED_COMMENT = 2;
+       const DELETED_USER = 4;
+       const DELETED_RESTRICTED = 8;
+       const SUPPRESSED_USER = self::DELETED_USER | self::DELETED_RESTRICTED; // convenience
+       const SUPPRESSED_ALL = self::DELETED_TEXT | self::DELETED_COMMENT | self::DELETED_USER |
+               self::DELETED_RESTRICTED; // convenience
+
+       // Audience options for accessors
+       const FOR_PUBLIC = 1;
+       const FOR_THIS_USER = 2;
+       const RAW = 3;
+
+       /** @var string Wiki ID; false means the current wiki */
+       protected $mWiki = false;
+       /** @var int|null */
+       protected $mId;
+       /** @var int|null */
+       protected $mPageId;
+       /** @var UserIdentity|null */
+       protected $mUser;
+       /** @var bool */
+       protected $mMinorEdit = false;
+       /** @var string|null */
+       protected $mTimestamp;
+       /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */
+       protected $mDeleted = 0;
+       /** @var int|null */
+       protected $mSize;
+       /** @var string|null */
+       protected $mSha1;
+       /** @var int|null */
+       protected $mParentId;
+       /** @var CommentStoreComment|null */
+       protected $mComment;
+
+       /**  @var Title */
+       protected $mTitle; // TODO: we only need the title for permission checks!
+
+       /** @var RevisionSlots */
+       protected $mSlots;
+
+       /**
+        * @note Avoid calling this constructor directly. Use the appropriate methods
+        * in RevisionStore instead.
+        *
+        * @param Title $title The title of the page this Revision is associated with.
+        * @param RevisionSlots $slots The slots of this revision.
+        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+        *        or false for the local site.
+        *
+        * @throws MWException
+        */
+       function __construct( Title $title, RevisionSlots $slots, $wikiId = false ) {
+               Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+
+               $this->mTitle = $title;
+               $this->mSlots = $slots;
+               $this->mWiki = $wikiId;
+
+               // XXX: this is a sensible default, but we may not have a Title object here in the future.
+               $this->mPageId = $title->getArticleID();
+       }
+
+       /**
+        * Implemented to defy serialization.
+        *
+        * @throws LogicException always
+        */
+       public function __sleep() {
+               throw new LogicException( __CLASS__ . ' is not serializable.' );
+       }
+
+       /**
+        * @param RevisionRecord $rec
+        *
+        * @return bool True if this RevisionRecord is known to have same content as $rec.
+        *         False if the content is different (or not known to be the same).
+        */
+       public function hasSameContent( RevisionRecord $rec ) {
+               if ( $rec === $this ) {
+                       return true;
+               }
+
+               if ( $this->getId() !== null && $this->getId() === $rec->getId() ) {
+                       return true;
+               }
+
+               // check size before hash, since size is quicker to compute
+               if ( $this->getSize() !== $rec->getSize() ) {
+                       return false;
+               }
+
+               // instead of checking the hash, we could also check the content addresses of all slots.
+
+               if ( $this->getSha1() === $rec->getSha1() ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Returns the Content of the given slot of this revision.
+        * Call getSlotNames() to get a list of available slots.
+        *
+        * Note that for mutable Content objects, each call to this method will return a
+        * fresh clone.
+        *
+        * MCR migration note: this replaces Revision::getContent
+        *
+        * @param string $role The role name of the desired slot
+        * @param int $audience
+        * @param User|null $user
+        *
+        * @throws RevisionAccessException if the slot does not exist or slot data
+        *        could not be lazy-loaded.
+        * @return Content|null The content of the given slot, or null if access is forbidden.
+        */
+       public function getContent( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
+               // XXX: throwing an exception would be nicer, but would a further
+               // departure from the signature of Revision::getContent(), and thus
+               // more complex and error prone refactoring.
+               if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
+                       return null;
+               }
+
+               $content = $this->getSlot( $role, $audience, $user )->getContent();
+               return $content->copy();
+       }
+
+       /**
+        * Returns meta-data for the given slot.
+        *
+        * @param string $role The role name of the desired slot
+        * @param int $audience
+        * @param User|null $user
+        *
+        * @throws RevisionAccessException if the slot does not exist or slot data
+        *        could not be lazy-loaded.
+        * @return SlotRecord The slot meta-data. If access to the slot content is forbidden,
+        *         calling getContent() on the SlotRecord will throw an exception.
+        */
+       public function getSlot( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
+               $slot = $this->mSlots->getSlot( $role );
+
+               if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
+                       return SlotRecord::newWithSuppressedContent( $slot );
+               }
+
+               return $slot;
+       }
+
+       /**
+        * Returns whether the given slot is defined in this revision.
+        *
+        * @param string $role The role name of the desired slot
+        *
+        * @return bool
+        */
+       public function hasSlot( $role ) {
+               return $this->mSlots->hasSlot( $role );
+       }
+
+       /**
+        * Returns the slot names (roles) of all slots present in this revision.
+        * getContent() will succeed only for the names returned by this method.
+        *
+        * @return string[]
+        */
+       public function getSlotRoles() {
+               return $this->mSlots->getSlotRoles();
+       }
+
+       /**
+        * Returns the slots defined for this revision.
+        *
+        * @return RevisionSlots
+        */
+       public function getSlots() {
+               return $this->mSlots;
+       }
+
+       /**
+        * Returns the slots that originate in this revision.
+        *
+        * Note that this does not include any slots inherited from some earlier revision,
+        * even if they are different from the slots in the immediate parent revision.
+        * This is the case for rollbacks: slots of a rollback revision are inherited from
+        * the rollback target, and are different from the slots in the parent revision,
+        * which was rolled back.
+        *
+        * To find all slots modified by this revision against its immediate parent
+        * revision, use RevisionSlotsUpdate::newFromRevisionSlots().
+        *
+        * @return RevisionSlots
+        */
+       public function getOriginalSlots() {
+               return new RevisionSlots( $this->mSlots->getOriginalSlots() );
+       }
+
+       /**
+        * Returns slots inherited from some previous revision.
+        *
+        * "Inherited" slots are all slots that do not originate in this revision.
+        * Note that these slots may still differ from the one in the parent revision.
+        * This is the case for rollbacks: slots of a rollback revision are inherited from
+        * the rollback target, and are different from the slots in the parent revision,
+        * which was rolled back.
+        *
+        * @return RevisionSlots
+        */
+       public function getInheritedSlots() {
+               return new RevisionSlots( $this->mSlots->getInheritedSlots() );
+       }
+
+       /**
+        * Get revision ID. Depending on the concrete subclass, this may return null if
+        * the revision ID is not known (e.g. because the revision does not yet exist
+        * in the database).
+        *
+        * MCR migration note: this replaces Revision::getId
+        *
+        * @return int|null
+        */
+       public function getId() {
+               return $this->mId;
+       }
+
+       /**
+        * Get parent revision ID (the original previous page revision).
+        * If there is no parent revision, this returns 0.
+        * If the parent revision is undefined or unknown, this returns null.
+        *
+        * @note As of MW 1.31, the database schema allows the parent ID to be
+        * NULL to indicate that it is unknown.
+        *
+        * MCR migration note: this replaces Revision::getParentId
+        *
+        * @return int|null
+        */
+       public function getParentId() {
+               return $this->mParentId;
+       }
+
+       /**
+        * Returns the nominal size of this revision, in bogo-bytes.
+        * May be calculated on the fly if not known, which may in the worst
+        * case may involve loading all content.
+        *
+        * MCR migration note: this replaces Revision::getSize
+        *
+        * @throws RevisionAccessException if the size was unknown and could not be calculated.
+        * @return int
+        */
+       abstract public function getSize();
+
+       /**
+        * Returns the base36 sha1 of this revision. This hash is derived from the
+        * hashes of all slots associated with the revision.
+        * May be calculated on the fly if not known, which may in the worst
+        * case may involve loading all content.
+        *
+        * MCR migration note: this replaces Revision::getSha1
+        *
+        * @throws RevisionAccessException if the hash was unknown and could not be calculated.
+        * @return string
+        */
+       abstract public function getSha1();
+
+       /**
+        * Get the page ID. If the page does not yet exist, the page ID is 0.
+        *
+        * MCR migration note: this replaces Revision::getPage
+        *
+        * @return int
+        */
+       public function getPageId() {
+               return $this->mPageId;
+       }
+
+       /**
+        * Get the ID of the wiki this revision belongs to.
+        *
+        * @return string|false The wiki's logical name, of false to indicate the local wiki.
+        */
+       public function getWikiId() {
+               return $this->mWiki;
+       }
+
+       /**
+        * Returns the title of the page this revision is associated with as a LinkTarget object.
+        *
+        * MCR migration note: this replaces Revision::getTitle
+        *
+        * @return LinkTarget
+        */
+       public function getPageAsLinkTarget() {
+               return $this->mTitle;
+       }
+
+       /**
+        * Fetch revision's author's user identity, if it's available to the specified audience.
+        * If the specified audience does not have access to it, null will be
+        * returned. Depending on the concrete subclass, null may also be returned if the user is
+        * not yet specified.
+        *
+        * MCR migration note: this replaces Revision::getUser
+        *
+        * @param int $audience One of:
+        *   RevisionRecord::FOR_PUBLIC       to be displayed to all users
+        *   RevisionRecord::FOR_THIS_USER    to be displayed to the given user
+        *   RevisionRecord::RAW              get the ID regardless of permissions
+        * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
+        *   to the $audience parameter
+        * @return UserIdentity|null
+        */
+       public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+               if ( !$this->audienceCan( self::DELETED_USER, $audience, $user ) ) {
+                       return null;
+               } else {
+                       return $this->mUser;
+               }
+       }
+
+       /**
+        * Fetch revision comment, if it's available to the specified audience.
+        * If the specified audience does not have access to the comment,
+        * this will return null. Depending on the concrete subclass, null may also be returned
+        * if the comment is not yet specified.
+        *
+        * MCR migration note: this replaces Revision::getComment
+        *
+        * @param int $audience One of:
+        *   RevisionRecord::FOR_PUBLIC       to be displayed to all users
+        *   RevisionRecord::FOR_THIS_USER    to be displayed to the given user
+        *   RevisionRecord::RAW              get the text regardless of permissions
+        * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
+        *   to the $audience parameter
+        *
+        * @return CommentStoreComment|null
+        */
+       public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+               if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $user ) ) {
+                       return null;
+               } else {
+                       return $this->mComment;
+               }
+       }
+
+       /**
+        * MCR migration note: this replaces Revision::isMinor
+        *
+        * @return bool
+        */
+       public function isMinor() {
+               return (bool)$this->mMinorEdit;
+       }
+
+       /**
+        * MCR migration note: this replaces Revision::isDeleted
+        *
+        * @param int $field One of DELETED_* bitfield constants
+        *
+        * @return bool
+        */
+       public function isDeleted( $field ) {
+               return ( $this->getVisibility() & $field ) == $field;
+       }
+
+       /**
+        * Get the deletion bitfield of the revision
+        *
+        * MCR migration note: this replaces Revision::getVisibility
+        *
+        * @return int
+        */
+       public function getVisibility() {
+               return (int)$this->mDeleted;
+       }
+
+       /**
+        * MCR migration note: this replaces Revision::getTimestamp.
+        *
+        * May return null if the timestamp was not specified.
+        *
+        * @return string|null
+        */
+       public function getTimestamp() {
+               return $this->mTimestamp;
+       }
+
+       /**
+        * Check that the given audience has access to the given field.
+        *
+        * MCR migration note: this corresponds to Revision::userCan
+        *
+        * @param int $field One of self::DELETED_TEXT,
+        *        self::DELETED_COMMENT,
+        *        self::DELETED_USER
+        * @param int $audience One of:
+        *        RevisionRecord::FOR_PUBLIC       to be displayed to all users
+        *        RevisionRecord::FOR_THIS_USER    to be displayed to the given user
+        *        RevisionRecord::RAW              get the text regardless of permissions
+        * @param User|null $user User object to check. Required if $audience is FOR_THIS_USER,
+        *        ignored otherwise.
+        *
+        * @return bool
+        */
+       public function audienceCan( $field, $audience, User $user = null ) {
+               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) {
+                       return false;
+               } elseif ( $audience == self::FOR_THIS_USER ) {
+                       if ( !$user ) {
+                               throw new InvalidArgumentException(
+                                       'A User object must be given when checking FOR_THIS_USER audience.'
+                               );
+                       }
+
+                       if ( !$this->userCan( $field, $user ) ) {
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Determine if the current user is allowed to view a particular
+        * field of this revision, if it's marked as deleted.
+        *
+        * MCR migration note: this corresponds to Revision::userCan
+        *
+        * @param int $field One of self::DELETED_TEXT,
+        *                              self::DELETED_COMMENT,
+        *                              self::DELETED_USER
+        * @param User $user User object to check
+        * @return bool
+        */
+       protected function userCan( $field, User $user ) {
+               // TODO: use callback for permission checks, so we don't need to know a Title object!
+               return self::userCanBitfield( $this->getVisibility(), $field, $user, $this->mTitle );
+       }
+
+       /**
+        * Determine if the current user is allowed to view a particular
+        * field of this revision, if it's marked as deleted. This is used
+        * by various classes to avoid duplication.
+        *
+        * MCR migration note: this replaces Revision::userCanBitfield
+        *
+        * @param int $bitfield Current field
+        * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
+        *                               self::DELETED_COMMENT = File::DELETED_COMMENT,
+        *                               self::DELETED_USER = File::DELETED_USER
+        * @param User $user User object to check
+        * @param Title|null $title A Title object to check for per-page restrictions on,
+        *                          instead of just plain userrights
+        * @return bool
+        */
+       public static function userCanBitfield( $bitfield, $field, User $user, Title $title = null ) {
+               if ( $bitfield & $field ) { // aspect is deleted
+                       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 $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;
+               }
+       }
+
+       /**
+        * Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all
+        * information needed to save it to the database. This should trivially be true for
+        * RevisionRecords loaded from the database.
+        *
+        * Note that this may return true even if getId() or getPage() return null or 0, since these
+        * are generally assigned while the revision is saved to the database, and may not be available
+        * before.
+        *
+        * @return bool
+        */
+       public function isReadyForInsertion() {
+               // NOTE: don't check getSize() and getSha1(), since that may cause the full content to
+               // be loaded in order to calculate the values. Just assume these methods will not return
+               // null if mSlots is not empty.
+
+               // NOTE: getId() and getPageId() may return null before a revision is saved, so don't
+               //check them.
+
+               return $this->getTimestamp() !== null
+                       && $this->getComment( self::RAW ) !== null
+                       && $this->getUser( self::RAW ) !== null
+                       && $this->mSlots->getSlotRoles() !== [];
+       }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionRecord::class, 'MediaWiki\Storage\RevisionRecord' );
index c937376..377477b 100644 (file)
@@ -24,8 +24,6 @@ namespace MediaWiki\Revision;
 
 use Html;
 use InvalidArgumentException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
 use ParserOptions;
 use ParserOutput;
 use Psr\Log\LoggerInterface;
diff --git a/includes/Revision/RevisionSlots.php b/includes/Revision/RevisionSlots.php
new file mode 100644 (file)
index 0000000..661137a
--- /dev/null
@@ -0,0 +1,319 @@
+<?php
+/**
+ * Value object representing the set of slots belonging to a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use Content;
+use LogicException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Value object representing the set of slots belonging to a revision.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionSlots
+ */
+class RevisionSlots {
+
+       /** @var SlotRecord[]|callable */
+       protected $slots;
+
+       /**
+        * @param SlotRecord[]|callable $slots SlotRecords,
+        *        or a callback that returns such a structure.
+        */
+       public function __construct( $slots ) {
+               Assert::parameterType( 'array|callable', $slots, '$slots' );
+
+               if ( is_callable( $slots ) ) {
+                       $this->slots = $slots;
+               } else {
+                       $this->setSlotsInternal( $slots );
+               }
+       }
+
+       /**
+        * @param SlotRecord[] $slots
+        */
+       private function setSlotsInternal( array $slots ) {
+               Assert::parameterElementType( SlotRecord::class, $slots, '$slots' );
+
+               $this->slots = [];
+
+               // re-key the slot array
+               foreach ( $slots as $slot ) {
+                       $role = $slot->getRole();
+                       $this->slots[$role] = $slot;
+               }
+       }
+
+       /**
+        * Implemented to defy serialization.
+        *
+        * @throws LogicException always
+        */
+       public function __sleep() {
+               throw new LogicException( __CLASS__ . ' is not serializable.' );
+       }
+
+       /**
+        * Returns the Content of the given slot.
+        * Call getSlotNames() to get a list of available slots.
+        *
+        * Note that for mutable Content objects, each call to this method will return a
+        * fresh clone.
+        *
+        * @param string $role The role name of the desired slot
+        *
+        * @throws RevisionAccessException if the slot does not exist or slot data
+        *        could not be lazy-loaded.
+        * @return Content
+        */
+       public function getContent( $role ) {
+               // Return a copy to be safe. Immutable content objects return $this from copy().
+               return $this->getSlot( $role )->getContent()->copy();
+       }
+
+       /**
+        * Returns the SlotRecord of the given slot.
+        * Call getSlotNames() to get a list of available slots.
+        *
+        * @param string $role The role name of the desired slot
+        *
+        * @throws RevisionAccessException if the slot does not exist or slot data
+        *        could not be lazy-loaded.
+        * @return SlotRecord
+        */
+       public function getSlot( $role ) {
+               $slots = $this->getSlots();
+
+               if ( isset( $slots[$role] ) ) {
+                       return $slots[$role];
+               } else {
+                       throw new RevisionAccessException( 'No such slot: ' . $role );
+               }
+       }
+
+       /**
+        * Returns whether the given slot is set.
+        *
+        * @param string $role The role name of the desired slot
+        *
+        * @return bool
+        */
+       public function hasSlot( $role ) {
+               $slots = $this->getSlots();
+
+               return isset( $slots[$role] );
+       }
+
+       /**
+        * Returns the slot names (roles) of all slots present in this revision.
+        * getContent() will succeed only for the names returned by this method.
+        *
+        * @return string[]
+        */
+       public function getSlotRoles() {
+               $slots = $this->getSlots();
+               return array_keys( $slots );
+       }
+
+       /**
+        * Computes the total nominal size of the revision's slots, in bogo-bytes.
+        *
+        * @warning This is potentially expensive! It may cause all slot's content to be loaded
+        * and deserialized.
+        *
+        * @return int
+        */
+       public function computeSize() {
+               return array_reduce( $this->getSlots(), function ( $accu, SlotRecord $slot ) {
+                       return $accu + $slot->getSize();
+               }, 0 );
+       }
+
+       /**
+        * Returns an associative array that maps role names to SlotRecords. Each SlotRecord
+        * represents the content meta-data of a slot, together they define the content of
+        * a revision.
+        *
+        * @note This may cause the content meta-data for the revision to be lazy-loaded.
+        *
+        * @return SlotRecord[] revision slot/content rows, keyed by slot role name.
+        */
+       public function getSlots() {
+               if ( is_callable( $this->slots ) ) {
+                       $slots = call_user_func( $this->slots );
+
+                       Assert::postcondition(
+                               is_array( $slots ),
+                               'Slots info callback should return an array of objects'
+                       );
+
+                       $this->setSlotsInternal( $slots );
+               }
+
+               return $this->slots;
+       }
+
+       /**
+        * Computes the combined hash of the revisions's slots.
+        *
+        * @note For backwards compatibility, the combined hash of a single slot
+        * is that slot's hash. For consistency, the combined hash of an empty set of slots
+        * is the hash of the empty string.
+        *
+        * @warning This is potentially expensive! It may cause all slot's content to be loaded
+        * and deserialized, then re-serialized and hashed.
+        *
+        * @return string
+        */
+       public function computeSha1() {
+               $slots = $this->getSlots();
+               ksort( $slots );
+
+               if ( empty( $slots ) ) {
+                       return SlotRecord::base36Sha1( '' );
+               }
+
+               return array_reduce( $slots, function ( $accu, SlotRecord $slot ) {
+                       return $accu === null
+                               ? $slot->getSha1()
+                               : SlotRecord::base36Sha1( $accu . $slot->getSha1() );
+               }, null );
+       }
+
+       /**
+        * Return all slots that belong to the revision they originate from (that is,
+        * they are not inherited from some other revision).
+        *
+        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+        *
+        * @return SlotRecord[]
+        */
+       public function getOriginalSlots() {
+               return array_filter(
+                       $this->getSlots(),
+                       function ( SlotRecord $slot ) {
+                               return !$slot->isInherited();
+                       }
+               );
+       }
+
+       /**
+        * Return all slots that are not not originate in the revision they belong to (that is,
+        * they are inherited from some other revision).
+        *
+        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+        *
+        * @return SlotRecord[]
+        */
+       public function getInheritedSlots() {
+               return array_filter(
+                       $this->getSlots(),
+                       function ( SlotRecord $slot ) {
+                               return $slot->isInherited();
+                       }
+               );
+       }
+
+       /**
+        * Checks whether the other RevisionSlots instance has the same content
+        * as this instance. Note that this does not mean that the slots have to be the same:
+        * they could for instance belong to different revisions.
+        *
+        * @param RevisionSlots $other
+        *
+        * @return bool
+        */
+       public function hasSameContent( RevisionSlots $other ) {
+               if ( $other === $this ) {
+                       return true;
+               }
+
+               $aSlots = $this->getSlots();
+               $bSlots = $other->getSlots();
+
+               ksort( $aSlots );
+               ksort( $bSlots );
+
+               if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) {
+                       return false;
+               }
+
+               foreach ( $aSlots as $role => $s ) {
+                       $t = $bSlots[$role];
+
+                       if ( !$s->hasSameContent( $t ) ) {
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Find roles for which the $other RevisionSlots object has different content
+        * as this RevisionSlots object, including any roles that are present in one
+        * but not the other.
+        *
+        * @param RevisionSlots $other
+        *
+        * @return string[] a list of slot roles that are different.
+        */
+       public function getRolesWithDifferentContent( RevisionSlots $other ) {
+               if ( $other === $this ) {
+                       return [];
+               }
+
+               $aSlots = $this->getSlots();
+               $bSlots = $other->getSlots();
+
+               ksort( $aSlots );
+               ksort( $bSlots );
+
+               $different = array_keys( array_merge(
+                       array_diff_key( $aSlots, $bSlots ),
+                       array_diff_key( $bSlots, $aSlots )
+               ) );
+
+               /** @var SlotRecord[] $common */
+               $common = array_intersect_key( $aSlots, $bSlots );
+
+               foreach ( $common as $role => $s ) {
+                       $t = $bSlots[$role];
+
+                       if ( !$s->hasSameContent( $t ) ) {
+                               $different[] = $role;
+                       }
+               }
+
+               return $different;
+       }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionSlots::class, 'MediaWiki\Storage\RevisionSlots' );
diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php
new file mode 100644 (file)
index 0000000..6d3b72c
--- /dev/null
@@ -0,0 +1,2791 @@
+<?php
+/**
+ * Service for looking up page revisions.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Attribution notice: when this file was created, much of its content was taken
+ * from the Revision.php file as present in release 1.30. Refer to the history
+ * of that file for original authorship.
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use ActorMigration;
+use CommentStore;
+use CommentStoreComment;
+use Content;
+use ContentHandler;
+use DBAccessObjectUtils;
+use Hooks;
+use IDBAccessObject;
+use InvalidArgumentException;
+use IP;
+use LogicException;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Storage\BlobAccessException;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\NameTableAccessException;
+use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\User\UserIdentity;
+use MediaWiki\User\UserIdentityValue;
+use Message;
+use MWException;
+use MWUnknownContentModelException;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use RecentChange;
+use Revision;
+use RuntimeException;
+use stdClass;
+use Title;
+use User;
+use WANObjectCache;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DBConnRef;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\ILoadBalancer;
+
+/**
+ * Service for looking up page revisions.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionStore
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ *       static methods in Revision.
+ */
+class RevisionStore
+       implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
+
+       const ROW_CACHE_KEY = 'revision-row-1.29';
+
+       /**
+        * @var SqlBlobStore
+        */
+       private $blobStore;
+
+       /**
+        * @var bool|string
+        */
+       private $wikiId;
+
+       /**
+        * @var boolean
+        * @see $wgContentHandlerUseDB
+        */
+       private $contentHandlerUseDB = true;
+
+       /**
+        * @var ILoadBalancer
+        */
+       private $loadBalancer;
+
+       /**
+        * @var WANObjectCache
+        */
+       private $cache;
+
+       /**
+        * @var CommentStore
+        */
+       private $commentStore;
+
+       /**
+        * @var ActorMigration
+        */
+       private $actorMigration;
+
+       /**
+        * @var LoggerInterface
+        */
+       private $logger;
+
+       /**
+        * @var NameTableStore
+        */
+       private $contentModelStore;
+
+       /**
+        * @var NameTableStore
+        */
+       private $slotRoleStore;
+
+       /** @var int An appropriate combination of SCHEMA_COMPAT_XXX flags. */
+       private $mcrMigrationStage;
+
+       /**
+        * @todo $blobStore should be allowed to be any BlobStore!
+        *
+        * @param ILoadBalancer $loadBalancer
+        * @param SqlBlobStore $blobStore
+        * @param WANObjectCache $cache A cache for caching revision rows. This can be the local
+        *        wiki's default instance even if $wikiId refers to a different wiki, since
+        *        makeGlobalKey() is used to constructed a key that allows cached revision rows from
+        *        the same database to be re-used between wikis. For example, enwiki and frwiki will
+        *        use the same cache keys for revision rows from the wikidatawiki database, regardless
+        *        of the cache's default key space.
+        * @param CommentStore $commentStore
+        * @param NameTableStore $contentModelStore
+        * @param NameTableStore $slotRoleStore
+        * @param int $mcrMigrationStage An appropriate combination of SCHEMA_COMPAT_XXX flags
+        * @param ActorMigration $actorMigration
+        * @param bool|string $wikiId
+        *
+        * @throws MWException if $mcrMigrationStage or $wikiId is invalid.
+        */
+       public function __construct(
+               ILoadBalancer $loadBalancer,
+               SqlBlobStore $blobStore,
+               WANObjectCache $cache,
+               CommentStore $commentStore,
+               NameTableStore $contentModelStore,
+               NameTableStore $slotRoleStore,
+               $mcrMigrationStage,
+               ActorMigration $actorMigration,
+               $wikiId = false
+       ) {
+               Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+               Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
+               Assert::parameter(
+                       ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== SCHEMA_COMPAT_READ_BOTH,
+                       '$mcrMigrationStage',
+                       'Reading from the old and the new schema at the same time is not supported.'
+               );
+               Assert::parameter(
+                       ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== 0,
+                       '$mcrMigrationStage',
+                       'Reading needs to be enabled for the old or the new schema.'
+               );
+               Assert::parameter(
+                       ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_BOTH ) !== 0,
+                       '$mcrMigrationStage',
+                       'Writing needs to be enabled for the old or the new schema.'
+               );
+               Assert::parameter(
+                       ( $mcrMigrationStage & SCHEMA_COMPAT_READ_OLD ) === 0
+                       || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) !== 0,
+                       '$mcrMigrationStage',
+                       'Cannot read the old schema when not also writing it.'
+               );
+               Assert::parameter(
+                       ( $mcrMigrationStage & SCHEMA_COMPAT_READ_NEW ) === 0
+                       || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) !== 0,
+                       '$mcrMigrationStage',
+                       'Cannot read the new schema when not also writing it.'
+               );
+
+               $this->loadBalancer = $loadBalancer;
+               $this->blobStore = $blobStore;
+               $this->cache = $cache;
+               $this->commentStore = $commentStore;
+               $this->contentModelStore = $contentModelStore;
+               $this->slotRoleStore = $slotRoleStore;
+               $this->mcrMigrationStage = $mcrMigrationStage;
+               $this->actorMigration = $actorMigration;
+               $this->wikiId = $wikiId;
+               $this->logger = new NullLogger();
+       }
+
+       /**
+        * @param int $flags A combination of SCHEMA_COMPAT_XXX flags.
+        * @return bool True if all the given flags were set in the $mcrMigrationStage
+        *         parameter passed to the constructor.
+        */
+       private function hasMcrSchemaFlags( $flags ) {
+               return ( $this->mcrMigrationStage & $flags ) === $flags;
+       }
+
+       /**
+        * Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading
+        * and still reading from the old DB schema.
+        *
+        * @throws RevisionAccessException
+        */
+       private function assertCrossWikiContentLoadingIsSafe() {
+               if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+                       throw new RevisionAccessException(
+                               "Cross-wiki content loading is not supported by the pre-MCR schema"
+                       );
+               }
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * @return bool Whether the store is read-only
+        */
+       public function isReadOnly() {
+               return $this->blobStore->isReadOnly();
+       }
+
+       /**
+        * @return bool
+        */
+       public function getContentHandlerUseDB() {
+               return $this->contentHandlerUseDB;
+       }
+
+       /**
+        * @see $wgContentHandlerUseDB
+        * @param bool $contentHandlerUseDB
+        * @throws MWException
+        */
+       public function setContentHandlerUseDB( $contentHandlerUseDB ) {
+               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
+                       || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
+               ) {
+                       if ( !$contentHandlerUseDB ) {
+                               throw new MWException(
+                                       'Content model must be stored in the database for multi content revision migration.'
+                               );
+                       }
+               }
+               $this->contentHandlerUseDB = $contentHandlerUseDB;
+       }
+
+       /**
+        * @return ILoadBalancer
+        */
+       private function getDBLoadBalancer() {
+               return $this->loadBalancer;
+       }
+
+       /**
+        * @param int $mode DB_MASTER or DB_REPLICA
+        *
+        * @return IDatabase
+        */
+       private function getDBConnection( $mode ) {
+               $lb = $this->getDBLoadBalancer();
+               return $lb->getConnection( $mode, [], $this->wikiId );
+       }
+
+       /**
+        * @param int $queryFlags a bit field composed of READ_XXX flags
+        *
+        * @return DBConnRef
+        */
+       private function getDBConnectionRefForQueryFlags( $queryFlags ) {
+               list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+               return $this->getDBConnectionRef( $mode );
+       }
+
+       /**
+        * @param IDatabase $connection
+        */
+       private function releaseDBConnection( IDatabase $connection ) {
+               $lb = $this->getDBLoadBalancer();
+               $lb->reuseConnection( $connection );
+       }
+
+       /**
+        * @param int $mode DB_MASTER or DB_REPLICA
+        *
+        * @return DBConnRef
+        */
+       private function getDBConnectionRef( $mode ) {
+               $lb = $this->getDBLoadBalancer();
+               return $lb->getConnectionRef( $mode, [], $this->wikiId );
+       }
+
+       /**
+        * Determines the page Title based on the available information.
+        *
+        * MCR migration note: this corresponds to Revision::getTitle
+        *
+        * @note this method should be private, external use should be avoided!
+        *
+        * @param int|null $pageId
+        * @param int|null $revId
+        * @param int $queryFlags
+        *
+        * @return Title
+        * @throws RevisionAccessException
+        */
+       public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
+               if ( !$pageId && !$revId ) {
+                       throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
+               }
+
+               // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
+               // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
+               if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
+                       $queryFlags = self::READ_NORMAL;
+               }
+
+               $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
+               list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+               $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
+
+               // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
+               if ( $canUseTitleNewFromId ) {
+                       // TODO: better foreign title handling (introduce TitleFactory)
+                       $title = Title::newFromID( $pageId, $titleFlags );
+                       if ( $title ) {
+                               return $title;
+                       }
+               }
+
+               // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
+               $canUseRevId = ( $revId !== null && $revId > 0 );
+
+               if ( $canUseRevId ) {
+                       $dbr = $this->getDBConnectionRef( $dbMode );
+                       // @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' => $revId ],
+                               __METHOD__,
+                               $dbOptions,
+                               [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
+                       );
+                       if ( $row ) {
+                               // TODO: better foreign title handling (introduce TitleFactory)
+                               return Title::newFromRow( $row );
+                       }
+               }
+
+               // If we still don't have a title, fallback to master if that wasn't already happening.
+               if ( $dbMode !== DB_MASTER ) {
+                       $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
+                       if ( $title ) {
+                               $this->logger->info(
+                                       __METHOD__ . ' fell back to READ_LATEST and got a Title.',
+                                       [ 'trace' => wfBacktrace() ]
+                               );
+                               return $title;
+                       }
+               }
+
+               throw new RevisionAccessException(
+                       "Could not determine title for page ID $pageId and revision ID $revId"
+               );
+       }
+
+       /**
+        * @param mixed $value
+        * @param string $name
+        *
+        * @throws IncompleteRevisionException if $value is null
+        * @return mixed $value, if $value is not null
+        */
+       private function failOnNull( $value, $name ) {
+               if ( $value === null ) {
+                       throw new IncompleteRevisionException(
+                               "$name must not be " . var_export( $value, true ) . "!"
+                       );
+               }
+
+               return $value;
+       }
+
+       /**
+        * @param mixed $value
+        * @param string $name
+        *
+        * @throws IncompleteRevisionException if $value is empty
+        * @return mixed $value, if $value is not null
+        */
+       private function failOnEmpty( $value, $name ) {
+               if ( $value === null || $value === 0 || $value === '' ) {
+                       throw new IncompleteRevisionException(
+                               "$name must not be " . var_export( $value, true ) . "!"
+                       );
+               }
+
+               return $value;
+       }
+
+       /**
+        * Insert a new revision into the database, returning the new revision record
+        * on success and dies horribly on failure.
+        *
+        * MCR migration note: this replaces Revision::insertOn
+        *
+        * @param RevisionRecord $rev
+        * @param IDatabase $dbw (master connection)
+        *
+        * @throws InvalidArgumentException
+        * @return RevisionRecord the new revision record.
+        */
+       public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
+               // TODO: pass in a DBTransactionContext instead of a database connection.
+               $this->checkDatabaseWikiId( $dbw );
+
+               $slotRoles = $rev->getSlotRoles();
+
+               // Make sure the main slot is always provided throughout migration
+               if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
+                       throw new InvalidArgumentException(
+                               'main slot must be provided'
+                       );
+               }
+
+               // If we are not writing into the new schema, we can't support extra slots.
+               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
+                       && $slotRoles !== [ SlotRecord::MAIN ]
+               ) {
+                       throw new InvalidArgumentException(
+                               'Only the main slot is supported when not writing to the MCR enabled schema!'
+                       );
+               }
+
+               // As long as we are not reading from the new schema, we don't want to write extra slots.
+               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
+                       && $slotRoles !== [ SlotRecord::MAIN ]
+               ) {
+                       throw new InvalidArgumentException(
+                               'Only the main slot is supported when not reading from the MCR enabled schema!'
+                       );
+               }
+
+               // Checks
+               $this->failOnNull( $rev->getSize(), 'size field' );
+               $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
+               $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
+               $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
+               $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
+               $this->failOnNull( $user->getId(), 'user field' );
+               $this->failOnEmpty( $user->getName(), 'user_text field' );
+
+               if ( !$rev->isReadyForInsertion() ) {
+                       // This is here for future-proofing. At the time this check being added, it
+                       // was redundant to the individual checks above.
+                       throw new IncompleteRevisionException( 'Revision is incomplete' );
+               }
+
+               // TODO: we shouldn't need an actual Title here.
+               $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
+               $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
+
+               $parentId = $rev->getParentId() === null
+                       ? $this->getPreviousRevisionId( $dbw, $rev )
+                       : $rev->getParentId();
+
+               /** @var RevisionRecord $rev */
+               $rev = $dbw->doAtomicSection(
+                       __METHOD__,
+                       function ( IDatabase $dbw, $fname ) use (
+                               $rev,
+                               $user,
+                               $comment,
+                               $title,
+                               $pageId,
+                               $parentId
+                       ) {
+                               return $this->insertRevisionInternal(
+                                       $rev,
+                                       $dbw,
+                                       $user,
+                                       $comment,
+                                       $title,
+                                       $pageId,
+                                       $parentId
+                               );
+                       }
+               );
+
+               // sanity checks
+               Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
+               Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
+               Assert::postcondition(
+                       $rev->getComment( RevisionRecord::RAW ) !== null,
+                       'revision must have a comment'
+               );
+               Assert::postcondition(
+                       $rev->getUser( RevisionRecord::RAW ) !== null,
+                       'revision must have a user'
+               );
+
+               // Trigger exception if the main slot is missing.
+               // Technically, this could go away after MCR migration: while
+               // calling code may require a main slot to exist, RevisionStore
+               // really should not know or care about that requirement.
+               $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
+
+               foreach ( $slotRoles as $role ) {
+                       $slot = $rev->getSlot( $role, RevisionRecord::RAW );
+                       Assert::postcondition(
+                               $slot->getContent() !== null,
+                               $role . ' slot must have content'
+                       );
+                       Assert::postcondition(
+                               $slot->hasRevision(),
+                               $role . ' slot must have a revision associated'
+                       );
+               }
+
+               Hooks::run( 'RevisionRecordInserted', [ $rev ] );
+
+               // TODO: deprecate in 1.32!
+               $legacyRevision = new Revision( $rev );
+               Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
+
+               return $rev;
+       }
+
+       private function insertRevisionInternal(
+               RevisionRecord $rev,
+               IDatabase $dbw,
+               User $user,
+               CommentStoreComment $comment,
+               Title $title,
+               $pageId,
+               $parentId
+       ) {
+               $slotRoles = $rev->getSlotRoles();
+
+               $revisionRow = $this->insertRevisionRowOn(
+                       $dbw,
+                       $rev,
+                       $title,
+                       $parentId
+               );
+
+               $revisionId = $revisionRow['rev_id'];
+
+               $blobHints = [
+                       BlobStore::PAGE_HINT => $pageId,
+                       BlobStore::REVISION_HINT => $revisionId,
+                       BlobStore::PARENT_HINT => $parentId,
+               ];
+
+               $newSlots = [];
+               foreach ( $slotRoles as $role ) {
+                       $slot = $rev->getSlot( $role, RevisionRecord::RAW );
+
+                       // If the SlotRecord already has a revision ID set, this means it already exists
+                       // in the database, and should already belong to the current revision.
+                       // However, a slot may already have a revision, but no content ID, if the slot
+                       // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
+                       // mode, and the respective archive row was not yet migrated to the new schema.
+                       // In that case, a new slot row (and content row) must be inserted even during
+                       // undeletion.
+                       if ( $slot->hasRevision() && $slot->hasContentId() ) {
+                               // TODO: properly abort transaction if the assertion fails!
+                               Assert::parameter(
+                                       $slot->getRevision() === $revisionId,
+                                       'slot role ' . $slot->getRole(),
+                                       'Existing slot should belong to revision '
+                                       . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
+                               );
+
+                               // Slot exists, nothing to do, move along.
+                               // This happens when restoring archived revisions.
+
+                               $newSlots[$role] = $slot;
+
+                               // Write the main slot's text ID to the revision table for backwards compatibility
+                               if ( $slot->getRole() === SlotRecord::MAIN
+                                       && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
+                               ) {
+                                       $blobAddress = $slot->getAddress();
+                                       $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+                               }
+                       } else {
+                               $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
+                       }
+               }
+
+               $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
+
+               $rev = new RevisionStoreRecord(
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$revisionRow,
+                       new RevisionSlots( $newSlots ),
+                       $this->wikiId
+               );
+
+               return $rev;
+       }
+
+       /**
+        * @param IDatabase $dbw
+        * @param int $revisionId
+        * @param string &$blobAddress (may change!)
+        *
+        * @return int the text row id
+        */
+       private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
+               $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
+               if ( !$textId ) {
+                       throw new LogicException(
+                               'Blob address not supported in 1.29 database schema: ' . $blobAddress
+                       );
+               }
+
+               // getTextIdFromAddress() is free to insert something into the text table, so $textId
+               // may be a new value, not anything already contained in $blobAddress.
+               $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
+
+               $dbw->update(
+                       'revision',
+                       [ 'rev_text_id' => $textId ],
+                       [ 'rev_id' => $revisionId ],
+                       __METHOD__
+               );
+
+               return $textId;
+       }
+
+       /**
+        * @param IDatabase $dbw
+        * @param int $revisionId
+        * @param SlotRecord $protoSlot
+        * @param Title $title
+        * @param array $blobHints See the BlobStore::XXX_HINT constants
+        * @return SlotRecord
+        */
+       private function insertSlotOn(
+               IDatabase $dbw,
+               $revisionId,
+               SlotRecord $protoSlot,
+               Title $title,
+               array $blobHints = []
+       ) {
+               if ( $protoSlot->hasAddress() ) {
+                       $blobAddress = $protoSlot->getAddress();
+               } else {
+                       $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
+               }
+
+               $contentId = null;
+
+               // Write the main slot's text ID to the revision table for backwards compatibility
+               if ( $protoSlot->getRole() === SlotRecord::MAIN
+                       && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
+               ) {
+                       // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
+                       // with the real content ID below.
+                       $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+                       $contentId = $this->emulateContentId( $textId );
+               }
+
+               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+                       if ( $protoSlot->hasContentId() ) {
+                               $contentId = $protoSlot->getContentId();
+                       } else {
+                               $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
+                       }
+
+                       $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
+               }
+
+               $savedSlot = SlotRecord::newSaved(
+                       $revisionId,
+                       $contentId,
+                       $blobAddress,
+                       $protoSlot
+               );
+
+               return $savedSlot;
+       }
+
+       /**
+        * Insert IP revision into ip_changes for use when querying for a range.
+        * @param IDatabase $dbw
+        * @param User $user
+        * @param RevisionRecord $rev
+        * @param int $revisionId
+        */
+       private function insertIpChangesRow(
+               IDatabase $dbw,
+               User $user,
+               RevisionRecord $rev,
+               $revisionId
+       ) {
+               if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
+                       $ipcRow = [
+                               'ipc_rev_id'        => $revisionId,
+                               'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
+                               'ipc_hex'           => IP::toHex( $user->getName() ),
+                       ];
+                       $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
+               }
+       }
+
+       /**
+        * @param IDatabase $dbw
+        * @param RevisionRecord $rev
+        * @param Title $title
+        * @param int $parentId
+        *
+        * @return array a revision table row
+        *
+        * @throws MWException
+        * @throws MWUnknownContentModelException
+        */
+       private function insertRevisionRowOn(
+               IDatabase $dbw,
+               RevisionRecord $rev,
+               Title $title,
+               $parentId
+       ) {
+               $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
+
+               list( $commentFields, $commentCallback ) =
+                       $this->commentStore->insertWithTempTable(
+                               $dbw,
+                               'rev_comment',
+                               $rev->getComment( RevisionRecord::RAW )
+                       );
+               $revisionRow += $commentFields;
+
+               list( $actorFields, $actorCallback ) =
+                       $this->actorMigration->getInsertValuesWithTempTable(
+                               $dbw,
+                               'rev_user',
+                               $rev->getUser( RevisionRecord::RAW )
+                       );
+               $revisionRow += $actorFields;
+
+               $dbw->insert( 'revision', $revisionRow, __METHOD__ );
+
+               if ( !isset( $revisionRow['rev_id'] ) ) {
+                       // only if auto-increment was used
+                       $revisionRow['rev_id'] = intval( $dbw->insertId() );
+
+                       if ( $dbw->getType() === 'mysql' ) {
+                               // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
+                               // auto-increment value to disk, so on server restart it might reuse IDs from deleted
+                               // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
+
+                               $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
+                               $table = 'archive';
+                               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+                                       $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
+                                       if ( $maxRevId2 >= $maxRevId ) {
+                                               $maxRevId = $maxRevId2;
+                                               $table = 'slots';
+                                       }
+                               }
+
+                               if ( $maxRevId >= $revisionRow['rev_id'] ) {
+                                       $this->logger->debug(
+                                               '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
+                                                       . ' Trying to fix it.',
+                                               [
+                                                       'revid' => $revisionRow['rev_id'],
+                                                       'table' => $table,
+                                                       'maxrevid' => $maxRevId,
+                                               ]
+                                       );
+
+                                       if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
+                                               throw new MWException( 'Failed to get database lock for T202032' );
+                                       }
+                                       $fname = __METHOD__;
+                                       $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) {
+                                               $dbw->unlock( 'fix-for-T202032', $fname );
+                                       } );
+
+                                       $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
+
+                                       // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
+                                       // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
+                                       // inserts too, though, at least on MariaDB 10.1.29.
+                                       //
+                                       // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
+                                       // transactions in this code path thanks to the row lock from the original ->insert() above.
+                                       //
+                                       // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
+                                       // that's for non-MySQL DBs.
+                                       $row1 = $dbw->query(
+                                               $dbw->selectSqlText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
+                                       )->fetchObject();
+                                       if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+                                               $row2 = $dbw->query(
+                                                       $dbw->selectSqlText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
+                                                               . ' FOR UPDATE'
+                                               )->fetchObject();
+                                       } else {
+                                               $row2 = null;
+                                       }
+                                       $maxRevId = max(
+                                               $maxRevId,
+                                               $row1 ? intval( $row1->v ) : 0,
+                                               $row2 ? intval( $row2->v ) : 0
+                                       );
+
+                                       // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
+                                       // transactions will throw a duplicate key error here. It doesn't seem worth trying
+                                       // to avoid that.
+                                       $revisionRow['rev_id'] = $maxRevId + 1;
+                                       $dbw->insert( 'revision', $revisionRow, __METHOD__ );
+                               }
+                       }
+               }
+
+               $commentCallback( $revisionRow['rev_id'] );
+               $actorCallback( $revisionRow['rev_id'], $revisionRow );
+
+               return $revisionRow;
+       }
+
+       /**
+        * @param IDatabase $dbw
+        * @param RevisionRecord $rev
+        * @param Title $title
+        * @param int $parentId
+        *
+        * @return array [ 0 => array $revisionRow, 1 => callable  ]
+        * @throws MWException
+        * @throws MWUnknownContentModelException
+        */
+       private function getBaseRevisionRow(
+               IDatabase $dbw,
+               RevisionRecord $rev,
+               Title $title,
+               $parentId
+       ) {
+               // Record the edit in revisions
+               $revisionRow = [
+                       'rev_page'       => $rev->getPageId(),
+                       'rev_parent_id'  => $parentId,
+                       'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
+                       'rev_timestamp'  => $dbw->timestamp( $rev->getTimestamp() ),
+                       'rev_deleted'    => $rev->getVisibility(),
+                       'rev_len'        => $rev->getSize(),
+                       'rev_sha1'       => $rev->getSha1(),
+               ];
+
+               if ( $rev->getId() !== null ) {
+                       // Needed to restore revisions with their original ID
+                       $revisionRow['rev_id'] = $rev->getId();
+               }
+
+               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
+                       // In non MCR mode this IF section will relate to the main slot
+                       $mainSlot = $rev->getSlot( SlotRecord::MAIN );
+                       $model = $mainSlot->getModel();
+                       $format = $mainSlot->getFormat();
+
+                       // MCR migration note: rev_content_model and rev_content_format will go away
+                       if ( $this->contentHandlerUseDB ) {
+                               $this->assertCrossWikiContentLoadingIsSafe();
+
+                               $defaultModel = ContentHandler::getDefaultModelFor( $title );
+                               $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+                               $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
+                               $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
+                       }
+               }
+
+               return $revisionRow;
+       }
+
+       /**
+        * @param SlotRecord $slot
+        * @param Title $title
+        * @param array $blobHints See the BlobStore::XXX_HINT constants
+        *
+        * @throws MWException
+        * @return string the blob address
+        */
+       private function storeContentBlob(
+               SlotRecord $slot,
+               Title $title,
+               array $blobHints = []
+       ) {
+               $content = $slot->getContent();
+               $format = $content->getDefaultFormat();
+               $model = $content->getModel();
+
+               $this->checkContent( $content, $title );
+
+               return $this->blobStore->storeBlob(
+                       $content->serialize( $format ),
+                       // These hints "leak" some information from the higher abstraction layer to
+                       // low level storage to allow for optimization.
+                       array_merge(
+                               $blobHints,
+                               [
+                                       BlobStore::DESIGNATION_HINT => 'page-content',
+                                       BlobStore::ROLE_HINT => $slot->getRole(),
+                                       BlobStore::SHA1_HINT => $slot->getSha1(),
+                                       BlobStore::MODEL_HINT => $model,
+                                       BlobStore::FORMAT_HINT => $format,
+                               ]
+                       )
+               );
+       }
+
+       /**
+        * @param SlotRecord $slot
+        * @param IDatabase $dbw
+        * @param int $revisionId
+        * @param int $contentId
+        */
+       private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
+               $slotRow = [
+                       'slot_revision_id' => $revisionId,
+                       'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
+                       'slot_content_id' => $contentId,
+                       // If the slot has a specific origin use that ID, otherwise use the ID of the revision
+                       // that we just inserted.
+                       'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
+               ];
+               $dbw->insert( 'slots', $slotRow, __METHOD__ );
+       }
+
+       /**
+        * @param SlotRecord $slot
+        * @param IDatabase $dbw
+        * @param string $blobAddress
+        * @return int content row ID
+        */
+       private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
+               $contentRow = [
+                       'content_size' => $slot->getSize(),
+                       'content_sha1' => $slot->getSha1(),
+                       'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
+                       'content_address' => $blobAddress,
+               ];
+               $dbw->insert( 'content', $contentRow, __METHOD__ );
+               return intval( $dbw->insertId() );
+       }
+
+       /**
+        * MCR migration note: this corresponds to Revision::checkContentModel
+        *
+        * @param Content $content
+        * @param Title $title
+        *
+        * @throws MWException
+        * @throws MWUnknownContentModelException
+        */
+       private function checkContent( Content $content, Title $title ) {
+               // Note: may return null for revisions that have not yet been inserted
+
+               $model = $content->getModel();
+               $format = $content->getDefaultFormat();
+               $handler = $content->getContentHandler();
+
+               $name = "$title";
+
+               if ( !$handler->isSupportedFormat( $format ) ) {
+                       throw new MWException( "Can't use format $format with content model $model on $name" );
+               }
+
+               if ( !$this->contentHandlerUseDB ) {
+                       // if $wgContentHandlerUseDB is not set,
+                       // all revisions must use the default content model and format.
+
+                       $this->assertCrossWikiContentLoadingIsSafe();
+
+                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
+                       $defaultHandler = ContentHandler::getForModelID( $defaultModel );
+                       $defaultFormat = $defaultHandler->getDefaultFormat();
+
+                       if ( $model != $defaultModel ) {
+                               throw new MWException( "Can't save non-default content model with "
+                                       . "\$wgContentHandlerUseDB disabled: model is $model, "
+                                       . "default for $name is $defaultModel"
+                               );
+                       }
+
+                       if ( $format != $defaultFormat ) {
+                               throw new MWException( "Can't use non-default content format with "
+                                       . "\$wgContentHandlerUseDB disabled: format is $format, "
+                                       . "default for $name is $defaultFormat"
+                               );
+                       }
+               }
+
+               if ( !$content->isValid() ) {
+                       throw new MWException(
+                               "New content for $name is not valid! Content model is $model"
+                       );
+               }
+       }
+
+       /**
+        * Create a new null-revision for insertion into a page's
+        * history. This will not re-save the text, but simply refer
+        * to the text from the previous version.
+        *
+        * Such revisions can for instance identify page rename
+        * operations and other such meta-modifications.
+        *
+        * @note This method grabs a FOR UPDATE lock on the relevant row of the page table,
+        * to prevent a new revision from being inserted before the null revision has been written
+        * to the database.
+        *
+        * MCR migration note: this replaces Revision::newNullRevision
+        *
+        * @todo Introduce newFromParentRevision(). newNullRevision can then be based on that
+        * (or go away).
+        *
+        * @param IDatabase $dbw used for obtaining the lock on the page table row
+        * @param Title $title Title of the page to read from
+        * @param CommentStoreComment $comment RevisionRecord's summary
+        * @param bool $minor Whether the revision should be considered as minor
+        * @param User $user The user to attribute the revision to
+        *
+        * @return RevisionRecord|null RevisionRecord or null on error
+        */
+       public function newNullRevision(
+               IDatabase $dbw,
+               Title $title,
+               CommentStoreComment $comment,
+               $minor,
+               User $user
+       ) {
+               $this->checkDatabaseWikiId( $dbw );
+
+               $pageId = $title->getArticleID();
+
+               // T51581: Lock the page table row to ensure no other process
+               // is adding a revision to the page at the same time.
+               // Avoid locking extra tables, compare T191892.
+               $pageLatest = $dbw->selectField(
+                       'page',
+                       'page_latest',
+                       [ 'page_id' => $pageId ],
+                       __METHOD__,
+                       [ 'FOR UPDATE' ]
+               );
+
+               if ( !$pageLatest ) {
+                       return null;
+               }
+
+               // Fetch the actual revision row from master, without locking all extra tables.
+               $oldRevision = $this->loadRevisionFromConds(
+                       $dbw,
+                       [ 'rev_id' => intval( $pageLatest ) ],
+                       self::READ_LATEST,
+                       $title
+               );
+
+               if ( !$oldRevision ) {
+                       $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
+                       $this->logger->error(
+                               $msg,
+                               [ 'exception' => new RuntimeException( $msg ) ]
+                       );
+                       return null;
+               }
+
+               // Construct the new revision
+               $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
+               $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
+
+               $newRevision->setComment( $comment );
+               $newRevision->setUser( $user );
+               $newRevision->setTimestamp( $timestamp );
+               $newRevision->setMinorEdit( $minor );
+
+               return $newRevision;
+       }
+
+       /**
+        * MCR migration note: this replaces Revision::isUnpatrolled
+        *
+        * @todo This is overly specific, so move or kill this method.
+        *
+        * @param RevisionRecord $rev
+        *
+        * @return int Rcid of the unpatrolled row, zero if there isn't one
+        */
+       public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
+               $rc = $this->getRecentChange( $rev );
+               if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
+                       return $rc->getAttribute( 'rc_id' );
+               } else {
+                       return 0;
+               }
+       }
+
+       /**
+        * Get the RC object belonging to the current revision, if there's one
+        *
+        * MCR migration note: this replaces Revision::getRecentChange
+        *
+        * @todo move this somewhere else?
+        *
+        * @param RevisionRecord $rev
+        * @param int $flags (optional) $flags include:
+        *      IDBAccessObject::READ_LATEST: Select the data from the master
+        *
+        * @return null|RecentChange
+        */
+       public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
+               list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
+               $db = $this->getDBConnection( $dbType );
+
+               $userIdentity = $rev->getUser( RevisionRecord::RAW );
+
+               if ( !$userIdentity ) {
+                       // If the revision has no user identity, chances are it never went
+                       // into the database, and doesn't have an RC entry.
+                       return null;
+               }
+
+               // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
+               $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
+               $rc = RecentChange::newFromConds(
+                       [
+                               $actorWhere['conds'],
+                               'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
+                               'rc_this_oldid' => $rev->getId()
+                       ],
+                       __METHOD__,
+                       $dbType
+               );
+
+               $this->releaseDBConnection( $db );
+
+               // XXX: cache this locally? Glue it to the RevisionRecord?
+               return $rc;
+       }
+
+       /**
+        * Maps fields of the archive row to corresponding revision rows.
+        *
+        * @param object $archiveRow
+        *
+        * @return object a revision row object, corresponding to $archiveRow.
+        */
+       private static function mapArchiveFields( $archiveRow ) {
+               $fieldMap = [
+                       // keep with ar prefix:
+                       'ar_id'        => 'ar_id',
+
+                       // not the same suffix:
+                       'ar_page_id'        => 'rev_page',
+                       'ar_rev_id'         => 'rev_id',
+
+                       // same suffix:
+                       'ar_text_id'        => 'rev_text_id',
+                       'ar_timestamp'      => 'rev_timestamp',
+                       'ar_user_text'      => 'rev_user_text',
+                       'ar_user'           => 'rev_user',
+                       'ar_actor'          => 'rev_actor',
+                       'ar_minor_edit'     => 'rev_minor_edit',
+                       'ar_deleted'        => 'rev_deleted',
+                       'ar_len'            => 'rev_len',
+                       'ar_parent_id'      => 'rev_parent_id',
+                       'ar_sha1'           => 'rev_sha1',
+                       'ar_comment'        => 'rev_comment',
+                       'ar_comment_cid'    => 'rev_comment_cid',
+                       'ar_comment_id'     => 'rev_comment_id',
+                       'ar_comment_text'   => 'rev_comment_text',
+                       'ar_comment_data'   => 'rev_comment_data',
+                       'ar_comment_old'    => 'rev_comment_old',
+                       'ar_content_format' => 'rev_content_format',
+                       'ar_content_model'  => 'rev_content_model',
+               ];
+
+               $revRow = new stdClass();
+               foreach ( $fieldMap as $arKey => $revKey ) {
+                       if ( property_exists( $archiveRow, $arKey ) ) {
+                               $revRow->$revKey = $archiveRow->$arKey;
+                       }
+               }
+
+               return $revRow;
+       }
+
+       /**
+        * Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
+        *
+        * @param object|array $row Either a database row or an array
+        * @param int $queryFlags for callbacks
+        * @param Title $title
+        *
+        * @return SlotRecord The main slot, extracted from the MW 1.29 style row.
+        * @throws MWException
+        */
+       private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
+               $mainSlotRow = new stdClass();
+               $mainSlotRow->role_name = SlotRecord::MAIN;
+               $mainSlotRow->model_name = null;
+               $mainSlotRow->slot_revision_id = null;
+               $mainSlotRow->slot_content_id = null;
+               $mainSlotRow->content_address = null;
+
+               $content = null;
+               $blobData = null;
+               $blobFlags = null;
+
+               if ( is_object( $row ) ) {
+                       if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
+                               // Don't emulate from a row when using the new schema.
+                               // Emulating from an array is still OK.
+                               throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
+                       }
+
+                       // archive row
+                       if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
+                               $row = $this->mapArchiveFields( $row );
+                       }
+
+                       if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
+                               $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
+                                       $row->rev_text_id
+                               );
+                       }
+
+                       // This is used by null-revisions
+                       $mainSlotRow->slot_origin = isset( $row->slot_origin )
+                               ? intval( $row->slot_origin )
+                               : null;
+
+                       if ( isset( $row->old_text ) ) {
+                               // this happens when the text-table gets joined directly, in the pre-1.30 schema
+                               $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
+                               // Check against selects that might have not included old_flags
+                               if ( !property_exists( $row, 'old_flags' ) ) {
+                                       throw new InvalidArgumentException( 'old_flags was not set in $row' );
+                               }
+                               $blobFlags = $row->old_flags ?? '';
+                       }
+
+                       $mainSlotRow->slot_revision_id = intval( $row->rev_id );
+
+                       $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
+                       $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
+                       $mainSlotRow->model_name = isset( $row->rev_content_model )
+                               ? strval( $row->rev_content_model )
+                               : null;
+                       // XXX: in the future, we'll probably always use the default format, and drop content_format
+                       $mainSlotRow->format_name = isset( $row->rev_content_format )
+                               ? strval( $row->rev_content_format )
+                               : null;
+
+                       if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
+                               // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
+                               $mainSlotRow->slot_content_id
+                                       = $this->emulateContentId( intval( $row->rev_text_id ) );
+                       }
+               } elseif ( is_array( $row ) ) {
+                       $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
+
+                       $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
+                               ? intval( $row['slot_origin'] )
+                               : null;
+                       $mainSlotRow->content_address = isset( $row['text_id'] )
+                               ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
+                               : null;
+                       $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
+                       $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
+
+                       $mainSlotRow->model_name = isset( $row['content_model'] )
+                               ? strval( $row['content_model'] ) : null;  // XXX: must be a string!
+                       // XXX: in the future, we'll probably always use the default format, and drop content_format
+                       $mainSlotRow->format_name = isset( $row['content_format'] )
+                               ? strval( $row['content_format'] ) : null;
+                       $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
+                       // XXX: If the flags field is not set then $blobFlags should be null so that no
+                       // decoding will happen. An empty string will result in default decodings.
+                       $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
+
+                       // if we have a Content object, override mText and mContentModel
+                       if ( !empty( $row['content'] ) ) {
+                               if ( !( $row['content'] instanceof Content ) ) {
+                                       throw new MWException( 'content field must contain a Content object.' );
+                               }
+
+                               /** @var Content $content */
+                               $content = $row['content'];
+                               $handler = $content->getContentHandler();
+
+                               $mainSlotRow->model_name = $content->getModel();
+
+                               // XXX: in the future, we'll probably always use the default format.
+                               if ( $mainSlotRow->format_name === null ) {
+                                       $mainSlotRow->format_name = $handler->getDefaultFormat();
+                               }
+                       }
+
+                       if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
+                               // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
+                               $mainSlotRow->slot_content_id
+                                       = $this->emulateContentId( intval( $row['text_id'] ) );
+                       }
+               } else {
+                       throw new MWException( 'Revision constructor passed invalid row format.' );
+               }
+
+               // With the old schema, the content changes with every revision,
+               // except for null-revisions.
+               if ( !isset( $mainSlotRow->slot_origin ) ) {
+                       $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
+               }
+
+               if ( $mainSlotRow->model_name === null ) {
+                       $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
+                               $this->assertCrossWikiContentLoadingIsSafe();
+
+                               // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget!
+                               // TODO: MCR: deprecate $title->getModel().
+                               return ContentHandler::getDefaultModelFor( $title );
+                       };
+               }
+
+               if ( !$content ) {
+                       // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
+                       // is missing, but "empty revisions" with no content are used in some edge cases.
+
+                       $content = function ( SlotRecord $slot )
+                               use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
+                       {
+                               return $this->loadSlotContent(
+                                       $slot,
+                                       $blobData,
+                                       $blobFlags,
+                                       $mainSlotRow->format_name,
+                                       $queryFlags
+                               );
+                       };
+               }
+
+               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+                       // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
+                       // the inherited slot to have the same content_id as the original slot. In that case,
+                       // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
+                       $mainSlotRow->slot_content_id =
+                               function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
+                                       $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+                                       return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
+                               };
+               }
+
+               return new SlotRecord( $mainSlotRow, $content );
+       }
+
+       /**
+        * Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode,
+        * based on the revision's text ID (rev_text_id or ar_text_id, respectively).
+        * Note that in SCHEMA_COMPAT_WRITE_BOTH, a callback to findSlotContentId() should be used
+        * instead, since in that mode, some revision rows may already have a real content ID,
+        * while other's don't - and for the ones that don't, we should indicate that it
+        * is missing and cause SlotRecords::hasContentId() to return false.
+        *
+        * @param int $textId
+        * @return int The emulated content ID
+        */
+       private function emulateContentId( $textId ) {
+               // Return a negative number to ensure the ID is distinct from any real content IDs
+               // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
+               // mode.
+               return -$textId;
+       }
+
+       /**
+        * Loads a Content object based on a slot row.
+        *
+        * This method does not call $slot->getContent(), and may be used as a callback
+        * called by $slot->getContent().
+        *
+        * MCR migration note: this roughly corresponds to Revision::getContentInternal
+        *
+        * @param SlotRecord $slot The SlotRecord to load content for
+        * @param string|null $blobData The content blob, in the form indicated by $blobFlags
+        * @param string|null $blobFlags Flags indicating how $blobData needs to be processed.
+        *        Use null if no processing should happen. That is in constrast to the empty string,
+        *        which causes the blob to be decoded according to the configured legacy encoding.
+        * @param string|null $blobFormat MIME type indicating how $dataBlob is encoded
+        * @param int $queryFlags
+        *
+        * @throws RevisionAccessException
+        * @return Content
+        */
+       private function loadSlotContent(
+               SlotRecord $slot,
+               $blobData = null,
+               $blobFlags = null,
+               $blobFormat = null,
+               $queryFlags = 0
+       ) {
+               if ( $blobData !== null ) {
+                       Assert::parameterType( 'string', $blobData, '$blobData' );
+                       Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
+
+                       $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
+
+                       if ( $blobFlags === null ) {
+                               // No blob flags, so use the blob verbatim.
+                               $data = $blobData;
+                       } else {
+                               $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
+                               if ( $data === false ) {
+                                       throw new RevisionAccessException(
+                                               "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
+                                       );
+                               }
+                       }
+
+               } else {
+                       $address = $slot->getAddress();
+                       try {
+                               $data = $this->blobStore->getBlob( $address, $queryFlags );
+                       } catch ( BlobAccessException $e ) {
+                               throw new RevisionAccessException(
+                                       "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
+                               );
+                       }
+               }
+
+               // Unserialize content
+               $handler = ContentHandler::getForModelID( $slot->getModel() );
+
+               $content = $handler->unserializeContent( $data, $blobFormat );
+               return $content;
+       }
+
+       /**
+        * Load a page revision from a given revision ID number.
+        * Returns null if no such revision can be found.
+        *
+        * MCR migration note: this replaces Revision::newFromId
+        *
+        * $flags include:
+        *      IDBAccessObject::READ_LATEST: Select the data from the master
+        *      IDBAccessObject::READ_LOCKING : Select & lock the data from the master
+        *
+        * @param int $id
+        * @param int $flags (optional)
+        * @return RevisionRecord|null
+        */
+       public function getRevisionById( $id, $flags = 0 ) {
+               return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
+       }
+
+       /**
+        * Load either the current, or a specified, revision
+        * that's attached to a given link target. If not attached
+        * to that link target, will return null.
+        *
+        * MCR migration note: this replaces Revision::newFromTitle
+        *
+        * $flags include:
+        *      IDBAccessObject::READ_LATEST: Select the data from the master
+        *      IDBAccessObject::READ_LOCKING : Select & lock the data from the master
+        *
+        * @param LinkTarget $linkTarget
+        * @param int $revId (optional)
+        * @param int $flags Bitfield (optional)
+        * @return RevisionRecord|null
+        */
+       public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
+               $conds = [
+                       'page_namespace' => $linkTarget->getNamespace(),
+                       'page_title' => $linkTarget->getDBkey()
+               ];
+               if ( $revId ) {
+                       // Use the specified revision ID.
+                       // Note that we use newRevisionFromConds here because we want to retry
+                       // and fall back to master if the page is not found on a replica.
+                       // Since the caller supplied a revision ID, we are pretty sure the revision is
+                       // supposed to exist, so we should try hard to find it.
+                       $conds['rev_id'] = $revId;
+                       return $this->newRevisionFromConds( $conds, $flags );
+               } else {
+                       // Use a join to get the latest revision.
+                       // Note that we don't use newRevisionFromConds here because we don't want to retry
+                       // and fall back to master. The assumption is that we only want to force the fallback
+                       // if we are quite sure the revision exists because the caller supplied a revision ID.
+                       // If the page isn't found at all on a replica, it probably simply does not exist.
+                       $db = $this->getDBConnectionRefForQueryFlags( $flags );
+
+                       $conds[] = 'rev_id=page_latest';
+                       $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
+
+                       return $rev;
+               }
+       }
+
+       /**
+        * Load either the current, or a specified, revision
+        * that's attached to a given page ID.
+        * Returns null if no such revision can be found.
+        *
+        * MCR migration note: this replaces Revision::newFromPageId
+        *
+        * $flags include:
+        *      IDBAccessObject::READ_LATEST: Select the data from the master (since 1.20)
+        *      IDBAccessObject::READ_LOCKING : Select & lock the data from the master
+        *
+        * @param int $pageId
+        * @param int $revId (optional)
+        * @param int $flags Bitfield (optional)
+        * @return RevisionRecord|null
+        */
+       public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
+               $conds = [ 'page_id' => $pageId ];
+               if ( $revId ) {
+                       // Use the specified revision ID.
+                       // Note that we use newRevisionFromConds here because we want to retry
+                       // and fall back to master if the page is not found on a replica.
+                       // Since the caller supplied a revision ID, we are pretty sure the revision is
+                       // supposed to exist, so we should try hard to find it.
+                       $conds['rev_id'] = $revId;
+                       return $this->newRevisionFromConds( $conds, $flags );
+               } else {
+                       // Use a join to get the latest revision.
+                       // Note that we don't use newRevisionFromConds here because we don't want to retry
+                       // and fall back to master. The assumption is that we only want to force the fallback
+                       // if we are quite sure the revision exists because the caller supplied a revision ID.
+                       // If the page isn't found at all on a replica, it probably simply does not exist.
+                       $db = $this->getDBConnectionRefForQueryFlags( $flags );
+
+                       $conds[] = 'rev_id=page_latest';
+                       $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
+
+                       return $rev;
+               }
+       }
+
+       /**
+        * Load the revision for the given title with the given timestamp.
+        * WARNING: Timestamps may in some circumstances not be unique,
+        * so this isn't the best key to use.
+        *
+        * MCR migration note: this replaces Revision::loadFromTimestamp
+        *
+        * @param Title $title
+        * @param string $timestamp
+        * @return RevisionRecord|null
+        */
+       public function getRevisionByTimestamp( $title, $timestamp ) {
+               $db = $this->getDBConnection( DB_REPLICA );
+               return $this->newRevisionFromConds(
+                       [
+                               'rev_timestamp' => $db->timestamp( $timestamp ),
+                               'page_namespace' => $title->getNamespace(),
+                               'page_title' => $title->getDBkey()
+                       ],
+                       0,
+                       $title
+               );
+       }
+
+       /**
+        * @param int $revId The revision to load slots for.
+        * @param int $queryFlags
+        *
+        * @return SlotRecord[]
+        */
+       private function loadSlotRecords( $revId, $queryFlags ) {
+               $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
+
+               list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+               $db = $this->getDBConnectionRef( $dbMode );
+
+               $res = $db->select(
+                       $revQuery['tables'],
+                       $revQuery['fields'],
+                       [
+                               'slot_revision_id' => $revId,
+                       ],
+                       __METHOD__,
+                       $dbOptions,
+                       $revQuery['joins']
+               );
+
+               $slots = [];
+
+               foreach ( $res as $row ) {
+                       // resolve role names and model names from in-memory cache, instead of joining.
+                       $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
+                       $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
+
+                       $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) {
+                               return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
+                       };
+
+                       $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
+               }
+
+               if ( !isset( $slots[SlotRecord::MAIN] ) ) {
+                       throw new RevisionAccessException(
+                               'Main slot of revision ' . $revId . ' not found in database!'
+                       );
+               };
+
+               return $slots;
+       }
+
+       /**
+        * Factory method for RevisionSlots.
+        *
+        * @note If other code has a need to construct RevisionSlots objects, this should be made
+        * public, since RevisionSlots instances should not be constructed directly.
+        *
+        * @param int $revId
+        * @param object $revisionRow
+        * @param int $queryFlags
+        * @param Title $title
+        *
+        * @return RevisionSlots
+        * @throws MWException
+        */
+       private function newRevisionSlots(
+               $revId,
+               $revisionRow,
+               $queryFlags,
+               Title $title
+       ) {
+               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
+                       $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
+                       $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
+               } else {
+                       // XXX: do we need the same kind of caching here
+                       // that getKnownCurrentRevision uses (if $revId == page_latest?)
+
+                       $slots = new RevisionSlots( function () use( $revId, $queryFlags ) {
+                               return $this->loadSlotRecords( $revId, $queryFlags );
+                       } );
+               }
+
+               return $slots;
+       }
+
+       /**
+        * Make a fake revision object from an archive table row. This is queried
+        * for permissions or even inserted (as in Special:Undelete)
+        *
+        * MCR migration note: this replaces Revision::newFromArchiveRow
+        *
+        * @param object $row
+        * @param int $queryFlags
+        * @param Title|null $title
+        * @param array $overrides associative array with fields of $row to override. This may be
+        *   used e.g. to force the parent revision ID or page ID. Keys in the array are fields
+        *   names from the archive table without the 'ar_' prefix, i.e. use 'parent_id' to
+        *   override ar_parent_id.
+        *
+        * @return RevisionRecord
+        * @throws MWException
+        */
+       public function newRevisionFromArchiveRow(
+               $row,
+               $queryFlags = 0,
+               Title $title = null,
+               array $overrides = []
+       ) {
+               Assert::parameterType( 'object', $row, '$row' );
+
+               // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
+               Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
+
+               if ( !$title && isset( $overrides['title'] ) ) {
+                       if ( !( $overrides['title'] instanceof Title ) ) {
+                               throw new MWException( 'title field override must contain a Title object.' );
+                       }
+
+                       $title = $overrides['title'];
+               }
+
+               if ( !isset( $title ) ) {
+                       if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
+                               $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+                       } else {
+                               throw new InvalidArgumentException(
+                                       'A Title or ar_namespace and ar_title must be given'
+                               );
+                       }
+               }
+
+               foreach ( $overrides as $key => $value ) {
+                       $field = "ar_$key";
+                       $row->$field = $value;
+               }
+
+               try {
+                       $user = User::newFromAnyId(
+                               $row->ar_user ?? null,
+                               $row->ar_user_text ?? null,
+                               $row->ar_actor ?? null
+                       );
+               } catch ( InvalidArgumentException $ex ) {
+                       wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
+                       $user = new UserIdentityValue( 0, 'Unknown user', 0 );
+               }
+
+               $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+               // Legacy because $row may have come from self::selectFields()
+               $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
+
+               $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
+
+               return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
+       }
+
+       /**
+        * @see RevisionFactory::newRevisionFromRow
+        *
+        * MCR migration note: this replaces Revision::newFromRow
+        *
+        * @param object $row
+        * @param int $queryFlags
+        * @param Title|null $title
+        *
+        * @return RevisionRecord
+        */
+       public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
+               Assert::parameterType( 'object', $row, '$row' );
+
+               if ( !$title ) {
+                       $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
+                       $revId = $row->rev_id ?? 0;
+
+                       $title = $this->getTitle( $pageId, $revId, $queryFlags );
+               }
+
+               if ( !isset( $row->page_latest ) ) {
+                       $row->page_latest = $title->getLatestRevID();
+                       if ( $row->page_latest === 0 && $title->exists() ) {
+                               wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
+                       }
+               }
+
+               try {
+                       $user = User::newFromAnyId(
+                               $row->rev_user ?? null,
+                               $row->rev_user_text ?? null,
+                               $row->rev_actor ?? null
+                       );
+               } catch ( InvalidArgumentException $ex ) {
+                       wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
+                       $user = new UserIdentityValue( 0, 'Unknown user', 0 );
+               }
+
+               $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+               // Legacy because $row may have come from self::selectFields()
+               $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
+
+               $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
+
+               return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
+       }
+
+       /**
+        * Constructs a new MutableRevisionRecord based on the given associative array following
+        * the MW1.29 convention for the Revision constructor.
+        *
+        * MCR migration note: this replaces Revision::newFromRow
+        *
+        * @param array $fields
+        * @param int $queryFlags
+        * @param Title|null $title
+        *
+        * @return MutableRevisionRecord
+        * @throws MWException
+        * @throws RevisionAccessException
+        */
+       public function newMutableRevisionFromArray(
+               array $fields,
+               $queryFlags = 0,
+               Title $title = null
+       ) {
+               if ( !$title && isset( $fields['title'] ) ) {
+                       if ( !( $fields['title'] instanceof Title ) ) {
+                               throw new MWException( 'title field must contain a Title object.' );
+                       }
+
+                       $title = $fields['title'];
+               }
+
+               if ( !$title ) {
+                       $pageId = $fields['page'] ?? 0;
+                       $revId = $fields['id'] ?? 0;
+
+                       $title = $this->getTitle( $pageId, $revId, $queryFlags );
+               }
+
+               if ( !isset( $fields['page'] ) ) {
+                       $fields['page'] = $title->getArticleID( $queryFlags );
+               }
+
+               // if we have a content object, use it to set the model and type
+               if ( !empty( $fields['content'] ) ) {
+                       if ( !( $fields['content'] instanceof Content ) && !is_array( $fields['content'] ) ) {
+                               throw new MWException(
+                                       'content field must contain a Content object or an array of Content objects.'
+                               );
+                       }
+               }
+
+               if ( !empty( $fields['text_id'] ) ) {
+                       if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+                               throw new MWException( "The text_id field is only available in the pre-MCR schema" );
+                       }
+
+                       if ( !empty( $fields['content'] ) ) {
+                               throw new MWException(
+                                       "Text already stored in external store (id {$fields['text_id']}), " .
+                                       "can't specify content object"
+                               );
+                       }
+               }
+
+               if (
+                       isset( $fields['comment'] )
+                       && !( $fields['comment'] instanceof CommentStoreComment )
+               ) {
+                       $commentData = $fields['comment_data'] ?? null;
+
+                       if ( $fields['comment'] instanceof Message ) {
+                               $fields['comment'] = CommentStoreComment::newUnsavedComment(
+                                       $fields['comment'],
+                                       $commentData
+                               );
+                       } else {
+                               $commentText = trim( strval( $fields['comment'] ) );
+                               $fields['comment'] = CommentStoreComment::newUnsavedComment(
+                                       $commentText,
+                                       $commentData
+                               );
+                       }
+               }
+
+               $revision = new MutableRevisionRecord( $title, $this->wikiId );
+               $this->initializeMutableRevisionFromArray( $revision, $fields );
+
+               if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
+                       foreach ( $fields['content'] as $role => $content ) {
+                               $revision->setContent( $role, $content );
+                       }
+               } else {
+                       $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
+                       $revision->setSlot( $mainSlot );
+               }
+
+               return $revision;
+       }
+
+       /**
+        * @param MutableRevisionRecord $record
+        * @param array $fields
+        */
+       private function initializeMutableRevisionFromArray(
+               MutableRevisionRecord $record,
+               array $fields
+       ) {
+               /** @var UserIdentity $user */
+               $user = null;
+
+               if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
+                       $user = $fields['user'];
+               } else {
+                       try {
+                               $user = User::newFromAnyId(
+                                       $fields['user'] ?? null,
+                                       $fields['user_text'] ?? null,
+                                       $fields['actor'] ?? null
+                               );
+                       } catch ( InvalidArgumentException $ex ) {
+                               $user = null;
+                       }
+               }
+
+               if ( $user ) {
+                       $record->setUser( $user );
+               }
+
+               $timestamp = isset( $fields['timestamp'] )
+                       ? strval( $fields['timestamp'] )
+                       : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
+
+               $record->setTimestamp( $timestamp );
+
+               if ( isset( $fields['page'] ) ) {
+                       $record->setPageId( intval( $fields['page'] ) );
+               }
+
+               if ( isset( $fields['id'] ) ) {
+                       $record->setId( intval( $fields['id'] ) );
+               }
+               if ( isset( $fields['parent_id'] ) ) {
+                       $record->setParentId( intval( $fields['parent_id'] ) );
+               }
+
+               if ( isset( $fields['sha1'] ) ) {
+                       $record->setSha1( $fields['sha1'] );
+               }
+               if ( isset( $fields['size'] ) ) {
+                       $record->setSize( intval( $fields['size'] ) );
+               }
+
+               if ( isset( $fields['minor_edit'] ) ) {
+                       $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
+               }
+               if ( isset( $fields['deleted'] ) ) {
+                       $record->setVisibility( intval( $fields['deleted'] ) );
+               }
+
+               if ( isset( $fields['comment'] ) ) {
+                       Assert::parameterType(
+                               CommentStoreComment::class,
+                               $fields['comment'],
+                               '$row[\'comment\']'
+                       );
+                       $record->setComment( $fields['comment'] );
+               }
+       }
+
+       /**
+        * Load a page revision from a given revision ID number.
+        * Returns null if no such revision can be found.
+        *
+        * MCR migration note: this corresponds to Revision::loadFromId
+        *
+        * @note direct use is deprecated!
+        * @todo remove when unused! there seem to be no callers of Revision::loadFromId
+        *
+        * @param IDatabase $db
+        * @param int $id
+        *
+        * @return RevisionRecord|null
+        */
+       public function loadRevisionFromId( IDatabase $db, $id ) {
+               return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
+       }
+
+       /**
+        * Load either the current, or a specified, revision
+        * that's attached to a given page. If not attached
+        * to that page, will return null.
+        *
+        * MCR migration note: this replaces Revision::loadFromPageId
+        *
+        * @note direct use is deprecated!
+        * @todo remove when unused!
+        *
+        * @param IDatabase $db
+        * @param int $pageid
+        * @param int $id
+        * @return RevisionRecord|null
+        */
+       public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
+               $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
+               if ( $id ) {
+                       $conds['rev_id'] = intval( $id );
+               } else {
+                       $conds[] = 'rev_id=page_latest';
+               }
+               return $this->loadRevisionFromConds( $db, $conds );
+       }
+
+       /**
+        * Load either the current, or a specified, revision
+        * that's attached to a given page. If not attached
+        * to that page, will return null.
+        *
+        * MCR migration note: this replaces Revision::loadFromTitle
+        *
+        * @note direct use is deprecated!
+        * @todo remove when unused!
+        *
+        * @param IDatabase $db
+        * @param Title $title
+        * @param int $id
+        *
+        * @return RevisionRecord|null
+        */
+       public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
+               if ( $id ) {
+                       $matchId = intval( $id );
+               } else {
+                       $matchId = 'page_latest';
+               }
+
+               return $this->loadRevisionFromConds(
+                       $db,
+                       [
+                               "rev_id=$matchId",
+                               'page_namespace' => $title->getNamespace(),
+                               'page_title' => $title->getDBkey()
+                       ],
+                       0,
+                       $title
+               );
+       }
+
+       /**
+        * Load the revision for the given title with the given timestamp.
+        * WARNING: Timestamps may in some circumstances not be unique,
+        * so this isn't the best key to use.
+        *
+        * MCR migration note: this replaces Revision::loadFromTimestamp
+        *
+        * @note direct use is deprecated! Use getRevisionFromTimestamp instead!
+        * @todo remove when unused!
+        *
+        * @param IDatabase $db
+        * @param Title $title
+        * @param string $timestamp
+        * @return RevisionRecord|null
+        */
+       public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
+               return $this->loadRevisionFromConds( $db,
+                       [
+                               'rev_timestamp' => $db->timestamp( $timestamp ),
+                               'page_namespace' => $title->getNamespace(),
+                               'page_title' => $title->getDBkey()
+                       ],
+                       0,
+                       $title
+               );
+       }
+
+       /**
+        * Given a set of conditions, fetch a revision
+        *
+        * This method should be used if we are pretty sure the revision exists.
+        * Unless $flags has READ_LATEST set, this method will first try to find the revision
+        * on a replica before hitting the master database.
+        *
+        * MCR migration note: this corresponds to Revision::newFromConds
+        *
+        * @param array $conditions
+        * @param int $flags (optional)
+        * @param Title|null $title
+        *
+        * @return RevisionRecord|null
+        */
+       private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
+               $db = $this->getDBConnectionRefForQueryFlags( $flags );
+               $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
+
+               $lb = $this->getDBLoadBalancer();
+
+               // 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 )
+                       && $lb->getServerCount() > 1
+                       && $lb->hasOrMadeRecentMasterChanges()
+               ) {
+                       $flags = self::READ_LATEST;
+                       $dbw = $this->getDBConnection( DB_MASTER );
+                       $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
+                       $this->releaseDBConnection( $dbw );
+               }
+
+               return $rev;
+       }
+
+       /**
+        * Given a set of conditions, fetch a revision from
+        * the given database connection.
+        *
+        * MCR migration note: this corresponds to Revision::loadFromConds
+        *
+        * @param IDatabase $db
+        * @param array $conditions
+        * @param int $flags (optional)
+        * @param Title|null $title
+        *
+        * @return RevisionRecord|null
+        */
+       private function loadRevisionFromConds(
+               IDatabase $db,
+               $conditions,
+               $flags = 0,
+               Title $title = null
+       ) {
+               $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
+               if ( $row ) {
+                       $rev = $this->newRevisionFromRow( $row, $flags, $title );
+
+                       return $rev;
+               }
+
+               return null;
+       }
+
+       /**
+        * Throws an exception if the given database connection does not belong to the wiki this
+        * RevisionStore is bound to.
+        *
+        * @param IDatabase $db
+        * @throws MWException
+        */
+       private function checkDatabaseWikiId( IDatabase $db ) {
+               $storeWiki = $this->wikiId;
+               $dbWiki = $db->getDomainID();
+
+               if ( $dbWiki === $storeWiki ) {
+                       return;
+               }
+
+               $storeWiki = $storeWiki ?: $this->loadBalancer->getLocalDomainID();
+               // @FIXME: when would getDomainID() be false here?
+               $dbWiki = $dbWiki ?: wfWikiID();
+
+               if ( $dbWiki === $storeWiki ) {
+                       return;
+               }
+
+               // HACK: counteract encoding imposed by DatabaseDomain
+               $storeWiki = str_replace( '?h', '-', $storeWiki );
+               $dbWiki = str_replace( '?h', '-', $dbWiki );
+
+               if ( $dbWiki === $storeWiki ) {
+                       return;
+               }
+
+               throw new MWException( "RevisionStore for $storeWiki "
+                       . "cannot be used with a DB connection for $dbWiki" );
+       }
+
+       /**
+        * Given a set of conditions, return a row with the
+        * fields necessary to build RevisionRecord objects.
+        *
+        * MCR migration note: this corresponds to Revision::fetchFromConds
+        *
+        * @param IDatabase $db
+        * @param array $conditions
+        * @param int $flags (optional)
+        *
+        * @return object|false data row as a raw object
+        */
+       private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
+               $this->checkDatabaseWikiId( $db );
+
+               $revQuery = $this->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']
+               );
+       }
+
+       /**
+        * Finds the ID of a content row for a given revision and slot role.
+        * This can be used to re-use content rows even while the content ID
+        * is still missing from SlotRecords, when writing to both the old and
+        * the new schema during MCR schema migration.
+        *
+        * @todo remove after MCR schema migration is complete.
+        *
+        * @param IDatabase $db
+        * @param int $revId
+        * @param string $role
+        *
+        * @return int|null
+        */
+       private function findSlotContentId( IDatabase $db, $revId, $role ) {
+               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+                       return null;
+               }
+
+               try {
+                       $roleId = $this->slotRoleStore->getId( $role );
+                       $conditions = [
+                               'slot_revision_id' => $revId,
+                               'slot_role_id' => $roleId,
+                       ];
+
+                       $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
+
+                       return $contentId ?: null;
+               } catch ( NameTableAccessException $ex ) {
+                       // If the role is missing from the slot_roles table,
+                       // the corresponding row in slots cannot exist.
+                       return null;
+               }
+       }
+
+       /**
+        * Return the tables, fields, and join conditions to be selected to create
+        * a new RevisionStoreRecord object.
+        *
+        * MCR migration note: this replaces Revision::getQueryInfo
+        *
+        * If the format of fields returned changes in any way then the cache key provided by
+        * self::getRevisionRowCacheKey should be updated.
+        *
+        * @since 1.31
+        *
+        * @param array $options Any combination of the following strings
+        *  - 'page': Join with the page table, and select fields to identify the page
+        *  - 'user': Join with the user table, and select the user name
+        *  - 'text': Join with the text table, and select fields to load page text. This
+        *    option is deprecated in MW 1.32 when the MCR migration flag SCHEMA_COMPAT_WRITE_NEW
+        *    is set, and disallowed when SCHEMA_COMPAT_READ_OLD is not set.
+        *
+        * @return array With three keys:
+        *  - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+        *  - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+        *  - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+        */
+       public function getQueryInfo( $options = [] ) {
+               $ret = [
+                       'tables' => [],
+                       'fields' => [],
+                       'joins'  => [],
+               ];
+
+               $ret['tables'][] = 'revision';
+               $ret['fields'] = array_merge( $ret['fields'], [
+                       'rev_id',
+                       'rev_page',
+                       'rev_timestamp',
+                       'rev_minor_edit',
+                       'rev_deleted',
+                       'rev_len',
+                       'rev_parent_id',
+                       'rev_sha1',
+               ] );
+
+               $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
+               $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
+               $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
+               $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
+
+               $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
+               $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
+               $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
+               $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
+
+               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+                       $ret['fields'][] = 'rev_text_id';
+
+                       if ( $this->contentHandlerUseDB ) {
+                               $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',
+                       ] );
+                       $u = $actorQuery['fields']['rev_user'];
+                       $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
+               }
+
+               if ( in_array( 'text', $options, true ) ) {
+                       if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
+                               throw new InvalidArgumentException( 'text table can no longer be joined directly' );
+                       } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+                               // NOTE: even when this class is set to not read from the old schema, callers
+                               // should still be able to join against the text table, as long as we are still
+                               // writing the old schema for compatibility.
+                               // TODO: This should trigger a deprecation warning eventually (T200918), but not
+                               // before all known usages are removed (see T198341 and T201164).
+                               // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
+                       }
+
+                       $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 SlotRecord.
+        *
+        * @since 1.32
+        *
+        * @param array $options Any combination of the following strings
+        *  - 'content': Join with the content table, and select content meta-data fields
+        *  - 'model': Join with the content_models table, and select the model_name field.
+        *             Only applicable if 'content' is also set.
+        *  - 'role': Join with the slot_roles table, and select the role_name field
+        *
+        * @return array With three keys:
+        *  - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+        *  - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+        *  - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+        */
+       public function getSlotsQueryInfo( $options = [] ) {
+               $ret = [
+                       'tables' => [],
+                       'fields' => [],
+                       'joins'  => [],
+               ];
+
+               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+                       $db = $this->getDBConnectionRef( DB_REPLICA );
+                       $ret['tables']['slots'] = 'revision';
+
+                       $ret['fields']['slot_revision_id'] = 'slots.rev_id';
+                       $ret['fields']['slot_content_id'] = 'NULL';
+                       $ret['fields']['slot_origin'] = 'slots.rev_id';
+                       $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
+
+                       if ( in_array( 'content', $options, true ) ) {
+                               $ret['fields']['content_size'] = 'slots.rev_len';
+                               $ret['fields']['content_sha1'] = 'slots.rev_sha1';
+                               $ret['fields']['content_address']
+                                       = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] );
+
+                               if ( $this->contentHandlerUseDB ) {
+                                       $ret['fields']['model_name'] = 'slots.rev_content_model';
+                               } else {
+                                       $ret['fields']['model_name'] = 'NULL';
+                               }
+                       }
+               } else {
+                       $ret['tables'][] = 'slots';
+                       $ret['fields'] = array_merge( $ret['fields'], [
+                               'slot_revision_id',
+                               'slot_content_id',
+                               'slot_origin',
+                               'slot_role_id',
+                       ] );
+
+                       if ( in_array( 'role', $options, true ) ) {
+                               // Use left join to attach role name, so we still find the revision row even
+                               // if the role name is missing. This triggers a more obvious failure mode.
+                               $ret['tables'][] = 'slot_roles';
+                               $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
+                               $ret['fields'][] = 'role_name';
+                       }
+
+                       if ( in_array( 'content', $options, true ) ) {
+                               $ret['tables'][] = 'content';
+                               $ret['fields'] = array_merge( $ret['fields'], [
+                                       'content_size',
+                                       'content_sha1',
+                                       'content_address',
+                                       'content_model',
+                               ] );
+                               $ret['joins']['content'] = [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ];
+
+                               if ( in_array( 'model', $options, true ) ) {
+                                       // Use left join to attach model name, so we still find the revision row even
+                                       // if the model name is missing. This triggers a more obvious failure mode.
+                                       $ret['tables'][] = 'content_models';
+                                       $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
+                                       $ret['fields'][] = 'model_name';
+                               }
+
+                       }
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Return the tables, fields, and join conditions to be selected to create
+        * a new RevisionArchiveRecord object.
+        *
+        * MCR migration note: this replaces Revision::getArchiveQueryInfo
+        *
+        * @since 1.31
+        *
+        * @return array With three keys:
+        *   - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+        *   - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+        *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+        */
+       public function getArchiveQueryInfo() {
+               $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
+               $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
+               $ret = [
+                       'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
+                       'fields' => [
+                                       'ar_id',
+                                       'ar_page_id',
+                                       'ar_namespace',
+                                       'ar_title',
+                                       'ar_rev_id',
+                                       'ar_timestamp',
+                                       'ar_minor_edit',
+                                       'ar_deleted',
+                                       'ar_len',
+                                       'ar_parent_id',
+                                       'ar_sha1',
+                               ] + $commentQuery['fields'] + $actorQuery['fields'],
+                       'joins' => $commentQuery['joins'] + $actorQuery['joins'],
+               ];
+
+               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+                       $ret['fields'][] = 'ar_text_id';
+
+                       if ( $this->contentHandlerUseDB ) {
+                               $ret['fields'][] = 'ar_content_format';
+                               $ret['fields'][] = 'ar_content_model';
+                       }
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Do a batched query for the sizes of a set of revisions.
+        *
+        * MCR migration note: this replaces Revision::getParentLengths
+        *
+        * @param int[] $revIds
+        * @return int[] associative array mapping revision IDs from $revIds to the nominal size
+        *         of the corresponding revision.
+        */
+       public function getRevisionSizes( array $revIds ) {
+               return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds );
+       }
+
+       /**
+        * Do a batched query for the sizes of a set of revisions.
+        *
+        * MCR migration note: this replaces Revision::getParentLengths
+        *
+        * @deprecated use RevisionStore::getRevisionSizes instead.
+        *
+        * @param IDatabase $db
+        * @param int[] $revIds
+        * @return int[] associative array mapping revision IDs from $revIds to the nominal size
+        *         of the corresponding revision.
+        */
+       public function listRevisionSizes( IDatabase $db, array $revIds ) {
+               $this->checkDatabaseWikiId( $db );
+
+               $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] = intval( $row->rev_len );
+               }
+
+               return $revLens;
+       }
+
+       /**
+        * Get previous revision for this title
+        *
+        * MCR migration note: this replaces Revision::getPrevious
+        *
+        * @param RevisionRecord $rev
+        * @param Title|null $title if known (optional)
+        *
+        * @return RevisionRecord|null
+        */
+       public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
+               if ( $title === null ) {
+                       $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
+               }
+               $prev = $title->getPreviousRevisionID( $rev->getId() );
+               if ( $prev ) {
+                       return $this->getRevisionByTitle( $title, $prev );
+               }
+               return null;
+       }
+
+       /**
+        * Get next revision for this title
+        *
+        * MCR migration note: this replaces Revision::getNext
+        *
+        * @param RevisionRecord $rev
+        * @param Title|null $title if known (optional)
+        *
+        * @return RevisionRecord|null
+        */
+       public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
+               if ( $title === null ) {
+                       $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
+               }
+               $next = $title->getNextRevisionID( $rev->getId() );
+               if ( $next ) {
+                       return $this->getRevisionByTitle( $title, $next );
+               }
+               return null;
+       }
+
+       /**
+        * Get previous revision Id for this page_id
+        * This is used to populate rev_parent_id on save
+        *
+        * MCR migration note: this corresponds to Revision::getPreviousRevisionId
+        *
+        * @param IDatabase $db
+        * @param RevisionRecord $rev
+        *
+        * @return int
+        */
+       private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
+               $this->checkDatabaseWikiId( $db );
+
+               if ( $rev->getPageId() === null ) {
+                       return 0;
+               }
+               # Use page_latest if ID is not given
+               if ( !$rev->getId() ) {
+                       $prevId = $db->selectField(
+                               'page', 'page_latest',
+                               [ 'page_id' => $rev->getPageId() ],
+                               __METHOD__
+                       );
+               } else {
+                       $prevId = $db->selectField(
+                               'revision', 'rev_id',
+                               [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
+                               __METHOD__,
+                               [ 'ORDER BY' => 'rev_id DESC' ]
+                       );
+               }
+               return intval( $prevId );
+       }
+
+       /**
+        * Get rev_timestamp from rev_id, without loading the rest of the row
+        *
+        * MCR migration note: this replaces Revision::getTimestampFromId
+        *
+        * @param Title $title
+        * @param int $id
+        * @param int $flags
+        * @return string|bool False if not found
+        */
+       public function getTimestampFromId( $title, $id, $flags = 0 ) {
+               $db = $this->getDBConnectionRefForQueryFlags( $flags );
+
+               $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;
+       }
+
+       /**
+        * Get count of revisions per page...not very efficient
+        *
+        * MCR migration note: this replaces Revision::countByPageId
+        *
+        * @param IDatabase $db
+        * @param int $id Page id
+        * @return int
+        */
+       public function countRevisionsByPageId( IDatabase $db, $id ) {
+               $this->checkDatabaseWikiId( $db );
+
+               $row = $db->selectRow( 'revision',
+                       [ 'revCount' => 'COUNT(*)' ],
+                       [ 'rev_page' => $id ],
+                       __METHOD__
+               );
+               if ( $row ) {
+                       return intval( $row->revCount );
+               }
+               return 0;
+       }
+
+       /**
+        * Get count of revisions per page...not very efficient
+        *
+        * MCR migration note: this replaces Revision::countByTitle
+        *
+        * @param IDatabase $db
+        * @param Title $title
+        * @return int
+        */
+       public function countRevisionsByTitle( IDatabase $db, $title ) {
+               $id = $title->getArticleID();
+               if ( $id ) {
+                       return $this->countRevisionsByPageId( $db, $id );
+               }
+               return 0;
+       }
+
+       /**
+        * Check if no edits were made by other users since
+        * the time a user started editing the page. Limit to
+        * 50 revisions for the sake of performance.
+        *
+        * MCR migration note: this replaces Revision::userWasLastToEdit
+        *
+        * @deprecated since 1.31; Can possibly be removed, since the self-conflict suppression
+        *       logic in EditPage that uses this seems conceptually dubious. Revision::userWasLastToEdit
+        *       has been deprecated since 1.24.
+        *
+        * @param IDatabase $db The Database to perform the check on.
+        * @param int $pageId The ID of the page in question
+        * @param int $userId The ID of the user in question
+        * @param string $since Look at edits since this time
+        *
+        * @return bool True if the given user was the only one to edit since the given timestamp
+        */
+       public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
+               $this->checkDatabaseWikiId( $db );
+
+               if ( !$userId ) {
+                       return false;
+               }
+
+               $revQuery = $this->getQueryInfo();
+               $res = $db->select(
+                       $revQuery['tables'],
+                       [
+                               'rev_user' => $revQuery['fields']['rev_user'],
+                       ],
+                       [
+                               'rev_page' => $pageId,
+                               'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
+                       ],
+                       __METHOD__,
+                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
+                       $revQuery['joins']
+               );
+               foreach ( $res as $row ) {
+                       if ( $row->rev_user != $userId ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * Load a revision based on a known page ID and current revision ID from the DB
+        *
+        * 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.
+        *
+        * MCR migration note: this replaces Revision::newKnownCurrent
+        *
+        * @param Title $title the associated page title
+        * @param int $revId current revision of this page. Defaults to $title->getLatestRevID().
+        *
+        * @return RevisionRecord|bool Returns false if missing
+        */
+       public function getKnownCurrentRevision( Title $title, $revId ) {
+               $db = $this->getDBConnectionRef( DB_REPLICA );
+
+               $pageId = $title->getArticleID();
+
+               if ( !$pageId ) {
+                       return false;
+               }
+
+               if ( !$revId ) {
+                       $revId = $title->getLatestRevID();
+               }
+
+               if ( !$revId ) {
+                       wfWarn(
+                               'No latest revision known for page ' . $title->getPrefixedDBkey()
+                               . ' even though it exists with page ID ' . $pageId
+                       );
+                       return false;
+               }
+
+               $row = $this->cache->getWithSetCallback(
+                       // Page/rev IDs passed in from DB to reflect history merges
+                       $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
+                       WANObjectCache::TTL_WEEK,
+                       function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
+                               $setOpts += Database::getCacheSetOptions( $db );
+
+                               $conds = [
+                                       'rev_page' => intval( $pageId ),
+                                       'page_id' => intval( $pageId ),
+                                       'rev_id' => intval( $revId ),
+                               ];
+
+                               $row = $this->fetchRevisionRowFromConds( $db, $conds );
+                               return $row ?: false; // don't cache negatives
+                       }
+               );
+
+               // Reflect revision deletion and user renames
+               if ( $row ) {
+                       return $this->newRevisionFromRow( $row, 0, $title );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] )
+        * Caching rows without 'page' or 'user' could lead to issues.
+        * If the format of the rows returned by the query provided by getQueryInfo changes the
+        * cache key should be updated to avoid conflicts.
+        *
+        * @param IDatabase $db
+        * @param int $pageId
+        * @param int $revId
+        * @return string
+        */
+       private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
+               return $this->cache->makeGlobalKey(
+                       self::ROW_CACHE_KEY,
+                       $db->getDomainID(),
+                       $pageId,
+                       $revId
+               );
+       }
+
+       // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
diff --git a/includes/Revision/RevisionStoreFactory.php b/includes/Revision/RevisionStoreFactory.php
new file mode 100644 (file)
index 0000000..30ffc99
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Attribution notice: when this file was created, much of its content was taken
+ * from the Revision.php file as present in release 1.30. Refer to the history
+ * of that file for original authorship.
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use ActorMigration;
+use CommentStore;
+use MediaWiki\Logger\Spi as LoggerSpi;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\NameTableStoreFactory;
+use WANObjectCache;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\ILBFactory;
+
+/**
+ * Factory service for RevisionStore instances. This allows RevisionStores to be created for
+ * cross-wiki access.
+ *
+ * @warning Beware compatibility issues with schema migration in the context of cross-wiki access!
+ * This class assumes that all wikis are at compatible migration stages for all relevant schemas.
+ * Relevant schemas are: revision storage (MCR), the revision comment table, and the actor table.
+ * Migration stages are compatible as long as a) there are no wikis in the cluster that only write
+ * the old schema or b) there are no wikis that read only the new schema.
+ *
+ * @since 1.32
+ */
+class RevisionStoreFactory {
+
+       /** @var BlobStoreFactory */
+       private $blobStoreFactory;
+       /** @var ILBFactory */
+       private $dbLoadBalancerFactory;
+       /** @var WANObjectCache */
+       private $cache;
+       /** @var LoggerSpi */
+       private $loggerProvider;
+
+       /** @var CommentStore */
+       private $commentStore;
+       /** @var ActorMigration */
+       private $actorMigration;
+       /** @var int One of the MIGRATION_* constants */
+       private $mcrMigrationStage;
+       /**
+        * @var bool
+        * @see $wgContentHandlerUseDB
+        */
+       private $contentHandlerUseDB;
+
+       /** @var NameTableStoreFactory */
+       private $nameTables;
+
+       /**
+        * @param ILBFactory $dbLoadBalancerFactory
+        * @param BlobStoreFactory $blobStoreFactory
+        * @param NameTableStoreFactory $nameTables
+        * @param WANObjectCache $cache
+        * @param CommentStore $commentStore
+        * @param ActorMigration $actorMigration
+        * @param int $migrationStage
+        * @param LoggerSpi $loggerProvider
+        * @param bool $contentHandlerUseDB see {@link $wgContentHandlerUseDB}. Must be the same
+        *        for all wikis in the cluster. Will go away after MCR migration.
+        */
+       public function __construct(
+               ILBFactory $dbLoadBalancerFactory,
+               BlobStoreFactory $blobStoreFactory,
+               NameTableStoreFactory $nameTables,
+               WANObjectCache $cache,
+               CommentStore $commentStore,
+               ActorMigration $actorMigration,
+               $migrationStage,
+               LoggerSpi $loggerProvider,
+               $contentHandlerUseDB
+       ) {
+               Assert::parameterType( 'integer', $migrationStage, '$migrationStage' );
+               $this->dbLoadBalancerFactory = $dbLoadBalancerFactory;
+               $this->blobStoreFactory = $blobStoreFactory;
+               $this->nameTables = $nameTables;
+               $this->cache = $cache;
+               $this->commentStore = $commentStore;
+               $this->actorMigration = $actorMigration;
+               $this->mcrMigrationStage = $migrationStage;
+               $this->loggerProvider = $loggerProvider;
+               $this->contentHandlerUseDB = $contentHandlerUseDB;
+       }
+
+       /**
+        * @since 1.32
+        *
+        * @param bool|string $wikiId false for the current domain / wikid
+        *
+        * @return RevisionStore for the given wikiId with all necessary services and a logger
+        */
+       public function getRevisionStore( $wikiId = false ) {
+               Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+
+               $store = new RevisionStore(
+                       $this->dbLoadBalancerFactory->getMainLB( $wikiId ),
+                       $this->blobStoreFactory->newSqlBlobStore( $wikiId ),
+                       $this->cache, // Pass local cache instance; Leave cache sharing to RevisionStore.
+                       $this->commentStore,
+                       $this->nameTables->getContentModels( $wikiId ),
+                       $this->nameTables->getSlotRoles( $wikiId ),
+                       $this->mcrMigrationStage,
+                       $this->actorMigration,
+                       $wikiId
+               );
+
+               $store->setLogger( $this->loggerProvider->getLogger( 'RevisionStore' ) );
+               $store->setContentHandlerUseDB( $this->contentHandlerUseDB );
+
+               return $store;
+       }
+}
diff --git a/includes/Revision/RevisionStoreRecord.php b/includes/Revision/RevisionStoreRecord.php
new file mode 100644 (file)
index 0000000..955cc82
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+/**
+ * A RevisionRecord representing an existing revision persisted in the revision table.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use CommentStoreComment;
+use InvalidArgumentException;
+use MediaWiki\User\UserIdentity;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * A RevisionRecord representing an existing revision persisted in the revision table.
+ * RevisionStoreRecord has no optional fields, getters will never return null.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionStoreRecord
+ */
+class RevisionStoreRecord extends RevisionRecord {
+
+       /** @var bool */
+       protected $mCurrent = false;
+
+       /**
+        * @note Avoid calling this constructor directly. Use the appropriate methods
+        * in RevisionStore instead.
+        *
+        * @param Title $title The title of the page this Revision is associated with.
+        * @param UserIdentity $user
+        * @param CommentStoreComment $comment
+        * @param object $row A row from the revision table. Use RevisionStore::getQueryInfo() to build
+        *        a query that yields the required fields.
+        * @param RevisionSlots $slots The slots of this revision.
+        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+        *        or false for the local site.
+        */
+       function __construct(
+               Title $title,
+               UserIdentity $user,
+               CommentStoreComment $comment,
+               $row,
+               RevisionSlots $slots,
+               $wikiId = false
+       ) {
+               parent::__construct( $title, $slots, $wikiId );
+               Assert::parameterType( 'object', $row, '$row' );
+
+               $this->mId = intval( $row->rev_id );
+               $this->mPageId = intval( $row->rev_page );
+               $this->mComment = $comment;
+
+               $timestamp = wfTimestamp( TS_MW, $row->rev_timestamp );
+               Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
+
+               $this->mUser = $user;
+               $this->mMinorEdit = boolval( $row->rev_minor_edit );
+               $this->mTimestamp = $timestamp;
+               $this->mDeleted = intval( $row->rev_deleted );
+
+               // NOTE: rev_parent_id = 0 indicates that there is no parent revision, while null
+               // indicates that the parent revision is unknown. As per MW 1.31, the database schema
+               // allows rev_parent_id to be NULL.
+               $this->mParentId = isset( $row->rev_parent_id ) ? intval( $row->rev_parent_id ) : null;
+               $this->mSize = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
+               $this->mSha1 = !empty( $row->rev_sha1 ) ? $row->rev_sha1 : null;
+
+               // NOTE: we must not call $this->mTitle->getLatestRevID() here, since the state of
+               // page_latest may be in limbo during revision creation. In that case, calling
+               // $this->mTitle->getLatestRevID() would cause a bad value to be cached in the Title
+               // object. During page creation, that bad value would be 0.
+               if ( isset( $row->page_latest ) ) {
+                       $this->mCurrent = ( $row->rev_id == $row->page_latest );
+               }
+
+               // sanity check
+               if (
+                       $this->mPageId && $this->mTitle->exists()
+                       && $this->mPageId !== $this->mTitle->getArticleID()
+               ) {
+                       throw new InvalidArgumentException(
+                               'The given Title does not belong to page ID ' . $this->mPageId .
+                               ' but actually belongs to ' . $this->mTitle->getArticleID()
+                       );
+               }
+       }
+
+       /**
+        * MCR migration note: this replaces Revision::isCurrent
+        *
+        * @return bool
+        */
+       public function isCurrent() {
+               return $this->mCurrent;
+       }
+
+       /**
+        * MCR migration note: this replaces Revision::isDeleted
+        *
+        * @param int $field One of DELETED_* bitfield constants
+        *
+        * @return bool
+        */
+       public function 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 parent::isDeleted( $field );
+       }
+
+       protected function userCan( $field, User $user ) {
+               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 true; // no need to check
+               }
+
+               return parent::userCan( $field, $user );
+       }
+
+       /**
+        * @return int The revision id, never null.
+        */
+       public function getId() {
+               // overwritten just to add a guarantee to the contract
+               return parent::getId();
+       }
+
+       /**
+        * @throws RevisionAccessException if the size was unknown and could not be calculated.
+        * @return string The nominal revision size, never null. May be computed on the fly.
+        */
+       public function getSize() {
+               // If length is null, calculate and remember it (potentially SLOW!).
+               // This is for compatibility with old database rows that don't have the field set.
+               if ( $this->mSize === null ) {
+                       $this->mSize = $this->mSlots->computeSize();
+               }
+
+               return $this->mSize;
+       }
+
+       /**
+        * @throws RevisionAccessException if the hash was unknown and could not be calculated.
+        * @return string The revision hash, never null. May be computed on the fly.
+        */
+       public function getSha1() {
+               // If hash is null, calculate it and remember (potentially SLOW!)
+               // This is for compatibility with old database rows that don't have the field set.
+               if ( $this->mSha1 === null ) {
+                       $this->mSha1 = $this->mSlots->computeSha1();
+               }
+
+               return $this->mSha1;
+       }
+
+       /**
+        * @param int $audience
+        * @param User|null $user
+        *
+        * @return UserIdentity The identity of the revision author, null if access is forbidden.
+        */
+       public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+               // overwritten just to add a guarantee to the contract
+               return parent::getUser( $audience, $user );
+       }
+
+       /**
+        * @param int $audience
+        * @param User|null $user
+        *
+        * @return CommentStoreComment The revision comment, null if access is forbidden.
+        */
+       public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+               // overwritten just to add a guarantee to the contract
+               return parent::getComment( $audience, $user );
+       }
+
+       /**
+        * @return string timestamp, never null
+        */
+       public function getTimestamp() {
+               // overwritten just to add a guarantee to the contract
+               return parent::getTimestamp();
+       }
+
+       /**
+        * @see RevisionStore::isComplete
+        *
+        * @return bool always true.
+        */
+       public function isReadyForInsertion() {
+               return true;
+       }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionStoreRecord::class, 'MediaWiki\Storage\RevisionStoreRecord' );
diff --git a/includes/Revision/SlotRecord.php b/includes/Revision/SlotRecord.php
new file mode 100644 (file)
index 0000000..89980f4
--- /dev/null
@@ -0,0 +1,665 @@
+<?php
+/**
+ * Value object representing a content slot associated with a page revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use Content;
+use InvalidArgumentException;
+use LogicException;
+use OutOfBoundsException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Value object representing a content slot associated with a page revision.
+ * SlotRecord provides direct access to a Content object.
+ * That access may be implemented through a callback.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\SlotRecord
+ */
+class SlotRecord {
+
+       const MAIN = 'main';
+
+       /**
+        * @var object database result row, as a raw object. Callbacks are supported for field values,
+        *      to enable on-demand emulation of these values. This is primarily intended for use
+        *      during schema migration.
+        */
+       private $row;
+
+       /**
+        * @var Content|callable
+        */
+       private $content;
+
+       /**
+        * Returns a new SlotRecord just like the given $slot, except that calling getContent()
+        * will fail with an exception.
+        *
+        * @param SlotRecord $slot
+        *
+        * @return SlotRecord
+        */
+       public static function newWithSuppressedContent( SlotRecord $slot ) {
+               $row = $slot->row;
+
+               return new SlotRecord( $row, function () {
+                       throw new SuppressedDataException( 'Content suppressed!' );
+               } );
+       }
+
+       /**
+        * Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
+        * The slot's content cannot be overwritten.
+        *
+        * @param SlotRecord $slot
+        * @param array $overrides
+        *
+        * @return SlotRecord
+        */
+       private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
+               $row = clone $slot->row;
+               $row->slot_id = null; // never copy the row ID!
+
+               foreach ( $overrides as $key => $value ) {
+                       $row->$key = $value;
+               }
+
+               return new SlotRecord( $row, $slot->content );
+       }
+
+       /**
+        * Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord
+        * of a previous revision.
+        *
+        * Note that a SlotRecord constructed this way are intended as prototypes,
+        * to be used wit newSaved(). They are incomplete, so some getters such as
+        * getRevision() will fail.
+        *
+        * @param SlotRecord $slot
+        *
+        * @return SlotRecord
+        */
+       public static function newInherited( SlotRecord $slot ) {
+               // Sanity check - we can't inherit from a Slot that's not attached to a revision.
+               $slot->getRevision();
+               $slot->getOrigin();
+               $slot->getAddress();
+
+               // NOTE: slot_origin and content_address are copied from $slot.
+               return self::newDerived( $slot, [
+                       'slot_revision_id' => null,
+               ] );
+       }
+
+       /**
+        * Constructs a new Slot from a Content object for a new revision.
+        * This is the preferred way to construct a slot for storing Content that
+        * resulted from a user edit. The slot is assumed to be not inherited.
+        *
+        * Note that a SlotRecord constructed this way are intended as prototypes,
+        * to be used wit newSaved(). They are incomplete, so some getters such as
+        * getAddress() will fail.
+        *
+        * @param string $role
+        * @param Content $content
+        *
+        * @return SlotRecord An incomplete proto-slot object, to be used with newSaved() later.
+        */
+       public static function newUnsaved( $role, Content $content ) {
+               Assert::parameterType( 'string', $role, '$role' );
+
+               $row = [
+                       'slot_id' => null, // not yet known
+                       'slot_revision_id' => null, // not yet known
+                       'slot_origin' => null, // not yet known, will be set in newSaved()
+                       'content_size' => null, // compute later
+                       'content_sha1' => null, // compute later
+                       'slot_content_id' => null, // not yet known, will be set in newSaved()
+                       'content_address' => null, // not yet known, will be set in newSaved()
+                       'role_name' => $role,
+                       'model_name' => $content->getModel(),
+               ];
+
+               return new SlotRecord( (object)$row, $content );
+       }
+
+       /**
+        * Constructs a complete SlotRecord for a newly saved revision, based on the incomplete
+        * proto-slot. This adds information that has only become available during saving,
+        * particularly the revision ID, content ID and content address.
+        *
+        * @param int $revisionId the revision the slot is to be associated with (field slot_revision_id).
+        *        If $protoSlot already has a revision, it must be the same.
+        * @param int|null $contentId the ID of the row in the content table describing the content
+        *        referenced by $contentAddress (field slot_content_id).
+        *        If $protoSlot already has a content ID, it must be the same.
+        * @param string $contentAddress the slot's content address (field content_address).
+        *        If $protoSlot already has an address, it must be the same.
+        * @param SlotRecord $protoSlot The proto-slot that was provided as input for creating a new
+        *        revision. $protoSlot must have a content address if inherited.
+        *
+        * @return SlotRecord If the state of $protoSlot is inappropriate for saving a new revision.
+        */
+       public static function newSaved(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               Assert::parameterType( 'integer', $revisionId, '$revisionId' );
+               // TODO once migration is over $contentId must be an integer
+               Assert::parameterType( 'integer|null', $contentId, '$contentId' );
+               Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
+
+               if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
+                       throw new LogicException(
+                               "Mismatching revision ID $revisionId: "
+                               . "The slot already belongs to revision {$protoSlot->getRevision()}. "
+                               . "Use SlotRecord::newInherited() to re-use content between revisions."
+                       );
+               }
+
+               if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
+                       throw new LogicException(
+                               "Mismatching blob address $contentAddress: "
+                               . "The slot already has content at {$protoSlot->getAddress()}."
+                       );
+               }
+
+               if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
+                       throw new LogicException(
+                               "Mismatching content ID $contentId: "
+                               . "The slot already has content row {$protoSlot->getContentId()} associated."
+                       );
+               }
+
+               if ( $protoSlot->isInherited() ) {
+                       if ( !$protoSlot->hasAddress() ) {
+                               throw new InvalidArgumentException(
+                                       "An inherited blob should have a content address!"
+                               );
+                       }
+                       if ( !$protoSlot->hasField( 'slot_origin' ) ) {
+                               throw new InvalidArgumentException(
+                                       "A saved inherited slot should have an origin set!"
+                               );
+                       }
+                       $origin = $protoSlot->getOrigin();
+               } else {
+                       $origin = $revisionId;
+               }
+
+               return self::newDerived( $protoSlot, [
+                       'slot_revision_id' => $revisionId,
+                       'slot_content_id' => $contentId,
+                       'slot_origin' => $origin,
+                       'content_address' => $contentAddress,
+               ] );
+       }
+
+       /**
+        * SlotRecord constructor.
+        *
+        * The following fields are supported by the $row parameter:
+        *
+        *   $row->blob_data
+        *   $row->blob_address
+        *
+        * @param object $row A database row composed of fields of the slot and content tables,
+        *        as a raw object. Any field value can be a callback that produces the field value
+        *        given this SlotRecord as a parameter. However, plain strings cannot be used as
+        *        callbacks here, for security reasons.
+        * @param Content|callable $content The content object associated with the slot, or a
+        *        callback that will return that Content object, given this SlotRecord as a parameter.
+        */
+       public function __construct( $row, $content ) {
+               Assert::parameterType( 'object', $row, '$row' );
+               Assert::parameterType( 'Content|callable', $content, '$content' );
+
+               Assert::parameter(
+                       property_exists( $row, 'slot_revision_id' ),
+                       '$row->slot_revision_id',
+                       'must exist'
+               );
+               Assert::parameter(
+                       property_exists( $row, 'slot_content_id' ),
+                       '$row->slot_content_id',
+                       'must exist'
+               );
+               Assert::parameter(
+                       property_exists( $row, 'content_address' ),
+                       '$row->content_address',
+                       'must exist'
+               );
+               Assert::parameter(
+                       property_exists( $row, 'model_name' ),
+                       '$row->model_name',
+                       'must exist'
+               );
+               Assert::parameter(
+                       property_exists( $row, 'slot_origin' ),
+                       '$row->slot_origin',
+                       'must exist'
+               );
+               Assert::parameter(
+                       !property_exists( $row, 'slot_inherited' ),
+                       '$row->slot_inherited',
+                       'must not exist'
+               );
+               Assert::parameter(
+                       !property_exists( $row, 'slot_revision' ),
+                       '$row->slot_revision',
+                       'must not exist'
+               );
+
+               $this->row = $row;
+               $this->content = $content;
+       }
+
+       /**
+        * Implemented to defy serialization.
+        *
+        * @throws LogicException always
+        */
+       public function __sleep() {
+               throw new LogicException( __CLASS__ . ' is not serializable.' );
+       }
+
+       /**
+        * Returns the Content of the given slot.
+        *
+        * @note This is free to load Content from whatever subsystem is necessary,
+        * performing potentially expensive operations and triggering I/O-related
+        * failure modes.
+        *
+        * @note This method does not apply audience filtering.
+        *
+        * @throws SuppressedDataException if access to the content is not allowed according
+        * to the audience check performed by RevisionRecord::getSlot().
+        *
+        * @return Content The slot's content. This is a direct reference to the internal instance,
+        * copy before exposing to application logic!
+        */
+       public function getContent() {
+               if ( $this->content instanceof Content ) {
+                       return $this->content;
+               }
+
+               $obj = call_user_func( $this->content, $this );
+
+               Assert::postcondition(
+                       $obj instanceof Content,
+                       'Slot content callback should return a Content object'
+               );
+
+               $this->content = $obj;
+
+               return $this->content;
+       }
+
+       /**
+        * Returns the string value of a data field from the database row supplied to the constructor.
+        * If the field was set to a callback, that callback is invoked and the result returned.
+        *
+        * @param string $name
+        *
+        * @throws OutOfBoundsException
+        * @throws IncompleteRevisionException
+        * @return mixed Returns the field's value, never null.
+        */
+       private function getField( $name ) {
+               if ( !isset( $this->row->$name ) ) {
+                       // distinguish between unknown and uninitialized fields
+                       if ( property_exists( $this->row, $name ) ) {
+                               throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
+                       } else {
+                               throw new OutOfBoundsException( 'No such field: ' . $name );
+                       }
+               }
+
+               $value = $this->row->$name;
+
+               // NOTE: allow callbacks, but don't trust plain string callables from the database!
+               if ( !is_string( $value ) && is_callable( $value ) ) {
+                       $value = call_user_func( $value, $this );
+                       $this->setField( $name, $value );
+               }
+
+               return $value;
+       }
+
+       /**
+        * Returns the string value of a data field from the database row supplied to the constructor.
+        *
+        * @param string $name
+        *
+        * @throws OutOfBoundsException
+        * @throws IncompleteRevisionException
+        * @return string Returns the string value
+        */
+       private function getStringField( $name ) {
+               return strval( $this->getField( $name ) );
+       }
+
+       /**
+        * Returns the int value of a data field from the database row supplied to the constructor.
+        *
+        * @param string $name
+        *
+        * @throws OutOfBoundsException
+        * @throws IncompleteRevisionException
+        * @return int Returns the int value
+        */
+       private function getIntField( $name ) {
+               return intval( $this->getField( $name ) );
+       }
+
+       /**
+        * @param string $name
+        * @return bool whether this record contains the given field
+        */
+       private function hasField( $name ) {
+               if ( isset( $this->row->$name ) ) {
+                       // if the field is a callback, resolve first, then re-check
+                       if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
+                               $this->getField( $name );
+                       }
+               }
+
+               return isset( $this->row->$name );
+       }
+
+       /**
+        * Returns the ID of the revision this slot is associated with.
+        *
+        * @return int
+        */
+       public function getRevision() {
+               return $this->getIntField( 'slot_revision_id' );
+       }
+
+       /**
+        * Returns the revision ID of the revision that originated the slot's content.
+        *
+        * @return int
+        */
+       public function getOrigin() {
+               return $this->getIntField( 'slot_origin' );
+       }
+
+       /**
+        * Whether this slot was inherited from an older revision.
+        *
+        * If this SlotRecord is already attached to a revision, this returns true
+        * if the slot's revision of origin is the same as the revision it belongs to.
+        *
+        * If this SlotRecord is not yet attached to a revision, this returns true
+        * if the slot already has an address.
+        *
+        * @return bool
+        */
+       public function isInherited() {
+               if ( $this->hasRevision() ) {
+                       return $this->getRevision() !== $this->getOrigin();
+               } else {
+                       return $this->hasAddress();
+               }
+       }
+
+       /**
+        * Whether this slot has an address. Slots will have an address if their
+        * content has been stored. While building a new revision,
+        * SlotRecords will not have an address associated.
+        *
+        * @return bool
+        */
+       public function hasAddress() {
+               return $this->hasField( 'content_address' );
+       }
+
+       /**
+        * Whether this slot has an origin (revision ID that originated the slot's content.
+        *
+        * @since 1.32
+        *
+        * @return bool
+        */
+       public function hasOrigin() {
+               return $this->hasField( 'slot_origin' );
+       }
+
+       /**
+        * Whether this slot has a content ID. Slots will have a content ID if their
+        * content has been stored in the content table. While building a new revision,
+        * SlotRecords will not have an ID associated.
+        *
+        * Also, during schema migration, hasContentId() may return false when encountering an
+        * un-migrated database entry in SCHEMA_COMPAT_WRITE_BOTH mode.
+        * It will however always return true for saved revisions on SCHEMA_COMPAT_READ_NEW mode,
+        * or without SCHEMA_COMPAT_WRITE_NEW mode. In the latter case, an emulated content ID
+        * is used, derived from the revision's text ID.
+        *
+        * Note that hasContentId() returning false while hasRevision() returns true always
+        * indicates an unmigrated row in SCHEMA_COMPAT_WRITE_BOTH mode, as described above.
+        * For an unsaved slot, both these methods would return false.
+        *
+        * @since 1.32
+        *
+        * @return bool
+        */
+       public function hasContentId() {
+               return $this->hasField( 'slot_content_id' );
+       }
+
+       /**
+        * Whether this slot has revision ID associated. Slots will have a revision ID associated
+        * only if they were loaded as part of an existing revision. While building a new revision,
+        * Slotrecords will not have a revision ID associated.
+        *
+        * @return bool
+        */
+       public function hasRevision() {
+               return $this->hasField( 'slot_revision_id' );
+       }
+
+       /**
+        * Returns the role of the slot.
+        *
+        * @return string
+        */
+       public function getRole() {
+               return $this->getStringField( 'role_name' );
+       }
+
+       /**
+        * Returns the address of this slot's content.
+        * This address can be used with BlobStore to load the Content object.
+        *
+        * @return string
+        */
+       public function getAddress() {
+               return $this->getStringField( 'content_address' );
+       }
+
+       /**
+        * Returns the ID of the content meta data row associated with the slot.
+        * This information should be irrelevant to application logic, it is here to allow
+        * the construction of a full row for the revision table.
+        *
+        * Note that this method may return an emulated value during schema migration in
+        * SCHEMA_COMPAT_WRITE_OLD mode. See RevisionStore::emulateContentId for more information.
+        *
+        * @return int
+        */
+       public function getContentId() {
+               return $this->getIntField( 'slot_content_id' );
+       }
+
+       /**
+        * Returns the content size
+        *
+        * @return int size of the content, in bogo-bytes, as reported by Content::getSize.
+        */
+       public function getSize() {
+               try {
+                       $size = $this->getIntField( 'content_size' );
+               } catch ( IncompleteRevisionException $ex ) {
+                       $size = $this->getContent()->getSize();
+                       $this->setField( 'content_size', $size );
+               }
+
+               return $size;
+       }
+
+       /**
+        * Returns the content size
+        *
+        * @return string hash of the content.
+        */
+       public function getSha1() {
+               try {
+                       $sha1 = $this->getStringField( 'content_sha1' );
+               } catch ( IncompleteRevisionException $ex ) {
+                       $format = $this->hasField( 'format_name' )
+                               ? $this->getStringField( 'format_name' )
+                               : null;
+
+                       $data = $this->getContent()->serialize( $format );
+                       $sha1 = self::base36Sha1( $data );
+                       $this->setField( 'content_sha1', $sha1 );
+               }
+
+               return $sha1;
+       }
+
+       /**
+        * Returns the content model. This is the model name that decides
+        * which ContentHandler is appropriate for interpreting the
+        * data of the blob referenced by the address returned by getAddress().
+        *
+        * @return string the content model of the content
+        */
+       public function getModel() {
+               try {
+                       $model = $this->getStringField( 'model_name' );
+               } catch ( IncompleteRevisionException $ex ) {
+                       $model = $this->getContent()->getModel();
+                       $this->setField( 'model_name', $model );
+               }
+
+               return $model;
+       }
+
+       /**
+        * Returns the blob serialization format as a MIME type.
+        *
+        * @note When this method returns null, the caller is expected
+        * to auto-detect the serialization format, or to rely on
+        * the default format associated with the content model.
+        *
+        * @return string|null
+        */
+       public function getFormat() {
+               // XXX: we currently do not plan to store the format for each slot!
+
+               if ( $this->hasField( 'format_name' ) ) {
+                       return $this->getStringField( 'format_name' );
+               }
+
+               return null;
+       }
+
+       /**
+        * @param string $name
+        * @param string|int|null $value
+        */
+       private function setField( $name, $value ) {
+               $this->row->$name = $value;
+       }
+
+       /**
+        * Get the base 36 SHA-1 value for a string of text
+        *
+        * MCR migration note: this replaces Revision::base36Sha1
+        *
+        * @param string $blob
+        * @return string
+        */
+       public static function base36Sha1( $blob ) {
+               return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
+       }
+
+       /**
+        * Returns true if $other has the same content as this slot.
+        * The check is performed based on the model, address size, and hash.
+        * Two slots can have the same content if they use different content addresses,
+        * but if they have the same address and the same model, they have the same content.
+        * Two slots can have the same content if they belong to different
+        * revisions or pages.
+        *
+        * Note that hasSameContent() may return false even if Content::equals returns true for
+        * the content of two slots. This may happen if the two slots have different serializations
+        * representing equivalent Content. Such false negatives are considered acceptable. Code
+        * that has to be absolutely sure the Content is really not the same if hasSameContent()
+        * returns false should call getContent() and compare the Content objects directly.
+        *
+        * @since 1.32
+        *
+        * @param SlotRecord $other
+        * @return bool
+        */
+       public function hasSameContent( SlotRecord $other ) {
+               if ( $other === $this ) {
+                       return true;
+               }
+
+               if ( $this->getModel() !== $other->getModel() ) {
+                       return false;
+               }
+
+               if ( $this->hasAddress()
+                       && $other->hasAddress()
+                       && $this->getAddress() == $other->getAddress()
+               ) {
+                       return true;
+               }
+
+               if ( $this->getSize() !== $other->getSize() ) {
+                       return false;
+               }
+
+               if ( $this->getSha1() !== $other->getSha1() ) {
+                       return false;
+               }
+
+               return true;
+       }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( SlotRecord::class, 'MediaWiki\Storage\SlotRecord' );
index 740f0f2..2c06b31 100644 (file)
@@ -7,7 +7,6 @@
  */
 namespace MediaWiki\Revision;
 
-use MediaWiki\Storage\SuppressedDataException;
 use ParserOutput;
 
 /**
diff --git a/includes/Revision/SuppressedDataException.php b/includes/Revision/SuppressedDataException.php
new file mode 100644 (file)
index 0000000..b7e60d6
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+/**
+ * Exception raised in response to an audience check when attempting to
+ * access suppressed information without permission.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\SuppressedDataException
+ */
+class SuppressedDataException extends RevisionAccessException {
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( SuppressedDataException::class, 'MediaWiki\Storage\SuppressedDataException' );
index dac3de6..c1ac932 100644 (file)
@@ -48,16 +48,16 @@ use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Preferences\PreferencesFactory;
 use MediaWiki\Preferences\DefaultPreferencesFactory;
+use MediaWiki\Revision\RevisionFactory;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRenderer;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreFactory;
 use MediaWiki\Shell\CommandFactory;
 use MediaWiki\Special\SpecialPageFactory;
 use MediaWiki\Storage\BlobStore;
-use MediaWiki\Revision\RevisionRenderer;
 use MediaWiki\Storage\BlobStoreFactory;
 use MediaWiki\Storage\NameTableStoreFactory;
-use MediaWiki\Storage\RevisionFactory;
-use MediaWiki\Storage\RevisionLookup;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreFactory;
 use MediaWiki\Storage\SqlBlobStore;
 
 return [
index 3f3b0cf..e908968 100644 (file)
@@ -38,8 +38,13 @@ use LinksDeletionUpdate;
 use LinksUpdate;
 use LogicException;
 use MediaWiki\Edit\PreparedEdit;
+use MediaWiki\Revision\MutableRevisionRecord;
 use MediaWiki\Revision\RenderedRevision;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Revision\RevisionRenderer;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\User\UserIdentity;
 use MessageCache;
 use ParserCache;
diff --git a/includes/Storage/IncompleteRevisionException.php b/includes/Storage/IncompleteRevisionException.php
deleted file mode 100644 (file)
index bf45b01..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-/**
- * Exception representing a failure to look up a revision.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-/**
- * Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
- *
- * @since 1.31
- */
-class IncompleteRevisionException extends RevisionAccessException {
-
-}
diff --git a/includes/Storage/MutableRevisionRecord.php b/includes/Storage/MutableRevisionRecord.php
deleted file mode 100644 (file)
index 72d6547..0000000
+++ /dev/null
@@ -1,336 +0,0 @@
-<?php
-/**
- * Mutable RevisionRecord implementation, for building new revision entries programmatically.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use CommentStoreComment;
-use Content;
-use InvalidArgumentException;
-use MediaWiki\User\UserIdentity;
-use MWException;
-use Title;
-use Wikimedia\Assert\Assert;
-
-/**
- * Mutable RevisionRecord implementation, for building new revision entries programmatically.
- * Provides setters for all fields.
- *
- * @since 1.31
- */
-class MutableRevisionRecord extends RevisionRecord {
-
-       /**
-        * Returns an incomplete MutableRevisionRecord which uses $parent as its
-        * parent revision, and inherits all slots form it. If saved unchanged,
-        * the new revision will act as a null-revision.
-        *
-        * @param RevisionRecord $parent
-        *
-        * @return MutableRevisionRecord
-        */
-       public static function newFromParentRevision( RevisionRecord $parent ) {
-               // TODO: ideally, we wouldn't need a Title here
-               $title = Title::newFromLinkTarget( $parent->getPageAsLinkTarget() );
-               $rev = new MutableRevisionRecord( $title, $parent->getWikiId() );
-
-               foreach ( $parent->getSlotRoles() as $role ) {
-                       $slot = $parent->getSlot( $role, self::RAW );
-                       $rev->inheritSlot( $slot );
-               }
-
-               $rev->setPageId( $parent->getPageId() );
-               $rev->setParentId( $parent->getId() );
-
-               return $rev;
-       }
-
-       /**
-        * @note Avoid calling this constructor directly. Use the appropriate methods
-        * in RevisionStore instead.
-        *
-        * @param Title $title The title of the page this Revision is associated with.
-        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
-        *        or false for the local site.
-        *
-        * @throws MWException
-        */
-       function __construct( Title $title, $wikiId = false ) {
-               $slots = new MutableRevisionSlots();
-
-               parent::__construct( $title, $slots, $wikiId );
-
-               $this->mSlots = $slots; // redundant, but nice for static analysis
-       }
-
-       /**
-        * @param int $parentId
-        */
-       public function setParentId( $parentId ) {
-               Assert::parameterType( 'integer', $parentId, '$parentId' );
-
-               $this->mParentId = $parentId;
-       }
-
-       /**
-        * Sets the given slot. If a slot with the same role is already present in the revision,
-        * it is replaced.
-        *
-        * @note This can only be used with a fresh "unattached" SlotRecord. Calling code that has a
-        * SlotRecord from another revision should use inheritSlot(). Calling code that has access to
-        * a Content object can use setContent().
-        *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
-        * @note Calling this method will cause the revision size and hash to be re-calculated upon
-        *       the next call to getSize() and getSha1(), respectively.
-        *
-        * @param SlotRecord $slot
-        */
-       public function setSlot( SlotRecord $slot ) {
-               if ( $slot->hasRevision() && $slot->getRevision() !== $this->getId() ) {
-                       throw new InvalidArgumentException(
-                               'The given slot must be an unsaved, unattached one. '
-                               . 'This slot is already attached to revision ' . $slot->getRevision() . '. '
-                               . 'Use inheritSlot() instead to preserve a slot from a previous revision.'
-                       );
-               }
-
-               $this->mSlots->setSlot( $slot );
-               $this->resetAggregateValues();
-       }
-
-       /**
-        * "Inherits" the given slot's content.
-        *
-        * If a slot with the same role is already present in the revision, it is replaced.
-        *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
-        * @param SlotRecord $parentSlot
-        */
-       public function inheritSlot( SlotRecord $parentSlot ) {
-               $this->mSlots->inheritSlot( $parentSlot );
-               $this->resetAggregateValues();
-       }
-
-       /**
-        * Sets the content for the slot with the given role.
-        *
-        * If a slot with the same role is already present in the revision, it is replaced.
-        * Calling code that has access to a SlotRecord can use inheritSlot() instead.
-        *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
-        * @note Calling this method will cause the revision size and hash to be re-calculated upon
-        *       the next call to getSize() and getSha1(), respectively.
-        *
-        * @param string $role
-        * @param Content $content
-        */
-       public function setContent( $role, Content $content ) {
-               $this->mSlots->setContent( $role, $content );
-               $this->resetAggregateValues();
-       }
-
-       /**
-        * Removes the slot with the given role from this revision.
-        * This effectively ends the "stream" with that role on the revision's page.
-        * Future revisions will no longer inherit this slot, unless it is added back explicitly.
-        *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
-        * @note Calling this method will cause the revision size and hash to be re-calculated upon
-        *       the next call to getSize() and getSha1(), respectively.
-        *
-        * @param string $role
-        */
-       public function removeSlot( $role ) {
-               $this->mSlots->removeSlot( $role );
-               $this->resetAggregateValues();
-       }
-
-       /**
-        * Applies the given update to the slots of this revision.
-        *
-        * @param RevisionSlotsUpdate $update
-        */
-       public function applyUpdate( RevisionSlotsUpdate $update ) {
-               $update->apply( $this->mSlots );
-       }
-
-       /**
-        * @param CommentStoreComment $comment
-        */
-       public function setComment( CommentStoreComment $comment ) {
-               $this->mComment = $comment;
-       }
-
-       /**
-        * Set revision hash, for optimization. Prevents getSha1() from re-calculating the hash.
-        *
-        * @note This should only be used if the calling code is sure that the given hash is correct
-        * for the revision's content, and there is no chance of the content being manipulated
-        * later. When in doubt, this method should not be called.
-        *
-        * @param string $sha1 SHA1 hash as a base36 string.
-        */
-       public function setSha1( $sha1 ) {
-               Assert::parameterType( 'string', $sha1, '$sha1' );
-
-               $this->mSha1 = $sha1;
-       }
-
-       /**
-        * Set nominal revision size, for optimization. Prevents getSize() from re-calculating the size.
-        *
-        * @note This should only be used if the calling code is sure that the given size is correct
-        * for the revision's content, and there is no chance of the content being manipulated
-        * later. When in doubt, this method should not be called.
-        *
-        * @param int $size nominal size in bogo-bytes
-        */
-       public function setSize( $size ) {
-               Assert::parameterType( 'integer', $size, '$size' );
-
-               $this->mSize = $size;
-       }
-
-       /**
-        * @param int $visibility
-        */
-       public function setVisibility( $visibility ) {
-               Assert::parameterType( 'integer', $visibility, '$visibility' );
-
-               $this->mDeleted = $visibility;
-       }
-
-       /**
-        * @param string $timestamp A timestamp understood by wfTimestamp
-        */
-       public function setTimestamp( $timestamp ) {
-               Assert::parameterType( 'string', $timestamp, '$timestamp' );
-
-               $this->mTimestamp = wfTimestamp( TS_MW, $timestamp );
-       }
-
-       /**
-        * @param bool $minorEdit
-        */
-       public function setMinorEdit( $minorEdit ) {
-               Assert::parameterType( 'boolean', $minorEdit, '$minorEdit' );
-
-               $this->mMinorEdit = $minorEdit;
-       }
-
-       /**
-        * Set the revision ID.
-        *
-        * MCR migration note: this replaces Revision::setId()
-        *
-        * @warning Use this with care, especially when preparing a revision for insertion
-        *          into the database! The revision ID should only be fixed in special cases
-        *          like preserving the original ID when restoring a revision.
-        *
-        * @param int $id
-        */
-       public function setId( $id ) {
-               Assert::parameterType( 'integer', $id, '$id' );
-
-               $this->mId = $id;
-       }
-
-       /**
-        * Sets the user identity associated with the revision
-        *
-        * @param UserIdentity $user
-        */
-       public function setUser( UserIdentity $user ) {
-               $this->mUser = $user;
-       }
-
-       /**
-        * @param int $pageId
-        */
-       public function setPageId( $pageId ) {
-               Assert::parameterType( 'integer', $pageId, '$pageId' );
-
-               if ( $this->mTitle->exists() && $pageId !== $this->mTitle->getArticleID() ) {
-                       throw new InvalidArgumentException(
-                               'The given Title does not belong to page ID ' . $this->mPageId
-                       );
-               }
-
-               $this->mPageId = $pageId;
-       }
-
-       /**
-        * Returns the nominal size of this revision.
-        *
-        * MCR migration note: this replaces Revision::getSize
-        *
-        * @return int The nominal size, may be computed on the fly if not yet known.
-        */
-       public function getSize() {
-               // If not known, re-calculate and remember. Will be reset when slots change.
-               if ( $this->mSize === null ) {
-                       $this->mSize = $this->mSlots->computeSize();
-               }
-
-               return $this->mSize;
-       }
-
-       /**
-        * Returns the base36 sha1 of this revision.
-        *
-        * MCR migration note: this replaces Revision::getSha1
-        *
-        * @return string The revision hash, may be computed on the fly if not yet known.
-        */
-       public function getSha1() {
-               // If not known, re-calculate and remember. Will be reset when slots change.
-               if ( $this->mSha1 === null ) {
-                       $this->mSha1 = $this->mSlots->computeSha1();
-               }
-
-               return $this->mSha1;
-       }
-
-       /**
-        * Returns the slots defined for this revision as a MutableRevisionSlots instance,
-        * which can be modified to defined the slots for this revision.
-        *
-        * @return MutableRevisionSlots
-        */
-       public function getSlots() {
-               // Overwritten just guarantee the more narrow return type.
-               return parent::getSlots();
-       }
-
-       /**
-        * Invalidate cached aggregate values such as hash and size.
-        */
-       private function resetAggregateValues() {
-               $this->mSize = null;
-               $this->mSha1 = null;
-       }
-
-}
diff --git a/includes/Storage/MutableRevisionSlots.php b/includes/Storage/MutableRevisionSlots.php
deleted file mode 100644 (file)
index df94964..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-/**
- * Mutable version of RevisionSlots, for constructing a new revision.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use Content;
-
-/**
- * Mutable version of RevisionSlots, for constructing a new revision.
- *
- * @since 1.31
- */
-class MutableRevisionSlots extends RevisionSlots {
-
-       /**
-        * Constructs a MutableRevisionSlots that inherits from the given
-        * list of slots.
-        *
-        * @param SlotRecord[] $slots
-        *
-        * @return MutableRevisionSlots
-        */
-       public static function newFromParentRevisionSlots( array $slots ) {
-               $inherited = [];
-               foreach ( $slots as $slot ) {
-                       $role = $slot->getRole();
-                       $inherited[$role] = SlotRecord::newInherited( $slot );
-               }
-
-               return new MutableRevisionSlots( $inherited );
-       }
-
-       /**
-        * @param SlotRecord[] $slots An array of SlotRecords.
-        */
-       public function __construct( array $slots = [] ) {
-               parent::__construct( $slots );
-       }
-
-       /**
-        * Sets the given slot.
-        * If a slot with the same role is already present, it is replaced.
-        *
-        * @param SlotRecord $slot
-        */
-       public function setSlot( SlotRecord $slot ) {
-               if ( !is_array( $this->slots ) ) {
-                       $this->getSlots(); // initialize $this->slots
-               }
-
-               $role = $slot->getRole();
-               $this->slots[$role] = $slot;
-       }
-
-       /**
-        * Sets the given slot to an inherited version of $slot.
-        * If a slot with the same role is already present, it is replaced.
-        *
-        * @param SlotRecord $slot
-        */
-       public function inheritSlot( SlotRecord $slot ) {
-               $this->setSlot( SlotRecord::newInherited( $slot ) );
-       }
-
-       /**
-        * Sets the content for the slot with the given role.
-        * If a slot with the same role is already present, it is replaced.
-        *
-        * @param string $role
-        * @param Content $content
-        */
-       public function setContent( $role, Content $content ) {
-               $slot = SlotRecord::newUnsaved( $role, $content );
-               $this->setSlot( $slot );
-       }
-
-       /**
-        * Remove the slot for the given role, discontinue the corresponding stream.
-        *
-        * @param string $role
-        */
-       public function removeSlot( $role ) {
-               if ( !is_array( $this->slots ) ) {
-                       $this->getSlots();  // initialize $this->slots
-               }
-
-               unset( $this->slots[$role] );
-       }
-
-}
index 02ea9a7..ec364f9 100644 (file)
@@ -94,9 +94,10 @@ class NameTableStoreFactory {
                if ( !isset( $infos[$tableName] ) ) {
                        throw new \InvalidArgumentException( "Invalid table name \$tableName" );
                }
-               if ( $wiki === wfWikiID() ) {
+               if ( $wiki === $this->lbFactory->getLocalDomainID() ) {
                        $wiki = false;
                }
+
                if ( isset( $this->stores[$tableName][$wiki] ) ) {
                        return $this->stores[$tableName][$wiki];
                }
index 29ce710..043e00e 100644 (file)
@@ -35,6 +35,11 @@ use InvalidArgumentException;
 use LogicException;
 use ManualLogEntry;
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
 use MWException;
 use RecentChange;
 use Revision;
diff --git a/includes/Storage/RevisionAccessException.php b/includes/Storage/RevisionAccessException.php
deleted file mode 100644 (file)
index ee6efc0..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-/**
- * Exception representing a failure to look up a revision.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use RuntimeException;
-
-/**
- * Exception representing a failure to look up a revision.
- *
- * @since 1.31
- */
-class RevisionAccessException extends RuntimeException {
-
-}
diff --git a/includes/Storage/RevisionArchiveRecord.php b/includes/Storage/RevisionArchiveRecord.php
deleted file mode 100644 (file)
index 173da51..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-<?php
-/**
- * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use CommentStoreComment;
-use MediaWiki\User\UserIdentity;
-use Title;
-use User;
-use Wikimedia\Assert\Assert;
-
-/**
- * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
- * Most getters on RevisionArchiveRecord will never return null. However, getId() and
- * getParentId() may indeed return null if this information was not stored when the archive entry
- * was created.
- *
- * @since 1.31
- */
-class RevisionArchiveRecord extends RevisionRecord {
-
-       /**
-        * @var int
-        */
-       protected $mArchiveId;
-
-       /**
-        * @note Avoid calling this constructor directly. Use the appropriate methods
-        * in RevisionStore instead.
-        *
-        * @param Title $title The title of the page this Revision is associated with.
-        * @param UserIdentity $user
-        * @param CommentStoreComment $comment
-        * @param object $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build
-        *        a query that yields the required fields.
-        * @param RevisionSlots $slots The slots of this revision.
-        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
-        *        or false for the local site.
-        */
-       function __construct(
-               Title $title,
-               UserIdentity $user,
-               CommentStoreComment $comment,
-               $row,
-               RevisionSlots $slots,
-               $wikiId = false
-       ) {
-               parent::__construct( $title, $slots, $wikiId );
-               Assert::parameterType( 'object', $row, '$row' );
-
-               $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
-               Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
-
-               $this->mArchiveId = intval( $row->ar_id );
-
-               // NOTE: ar_page_id may be different from $this->mTitle->getArticleID() in some cases,
-               // notably when a partially restored page has been moved, and a new page has been created
-               // with the same title. Archive rows for that title will then have the wrong page id.
-               $this->mPageId = isset( $row->ar_page_id ) ? intval( $row->ar_page_id ) : $title->getArticleID();
-
-               // NOTE: ar_parent_id = 0 indicates that there is no parent revision, while null
-               // indicates that the parent revision is unknown. As per MW 1.31, the database schema
-               // allows ar_parent_id to be NULL.
-               $this->mParentId = isset( $row->ar_parent_id ) ? intval( $row->ar_parent_id ) : null;
-               $this->mId = isset( $row->ar_rev_id ) ? intval( $row->ar_rev_id ) : null;
-               $this->mComment = $comment;
-               $this->mUser = $user;
-               $this->mTimestamp = $timestamp;
-               $this->mMinorEdit = boolval( $row->ar_minor_edit );
-               $this->mDeleted = intval( $row->ar_deleted );
-               $this->mSize = isset( $row->ar_len ) ? intval( $row->ar_len ) : null;
-               $this->mSha1 = !empty( $row->ar_sha1 ) ? $row->ar_sha1 : null;
-       }
-
-       /**
-        * Get archive row ID
-        *
-        * @return int
-        */
-       public function getArchiveId() {
-               return $this->mArchiveId;
-       }
-
-       /**
-        * @return int|null The revision id, or null if the original revision ID
-        *         was not recorded in the archive table.
-        */
-       public function getId() {
-               // overwritten just to refine the contract specification.
-               return parent::getId();
-       }
-
-       /**
-        * @throws RevisionAccessException if the size was unknown and could not be calculated.
-        * @return int The nominal revision size, never null. May be computed on the fly.
-        */
-       public function getSize() {
-               // If length is null, calculate and remember it (potentially SLOW!).
-               // This is for compatibility with old database rows that don't have the field set.
-               if ( $this->mSize === null ) {
-                       $this->mSize = $this->mSlots->computeSize();
-               }
-
-               return $this->mSize;
-       }
-
-       /**
-        * @throws RevisionAccessException if the hash was unknown and could not be calculated.
-        * @return string The revision hash, never null. May be computed on the fly.
-        */
-       public function getSha1() {
-               // If hash is null, calculate it and remember (potentially SLOW!)
-               // This is for compatibility with old database rows that don't have the field set.
-               if ( $this->mSha1 === null ) {
-                       $this->mSha1 = $this->mSlots->computeSha1();
-               }
-
-               return $this->mSha1;
-       }
-
-       /**
-        * @param int $audience
-        * @param User|null $user
-        *
-        * @return UserIdentity The identity of the revision author, null if access is forbidden.
-        */
-       public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
-               // overwritten just to add a guarantee to the contract
-               return parent::getUser( $audience, $user );
-       }
-
-       /**
-        * @param int $audience
-        * @param User|null $user
-        *
-        * @return CommentStoreComment The revision comment, null if access is forbidden.
-        */
-       public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
-               // overwritten just to add a guarantee to the contract
-               return parent::getComment( $audience, $user );
-       }
-
-       /**
-        * @return string never null
-        */
-       public function getTimestamp() {
-               // overwritten just to add a guarantee to the contract
-               return parent::getTimestamp();
-       }
-
-       /**
-        * @see RevisionStore::isComplete
-        *
-        * @return bool always true.
-        */
-       public function isReadyForInsertion() {
-               return true;
-       }
-
-}
diff --git a/includes/Storage/RevisionFactory.php b/includes/Storage/RevisionFactory.php
deleted file mode 100644 (file)
index 2c45468..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-/**
- * Service for constructing revision objects.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use MWException;
-use Title;
-
-/**
- * Service for constructing revision objects.
- *
- * @since 1.31
- *
- * @note This was written to act as a drop-in replacement for the corresponding
- *       static methods in Revision.
- */
-interface RevisionFactory {
-
-       /**
-        * Constructs a new RevisionRecord based on the given associative array following the MW1.29
-        * database convention for the Revision constructor.
-        *
-        * MCR migration note: this replaces Revision::newFromRow
-        *
-        * @deprecated since 1.31. Use a MutableRevisionRecord instead.
-        *
-        * @param array $fields
-        * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
-        * @param Title|null $title
-        *
-        * @return MutableRevisionRecord
-        * @throws MWException
-        */
-       public function newMutableRevisionFromArray( array $fields, $queryFlags = 0, Title $title = null );
-
-       /**
-        * Constructs a RevisionRecord given a database row and content slots.
-        *
-        * MCR migration note: this replaces Revision::newFromRow for rows based on the
-        * revision, slot, and content tables defined for MCR since MW1.31.
-        *
-        * @param object $row A query result row as a raw object.
-        *        Use RevisionStore::getQueryInfo() to build a query that yields the required fields.
-        * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
-        * @param Title|null $title
-        *
-        * @return RevisionRecord
-        */
-       public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null );
-
-       /**
-        * Make a fake revision object from an archive table row. This is queried
-        * for permissions or even inserted (as in Special:Undelete)
-        *
-        * MCR migration note: this replaces Revision::newFromArchiveRow
-        *
-        * @param object $row A query result row as a raw object.
-        *        Use RevisionStore::getArchiveQueryInfo() to build a query that yields the
-        *        required fields.
-        * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
-        * @param Title|null $title
-        * @param array $overrides An associative array that allows fields in $row to be overwritten.
-        *        Keys in this array correspond to field names in $row without the "ar_" prefix, so
-        *        $overrides['user'] will override $row->ar_user, etc.
-        *
-        * @return RevisionRecord
-        */
-       public function newRevisionFromArchiveRow(
-               $row,
-               $queryFlags = 0,
-               Title $title = null,
-               array $overrides = []
-       );
-
-}
diff --git a/includes/Storage/RevisionLookup.php b/includes/Storage/RevisionLookup.php
deleted file mode 100644 (file)
index a6e2930..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-/**
- *  Service for looking up page revisions.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use IDBAccessObject;
-use MediaWiki\Linker\LinkTarget;
-use Title;
-
-/**
- * Service for looking up page revisions.
- *
- * @note This was written to act as a drop-in replacement for the corresponding
- *       static methods in Revision.
- *
- * @since 1.31
- */
-interface RevisionLookup extends IDBAccessObject {
-
-       /**
-        * Load a page revision from a given revision ID number.
-        * Returns null if no such revision can be found.
-        *
-        * MCR migration note: this replaces Revision::newFromId
-        *
-        * $flags include:
-        *
-        * @param int $id
-        * @param int $flags bit field, see IDBAccessObject::READ_XXX
-        * @return RevisionRecord|null
-        */
-       public function getRevisionById( $id, $flags = 0 );
-
-       /**
-        * Load either the current, or a specified, revision
-        * that's attached to a given link target. If not attached
-        * to that link target, will return null.
-        *
-        * MCR migration note: this replaces Revision::newFromTitle
-        *
-        * @param LinkTarget $linkTarget
-        * @param int $revId (optional)
-        * @param int $flags bit field, see IDBAccessObject::READ_XXX
-        * @return RevisionRecord|null
-        */
-       public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 );
-
-       /**
-        * Load either the current, or a specified, revision
-        * that's attached to a given page ID.
-        * Returns null if no such revision can be found.
-        *
-        * MCR migration note: this replaces Revision::newFromPageId
-        *
-        * @param int $pageId
-        * @param int $revId (optional)
-        * @param int $flags bit field, see IDBAccessObject::READ_XXX
-        * @return RevisionRecord|null
-        */
-       public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 );
-
-       /**
-        * Get previous revision for this title
-        *
-        * MCR migration note: this replaces Revision::getPrevious
-        *
-        * @param RevisionRecord $rev
-        * @param Title|null $title if known (optional)
-        *
-        * @return RevisionRecord|null
-        */
-       public function getPreviousRevision( RevisionRecord $rev, Title $title = null );
-
-       /**
-        * Get next revision for this title
-        *
-        * MCR migration note: this replaces Revision::getNext
-        *
-        * @param RevisionRecord $rev
-        * @param Title|null $title if known (optional)
-        *
-        * @return RevisionRecord|null
-        */
-       public function getNextRevision( RevisionRecord $rev, Title $title = null );
-
-       /**
-        * Load a revision based on a known page ID and current revision ID from the DB
-        *
-        * 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.
-        *
-        * MCR migration note: this replaces Revision::newKnownCurrent
-        *
-        * @param Title $title the associated page title
-        * @param int $revId current revision of this page
-        *
-        * @return RevisionRecord|bool Returns false if missing
-        */
-       public function getKnownCurrentRevision( Title $title, $revId );
-
-}
diff --git a/includes/Storage/RevisionRecord.php b/includes/Storage/RevisionRecord.php
deleted file mode 100644 (file)
index 8c31a3c..0000000
+++ /dev/null
@@ -1,560 +0,0 @@
-<?php
-/**
- * Page revision base class.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use CommentStoreComment;
-use Content;
-use InvalidArgumentException;
-use LogicException;
-use MediaWiki\Linker\LinkTarget;
-use MediaWiki\User\UserIdentity;
-use MWException;
-use Title;
-use User;
-use Wikimedia\Assert\Assert;
-
-/**
- * Page revision base class.
- *
- * RevisionRecords are considered value objects, but they may use callbacks for lazy loading.
- * Note that while the base class has no setters, subclasses may offer a mutable interface.
- *
- * @since 1.31
- */
-abstract class RevisionRecord {
-
-       // RevisionRecord deletion constants
-       const DELETED_TEXT = 1;
-       const DELETED_COMMENT = 2;
-       const DELETED_USER = 4;
-       const DELETED_RESTRICTED = 8;
-       const SUPPRESSED_USER = self::DELETED_USER | self::DELETED_RESTRICTED; // convenience
-       const SUPPRESSED_ALL = self::DELETED_TEXT | self::DELETED_COMMENT | self::DELETED_USER |
-               self::DELETED_RESTRICTED; // convenience
-
-       // Audience options for accessors
-       const FOR_PUBLIC = 1;
-       const FOR_THIS_USER = 2;
-       const RAW = 3;
-
-       /** @var string Wiki ID; false means the current wiki */
-       protected $mWiki = false;
-       /** @var int|null */
-       protected $mId;
-       /** @var int|null */
-       protected $mPageId;
-       /** @var UserIdentity|null */
-       protected $mUser;
-       /** @var bool */
-       protected $mMinorEdit = false;
-       /** @var string|null */
-       protected $mTimestamp;
-       /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */
-       protected $mDeleted = 0;
-       /** @var int|null */
-       protected $mSize;
-       /** @var string|null */
-       protected $mSha1;
-       /** @var int|null */
-       protected $mParentId;
-       /** @var CommentStoreComment|null */
-       protected $mComment;
-
-       /**  @var Title */
-       protected $mTitle; // TODO: we only need the title for permission checks!
-
-       /** @var RevisionSlots */
-       protected $mSlots;
-
-       /**
-        * @note Avoid calling this constructor directly. Use the appropriate methods
-        * in RevisionStore instead.
-        *
-        * @param Title $title The title of the page this Revision is associated with.
-        * @param RevisionSlots $slots The slots of this revision.
-        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
-        *        or false for the local site.
-        *
-        * @throws MWException
-        */
-       function __construct( Title $title, RevisionSlots $slots, $wikiId = false ) {
-               Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
-
-               $this->mTitle = $title;
-               $this->mSlots = $slots;
-               $this->mWiki = $wikiId;
-
-               // XXX: this is a sensible default, but we may not have a Title object here in the future.
-               $this->mPageId = $title->getArticleID();
-       }
-
-       /**
-        * Implemented to defy serialization.
-        *
-        * @throws LogicException always
-        */
-       public function __sleep() {
-               throw new LogicException( __CLASS__ . ' is not serializable.' );
-       }
-
-       /**
-        * @param RevisionRecord $rec
-        *
-        * @return bool True if this RevisionRecord is known to have same content as $rec.
-        *         False if the content is different (or not known to be the same).
-        */
-       public function hasSameContent( RevisionRecord $rec ) {
-               if ( $rec === $this ) {
-                       return true;
-               }
-
-               if ( $this->getId() !== null && $this->getId() === $rec->getId() ) {
-                       return true;
-               }
-
-               // check size before hash, since size is quicker to compute
-               if ( $this->getSize() !== $rec->getSize() ) {
-                       return false;
-               }
-
-               // instead of checking the hash, we could also check the content addresses of all slots.
-
-               if ( $this->getSha1() === $rec->getSha1() ) {
-                       return true;
-               }
-
-               return false;
-       }
-
-       /**
-        * Returns the Content of the given slot of this revision.
-        * Call getSlotNames() to get a list of available slots.
-        *
-        * Note that for mutable Content objects, each call to this method will return a
-        * fresh clone.
-        *
-        * MCR migration note: this replaces Revision::getContent
-        *
-        * @param string $role The role name of the desired slot
-        * @param int $audience
-        * @param User|null $user
-        *
-        * @throws RevisionAccessException if the slot does not exist or slot data
-        *        could not be lazy-loaded.
-        * @return Content|null The content of the given slot, or null if access is forbidden.
-        */
-       public function getContent( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
-               // XXX: throwing an exception would be nicer, but would a further
-               // departure from the signature of Revision::getContent(), and thus
-               // more complex and error prone refactoring.
-               if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
-                       return null;
-               }
-
-               $content = $this->getSlot( $role, $audience, $user )->getContent();
-               return $content->copy();
-       }
-
-       /**
-        * Returns meta-data for the given slot.
-        *
-        * @param string $role The role name of the desired slot
-        * @param int $audience
-        * @param User|null $user
-        *
-        * @throws RevisionAccessException if the slot does not exist or slot data
-        *        could not be lazy-loaded.
-        * @return SlotRecord The slot meta-data. If access to the slot content is forbidden,
-        *         calling getContent() on the SlotRecord will throw an exception.
-        */
-       public function getSlot( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
-               $slot = $this->mSlots->getSlot( $role );
-
-               if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
-                       return SlotRecord::newWithSuppressedContent( $slot );
-               }
-
-               return $slot;
-       }
-
-       /**
-        * Returns whether the given slot is defined in this revision.
-        *
-        * @param string $role The role name of the desired slot
-        *
-        * @return bool
-        */
-       public function hasSlot( $role ) {
-               return $this->mSlots->hasSlot( $role );
-       }
-
-       /**
-        * Returns the slot names (roles) of all slots present in this revision.
-        * getContent() will succeed only for the names returned by this method.
-        *
-        * @return string[]
-        */
-       public function getSlotRoles() {
-               return $this->mSlots->getSlotRoles();
-       }
-
-       /**
-        * Returns the slots defined for this revision.
-        *
-        * @return RevisionSlots
-        */
-       public function getSlots() {
-               return $this->mSlots;
-       }
-
-       /**
-        * Returns the slots that originate in this revision.
-        *
-        * Note that this does not include any slots inherited from some earlier revision,
-        * even if they are different from the slots in the immediate parent revision.
-        * This is the case for rollbacks: slots of a rollback revision are inherited from
-        * the rollback target, and are different from the slots in the parent revision,
-        * which was rolled back.
-        *
-        * To find all slots modified by this revision against its immediate parent
-        * revision, use RevisionSlotsUpdate::newFromRevisionSlots().
-        *
-        * @return RevisionSlots
-        */
-       public function getOriginalSlots() {
-               return new RevisionSlots( $this->mSlots->getOriginalSlots() );
-       }
-
-       /**
-        * Returns slots inherited from some previous revision.
-        *
-        * "Inherited" slots are all slots that do not originate in this revision.
-        * Note that these slots may still differ from the one in the parent revision.
-        * This is the case for rollbacks: slots of a rollback revision are inherited from
-        * the rollback target, and are different from the slots in the parent revision,
-        * which was rolled back.
-        *
-        * @return RevisionSlots
-        */
-       public function getInheritedSlots() {
-               return new RevisionSlots( $this->mSlots->getInheritedSlots() );
-       }
-
-       /**
-        * Get revision ID. Depending on the concrete subclass, this may return null if
-        * the revision ID is not known (e.g. because the revision does not yet exist
-        * in the database).
-        *
-        * MCR migration note: this replaces Revision::getId
-        *
-        * @return int|null
-        */
-       public function getId() {
-               return $this->mId;
-       }
-
-       /**
-        * Get parent revision ID (the original previous page revision).
-        * If there is no parent revision, this returns 0.
-        * If the parent revision is undefined or unknown, this returns null.
-        *
-        * @note As of MW 1.31, the database schema allows the parent ID to be
-        * NULL to indicate that it is unknown.
-        *
-        * MCR migration note: this replaces Revision::getParentId
-        *
-        * @return int|null
-        */
-       public function getParentId() {
-               return $this->mParentId;
-       }
-
-       /**
-        * Returns the nominal size of this revision, in bogo-bytes.
-        * May be calculated on the fly if not known, which may in the worst
-        * case may involve loading all content.
-        *
-        * MCR migration note: this replaces Revision::getSize
-        *
-        * @throws RevisionAccessException if the size was unknown and could not be calculated.
-        * @return int
-        */
-       abstract public function getSize();
-
-       /**
-        * Returns the base36 sha1 of this revision. This hash is derived from the
-        * hashes of all slots associated with the revision.
-        * May be calculated on the fly if not known, which may in the worst
-        * case may involve loading all content.
-        *
-        * MCR migration note: this replaces Revision::getSha1
-        *
-        * @throws RevisionAccessException if the hash was unknown and could not be calculated.
-        * @return string
-        */
-       abstract public function getSha1();
-
-       /**
-        * Get the page ID. If the page does not yet exist, the page ID is 0.
-        *
-        * MCR migration note: this replaces Revision::getPage
-        *
-        * @return int
-        */
-       public function getPageId() {
-               return $this->mPageId;
-       }
-
-       /**
-        * Get the ID of the wiki this revision belongs to.
-        *
-        * @return string|false The wiki's logical name, of false to indicate the local wiki.
-        */
-       public function getWikiId() {
-               return $this->mWiki;
-       }
-
-       /**
-        * Returns the title of the page this revision is associated with as a LinkTarget object.
-        *
-        * MCR migration note: this replaces Revision::getTitle
-        *
-        * @return LinkTarget
-        */
-       public function getPageAsLinkTarget() {
-               return $this->mTitle;
-       }
-
-       /**
-        * Fetch revision's author's user identity, if it's available to the specified audience.
-        * If the specified audience does not have access to it, null will be
-        * returned. Depending on the concrete subclass, null may also be returned if the user is
-        * not yet specified.
-        *
-        * MCR migration note: this replaces Revision::getUser
-        *
-        * @param int $audience One of:
-        *   RevisionRecord::FOR_PUBLIC       to be displayed to all users
-        *   RevisionRecord::FOR_THIS_USER    to be displayed to the given user
-        *   RevisionRecord::RAW              get the ID regardless of permissions
-        * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
-        *   to the $audience parameter
-        * @return UserIdentity|null
-        */
-       public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
-               if ( !$this->audienceCan( self::DELETED_USER, $audience, $user ) ) {
-                       return null;
-               } else {
-                       return $this->mUser;
-               }
-       }
-
-       /**
-        * Fetch revision comment, if it's available to the specified audience.
-        * If the specified audience does not have access to the comment,
-        * this will return null. Depending on the concrete subclass, null may also be returned
-        * if the comment is not yet specified.
-        *
-        * MCR migration note: this replaces Revision::getComment
-        *
-        * @param int $audience One of:
-        *   RevisionRecord::FOR_PUBLIC       to be displayed to all users
-        *   RevisionRecord::FOR_THIS_USER    to be displayed to the given user
-        *   RevisionRecord::RAW              get the text regardless of permissions
-        * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
-        *   to the $audience parameter
-        *
-        * @return CommentStoreComment|null
-        */
-       public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
-               if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $user ) ) {
-                       return null;
-               } else {
-                       return $this->mComment;
-               }
-       }
-
-       /**
-        * MCR migration note: this replaces Revision::isMinor
-        *
-        * @return bool
-        */
-       public function isMinor() {
-               return (bool)$this->mMinorEdit;
-       }
-
-       /**
-        * MCR migration note: this replaces Revision::isDeleted
-        *
-        * @param int $field One of DELETED_* bitfield constants
-        *
-        * @return bool
-        */
-       public function isDeleted( $field ) {
-               return ( $this->getVisibility() & $field ) == $field;
-       }
-
-       /**
-        * Get the deletion bitfield of the revision
-        *
-        * MCR migration note: this replaces Revision::getVisibility
-        *
-        * @return int
-        */
-       public function getVisibility() {
-               return (int)$this->mDeleted;
-       }
-
-       /**
-        * MCR migration note: this replaces Revision::getTimestamp.
-        *
-        * May return null if the timestamp was not specified.
-        *
-        * @return string|null
-        */
-       public function getTimestamp() {
-               return $this->mTimestamp;
-       }
-
-       /**
-        * Check that the given audience has access to the given field.
-        *
-        * MCR migration note: this corresponds to Revision::userCan
-        *
-        * @param int $field One of self::DELETED_TEXT,
-        *        self::DELETED_COMMENT,
-        *        self::DELETED_USER
-        * @param int $audience One of:
-        *        RevisionRecord::FOR_PUBLIC       to be displayed to all users
-        *        RevisionRecord::FOR_THIS_USER    to be displayed to the given user
-        *        RevisionRecord::RAW              get the text regardless of permissions
-        * @param User|null $user User object to check. Required if $audience is FOR_THIS_USER,
-        *        ignored otherwise.
-        *
-        * @return bool
-        */
-       public function audienceCan( $field, $audience, User $user = null ) {
-               if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) {
-                       return false;
-               } elseif ( $audience == self::FOR_THIS_USER ) {
-                       if ( !$user ) {
-                               throw new InvalidArgumentException(
-                                       'A User object must be given when checking FOR_THIS_USER audience.'
-                               );
-                       }
-
-                       if ( !$this->userCan( $field, $user ) ) {
-                               return false;
-                       }
-               }
-
-               return true;
-       }
-
-       /**
-        * Determine if the current user is allowed to view a particular
-        * field of this revision, if it's marked as deleted.
-        *
-        * MCR migration note: this corresponds to Revision::userCan
-        *
-        * @param int $field One of self::DELETED_TEXT,
-        *                              self::DELETED_COMMENT,
-        *                              self::DELETED_USER
-        * @param User $user User object to check
-        * @return bool
-        */
-       protected function userCan( $field, User $user ) {
-               // TODO: use callback for permission checks, so we don't need to know a Title object!
-               return self::userCanBitfield( $this->getVisibility(), $field, $user, $this->mTitle );
-       }
-
-       /**
-        * Determine if the current user is allowed to view a particular
-        * field of this revision, if it's marked as deleted. This is used
-        * by various classes to avoid duplication.
-        *
-        * MCR migration note: this replaces Revision::userCanBitfield
-        *
-        * @param int $bitfield Current field
-        * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
-        *                               self::DELETED_COMMENT = File::DELETED_COMMENT,
-        *                               self::DELETED_USER = File::DELETED_USER
-        * @param User $user User object to check
-        * @param Title|null $title A Title object to check for per-page restrictions on,
-        *                          instead of just plain userrights
-        * @return bool
-        */
-       public static function userCanBitfield( $bitfield, $field, User $user, Title $title = null ) {
-               if ( $bitfield & $field ) { // aspect is deleted
-                       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 $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;
-               }
-       }
-
-       /**
-        * Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all
-        * information needed to save it to the database. This should trivially be true for
-        * RevisionRecords loaded from the database.
-        *
-        * Note that this may return true even if getId() or getPage() return null or 0, since these
-        * are generally assigned while the revision is saved to the database, and may not be available
-        * before.
-        *
-        * @return bool
-        */
-       public function isReadyForInsertion() {
-               // NOTE: don't check getSize() and getSha1(), since that may cause the full content to
-               // be loaded in order to calculate the values. Just assume these methods will not return
-               // null if mSlots is not empty.
-
-               // NOTE: getId() and getPageId() may return null before a revision is saved, so don't
-               //check them.
-
-               return $this->getTimestamp() !== null
-                       && $this->getComment( self::RAW ) !== null
-                       && $this->getUser( self::RAW ) !== null
-                       && $this->mSlots->getSlotRoles() !== [];
-       }
-
-}
diff --git a/includes/Storage/RevisionSlots.php b/includes/Storage/RevisionSlots.php
deleted file mode 100644 (file)
index 91969fc..0000000
+++ /dev/null
@@ -1,312 +0,0 @@
-<?php
-/**
- * Value object representing the set of slots belonging to a revision.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use Content;
-use LogicException;
-use Wikimedia\Assert\Assert;
-
-/**
- * Value object representing the set of slots belonging to a revision.
- *
- * @since 1.31
- */
-class RevisionSlots {
-
-       /** @var SlotRecord[]|callable */
-       protected $slots;
-
-       /**
-        * @param SlotRecord[]|callable $slots SlotRecords,
-        *        or a callback that returns such a structure.
-        */
-       public function __construct( $slots ) {
-               Assert::parameterType( 'array|callable', $slots, '$slots' );
-
-               if ( is_callable( $slots ) ) {
-                       $this->slots = $slots;
-               } else {
-                       $this->setSlotsInternal( $slots );
-               }
-       }
-
-       /**
-        * @param SlotRecord[] $slots
-        */
-       private function setSlotsInternal( array $slots ) {
-               Assert::parameterElementType( SlotRecord::class, $slots, '$slots' );
-
-               $this->slots = [];
-
-               // re-key the slot array
-               foreach ( $slots as $slot ) {
-                       $role = $slot->getRole();
-                       $this->slots[$role] = $slot;
-               }
-       }
-
-       /**
-        * Implemented to defy serialization.
-        *
-        * @throws LogicException always
-        */
-       public function __sleep() {
-               throw new LogicException( __CLASS__ . ' is not serializable.' );
-       }
-
-       /**
-        * Returns the Content of the given slot.
-        * Call getSlotNames() to get a list of available slots.
-        *
-        * Note that for mutable Content objects, each call to this method will return a
-        * fresh clone.
-        *
-        * @param string $role The role name of the desired slot
-        *
-        * @throws RevisionAccessException if the slot does not exist or slot data
-        *        could not be lazy-loaded.
-        * @return Content
-        */
-       public function getContent( $role ) {
-               // Return a copy to be safe. Immutable content objects return $this from copy().
-               return $this->getSlot( $role )->getContent()->copy();
-       }
-
-       /**
-        * Returns the SlotRecord of the given slot.
-        * Call getSlotNames() to get a list of available slots.
-        *
-        * @param string $role The role name of the desired slot
-        *
-        * @throws RevisionAccessException if the slot does not exist or slot data
-        *        could not be lazy-loaded.
-        * @return SlotRecord
-        */
-       public function getSlot( $role ) {
-               $slots = $this->getSlots();
-
-               if ( isset( $slots[$role] ) ) {
-                       return $slots[$role];
-               } else {
-                       throw new RevisionAccessException( 'No such slot: ' . $role );
-               }
-       }
-
-       /**
-        * Returns whether the given slot is set.
-        *
-        * @param string $role The role name of the desired slot
-        *
-        * @return bool
-        */
-       public function hasSlot( $role ) {
-               $slots = $this->getSlots();
-
-               return isset( $slots[$role] );
-       }
-
-       /**
-        * Returns the slot names (roles) of all slots present in this revision.
-        * getContent() will succeed only for the names returned by this method.
-        *
-        * @return string[]
-        */
-       public function getSlotRoles() {
-               $slots = $this->getSlots();
-               return array_keys( $slots );
-       }
-
-       /**
-        * Computes the total nominal size of the revision's slots, in bogo-bytes.
-        *
-        * @warning This is potentially expensive! It may cause all slot's content to be loaded
-        * and deserialized.
-        *
-        * @return int
-        */
-       public function computeSize() {
-               return array_reduce( $this->getSlots(), function ( $accu, SlotRecord $slot ) {
-                       return $accu + $slot->getSize();
-               }, 0 );
-       }
-
-       /**
-        * Returns an associative array that maps role names to SlotRecords. Each SlotRecord
-        * represents the content meta-data of a slot, together they define the content of
-        * a revision.
-        *
-        * @note This may cause the content meta-data for the revision to be lazy-loaded.
-        *
-        * @return SlotRecord[] revision slot/content rows, keyed by slot role name.
-        */
-       public function getSlots() {
-               if ( is_callable( $this->slots ) ) {
-                       $slots = call_user_func( $this->slots );
-
-                       Assert::postcondition(
-                               is_array( $slots ),
-                               'Slots info callback should return an array of objects'
-                       );
-
-                       $this->setSlotsInternal( $slots );
-               }
-
-               return $this->slots;
-       }
-
-       /**
-        * Computes the combined hash of the revisions's slots.
-        *
-        * @note For backwards compatibility, the combined hash of a single slot
-        * is that slot's hash. For consistency, the combined hash of an empty set of slots
-        * is the hash of the empty string.
-        *
-        * @warning This is potentially expensive! It may cause all slot's content to be loaded
-        * and deserialized, then re-serialized and hashed.
-        *
-        * @return string
-        */
-       public function computeSha1() {
-               $slots = $this->getSlots();
-               ksort( $slots );
-
-               if ( empty( $slots ) ) {
-                       return SlotRecord::base36Sha1( '' );
-               }
-
-               return array_reduce( $slots, function ( $accu, SlotRecord $slot ) {
-                       return $accu === null
-                               ? $slot->getSha1()
-                               : SlotRecord::base36Sha1( $accu . $slot->getSha1() );
-               }, null );
-       }
-
-       /**
-        * Return all slots that belong to the revision they originate from (that is,
-        * they are not inherited from some other revision).
-        *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
-        * @return SlotRecord[]
-        */
-       public function getOriginalSlots() {
-               return array_filter(
-                       $this->getSlots(),
-                       function ( SlotRecord $slot ) {
-                               return !$slot->isInherited();
-                       }
-               );
-       }
-
-       /**
-        * Return all slots that are not not originate in the revision they belong to (that is,
-        * they are inherited from some other revision).
-        *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
-        * @return SlotRecord[]
-        */
-       public function getInheritedSlots() {
-               return array_filter(
-                       $this->getSlots(),
-                       function ( SlotRecord $slot ) {
-                               return $slot->isInherited();
-                       }
-               );
-       }
-
-       /**
-        * Checks whether the other RevisionSlots instance has the same content
-        * as this instance. Note that this does not mean that the slots have to be the same:
-        * they could for instance belong to different revisions.
-        *
-        * @param RevisionSlots $other
-        *
-        * @return bool
-        */
-       public function hasSameContent( RevisionSlots $other ) {
-               if ( $other === $this ) {
-                       return true;
-               }
-
-               $aSlots = $this->getSlots();
-               $bSlots = $other->getSlots();
-
-               ksort( $aSlots );
-               ksort( $bSlots );
-
-               if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) {
-                       return false;
-               }
-
-               foreach ( $aSlots as $role => $s ) {
-                       $t = $bSlots[$role];
-
-                       if ( !$s->hasSameContent( $t ) ) {
-                               return false;
-                       }
-               }
-
-               return true;
-       }
-
-       /**
-        * Find roles for which the $other RevisionSlots object has different content
-        * as this RevisionSlots object, including any roles that are present in one
-        * but not the other.
-        *
-        * @param RevisionSlots $other
-        *
-        * @return string[] a list of slot roles that are different.
-        */
-       public function getRolesWithDifferentContent( RevisionSlots $other ) {
-               if ( $other === $this ) {
-                       return [];
-               }
-
-               $aSlots = $this->getSlots();
-               $bSlots = $other->getSlots();
-
-               ksort( $aSlots );
-               ksort( $bSlots );
-
-               $different = array_keys( array_merge(
-                       array_diff_key( $aSlots, $bSlots ),
-                       array_diff_key( $bSlots, $aSlots )
-               ) );
-
-               /** @var SlotRecord[] $common */
-               $common = array_intersect_key( $aSlots, $bSlots );
-
-               foreach ( $common as $role => $s ) {
-                       $t = $bSlots[$role];
-
-                       if ( !$s->hasSameContent( $t ) ) {
-                               $different[] = $role;
-                       }
-               }
-
-               return $different;
-       }
-
-}
index d173a3c..a863ad5 100644 (file)
 namespace MediaWiki\Storage;
 
 use Content;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\SlotRecord;
 
 /**
  * Value object representing a modification of revision slots.
diff --git a/includes/Storage/RevisionStore.php b/includes/Storage/RevisionStore.php
deleted file mode 100644 (file)
index d9db8bd..0000000
+++ /dev/null
@@ -1,2779 +0,0 @@
-<?php
-/**
- * Service for looking up page revisions.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * Attribution notice: when this file was created, much of its content was taken
- * from the Revision.php file as present in release 1.30. Refer to the history
- * of that file for original authorship.
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use ActorMigration;
-use CommentStore;
-use CommentStoreComment;
-use Content;
-use ContentHandler;
-use DBAccessObjectUtils;
-use Hooks;
-use IDBAccessObject;
-use InvalidArgumentException;
-use IP;
-use LogicException;
-use MediaWiki\Linker\LinkTarget;
-use MediaWiki\User\UserIdentity;
-use MediaWiki\User\UserIdentityValue;
-use Message;
-use MWException;
-use MWUnknownContentModelException;
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use RecentChange;
-use Revision;
-use RuntimeException;
-use stdClass;
-use Title;
-use User;
-use WANObjectCache;
-use Wikimedia\Assert\Assert;
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\DBConnRef;
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\ILoadBalancer;
-
-/**
- * Service for looking up page revisions.
- *
- * @since 1.31
- *
- * @note This was written to act as a drop-in replacement for the corresponding
- *       static methods in Revision.
- */
-class RevisionStore
-       implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
-
-       const ROW_CACHE_KEY = 'revision-row-1.29';
-
-       /**
-        * @var SqlBlobStore
-        */
-       private $blobStore;
-
-       /**
-        * @var bool|string
-        */
-       private $wikiId;
-
-       /**
-        * @var boolean
-        * @see $wgContentHandlerUseDB
-        */
-       private $contentHandlerUseDB = true;
-
-       /**
-        * @var ILoadBalancer
-        */
-       private $loadBalancer;
-
-       /**
-        * @var WANObjectCache
-        */
-       private $cache;
-
-       /**
-        * @var CommentStore
-        */
-       private $commentStore;
-
-       /**
-        * @var ActorMigration
-        */
-       private $actorMigration;
-
-       /**
-        * @var LoggerInterface
-        */
-       private $logger;
-
-       /**
-        * @var NameTableStore
-        */
-       private $contentModelStore;
-
-       /**
-        * @var NameTableStore
-        */
-       private $slotRoleStore;
-
-       /** @var int An appropriate combination of SCHEMA_COMPAT_XXX flags. */
-       private $mcrMigrationStage;
-
-       /**
-        * @todo $blobStore should be allowed to be any BlobStore!
-        *
-        * @param ILoadBalancer $loadBalancer
-        * @param SqlBlobStore $blobStore
-        * @param WANObjectCache $cache A cache for caching revision rows. This can be the local
-        *        wiki's default instance even if $wikiId refers to a different wiki, since
-        *        makeGlobalKey() is used to constructed a key that allows cached revision rows from
-        *        the same database to be re-used between wikis. For example, enwiki and frwiki will
-        *        use the same cache keys for revision rows from the wikidatawiki database, regardless
-        *        of the cache's default key space.
-        * @param CommentStore $commentStore
-        * @param NameTableStore $contentModelStore
-        * @param NameTableStore $slotRoleStore
-        * @param int $mcrMigrationStage An appropriate combination of SCHEMA_COMPAT_XXX flags
-        * @param ActorMigration $actorMigration
-        * @param bool|string $wikiId
-        *
-        * @throws MWException if $mcrMigrationStage or $wikiId is invalid.
-        */
-       public function __construct(
-               ILoadBalancer $loadBalancer,
-               SqlBlobStore $blobStore,
-               WANObjectCache $cache,
-               CommentStore $commentStore,
-               NameTableStore $contentModelStore,
-               NameTableStore $slotRoleStore,
-               $mcrMigrationStage,
-               ActorMigration $actorMigration,
-               $wikiId = false
-       ) {
-               Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
-               Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
-               Assert::parameter(
-                       ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== SCHEMA_COMPAT_READ_BOTH,
-                       '$mcrMigrationStage',
-                       'Reading from the old and the new schema at the same time is not supported.'
-               );
-               Assert::parameter(
-                       ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== 0,
-                       '$mcrMigrationStage',
-                       'Reading needs to be enabled for the old or the new schema.'
-               );
-               Assert::parameter(
-                       ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_BOTH ) !== 0,
-                       '$mcrMigrationStage',
-                       'Writing needs to be enabled for the old or the new schema.'
-               );
-               Assert::parameter(
-                       ( $mcrMigrationStage & SCHEMA_COMPAT_READ_OLD ) === 0
-                       || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) !== 0,
-                       '$mcrMigrationStage',
-                       'Cannot read the old schema when not also writing it.'
-               );
-               Assert::parameter(
-                       ( $mcrMigrationStage & SCHEMA_COMPAT_READ_NEW ) === 0
-                       || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) !== 0,
-                       '$mcrMigrationStage',
-                       'Cannot read the new schema when not also writing it.'
-               );
-
-               $this->loadBalancer = $loadBalancer;
-               $this->blobStore = $blobStore;
-               $this->cache = $cache;
-               $this->commentStore = $commentStore;
-               $this->contentModelStore = $contentModelStore;
-               $this->slotRoleStore = $slotRoleStore;
-               $this->mcrMigrationStage = $mcrMigrationStage;
-               $this->actorMigration = $actorMigration;
-               $this->wikiId = $wikiId;
-               $this->logger = new NullLogger();
-       }
-
-       /**
-        * @param int $flags A combination of SCHEMA_COMPAT_XXX flags.
-        * @return bool True if all the given flags were set in the $mcrMigrationStage
-        *         parameter passed to the constructor.
-        */
-       private function hasMcrSchemaFlags( $flags ) {
-               return ( $this->mcrMigrationStage & $flags ) === $flags;
-       }
-
-       /**
-        * Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading
-        * and still reading from the old DB schema.
-        *
-        * @throws RevisionAccessException
-        */
-       private function assertCrossWikiContentLoadingIsSafe() {
-               if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
-                       throw new RevisionAccessException(
-                               "Cross-wiki content loading is not supported by the pre-MCR schema"
-                       );
-               }
-       }
-
-       public function setLogger( LoggerInterface $logger ) {
-               $this->logger = $logger;
-       }
-
-       /**
-        * @return bool Whether the store is read-only
-        */
-       public function isReadOnly() {
-               return $this->blobStore->isReadOnly();
-       }
-
-       /**
-        * @return bool
-        */
-       public function getContentHandlerUseDB() {
-               return $this->contentHandlerUseDB;
-       }
-
-       /**
-        * @see $wgContentHandlerUseDB
-        * @param bool $contentHandlerUseDB
-        * @throws MWException
-        */
-       public function setContentHandlerUseDB( $contentHandlerUseDB ) {
-               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
-                       || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
-               ) {
-                       if ( !$contentHandlerUseDB ) {
-                               throw new MWException(
-                                       'Content model must be stored in the database for multi content revision migration.'
-                               );
-                       }
-               }
-               $this->contentHandlerUseDB = $contentHandlerUseDB;
-       }
-
-       /**
-        * @return ILoadBalancer
-        */
-       private function getDBLoadBalancer() {
-               return $this->loadBalancer;
-       }
-
-       /**
-        * @param int $mode DB_MASTER or DB_REPLICA
-        *
-        * @return IDatabase
-        */
-       private function getDBConnection( $mode ) {
-               $lb = $this->getDBLoadBalancer();
-               return $lb->getConnection( $mode, [], $this->wikiId );
-       }
-
-       /**
-        * @param int $queryFlags a bit field composed of READ_XXX flags
-        *
-        * @return DBConnRef
-        */
-       private function getDBConnectionRefForQueryFlags( $queryFlags ) {
-               list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
-               return $this->getDBConnectionRef( $mode );
-       }
-
-       /**
-        * @param IDatabase $connection
-        */
-       private function releaseDBConnection( IDatabase $connection ) {
-               $lb = $this->getDBLoadBalancer();
-               $lb->reuseConnection( $connection );
-       }
-
-       /**
-        * @param int $mode DB_MASTER or DB_REPLICA
-        *
-        * @return DBConnRef
-        */
-       private function getDBConnectionRef( $mode ) {
-               $lb = $this->getDBLoadBalancer();
-               return $lb->getConnectionRef( $mode, [], $this->wikiId );
-       }
-
-       /**
-        * Determines the page Title based on the available information.
-        *
-        * MCR migration note: this corresponds to Revision::getTitle
-        *
-        * @note this method should be private, external use should be avoided!
-        *
-        * @param int|null $pageId
-        * @param int|null $revId
-        * @param int $queryFlags
-        *
-        * @return Title
-        * @throws RevisionAccessException
-        */
-       public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
-               if ( !$pageId && !$revId ) {
-                       throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
-               }
-
-               // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
-               // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
-               if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
-                       $queryFlags = self::READ_NORMAL;
-               }
-
-               $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
-               list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
-               $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
-
-               // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
-               if ( $canUseTitleNewFromId ) {
-                       // TODO: better foreign title handling (introduce TitleFactory)
-                       $title = Title::newFromID( $pageId, $titleFlags );
-                       if ( $title ) {
-                               return $title;
-                       }
-               }
-
-               // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
-               $canUseRevId = ( $revId !== null && $revId > 0 );
-
-               if ( $canUseRevId ) {
-                       $dbr = $this->getDBConnectionRef( $dbMode );
-                       // @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' => $revId ],
-                               __METHOD__,
-                               $dbOptions,
-                               [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
-                       );
-                       if ( $row ) {
-                               // TODO: better foreign title handling (introduce TitleFactory)
-                               return Title::newFromRow( $row );
-                       }
-               }
-
-               // If we still don't have a title, fallback to master if that wasn't already happening.
-               if ( $dbMode !== DB_MASTER ) {
-                       $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
-                       if ( $title ) {
-                               $this->logger->info(
-                                       __METHOD__ . ' fell back to READ_LATEST and got a Title.',
-                                       [ 'trace' => wfBacktrace() ]
-                               );
-                               return $title;
-                       }
-               }
-
-               throw new RevisionAccessException(
-                       "Could not determine title for page ID $pageId and revision ID $revId"
-               );
-       }
-
-       /**
-        * @param mixed $value
-        * @param string $name
-        *
-        * @throws IncompleteRevisionException if $value is null
-        * @return mixed $value, if $value is not null
-        */
-       private function failOnNull( $value, $name ) {
-               if ( $value === null ) {
-                       throw new IncompleteRevisionException(
-                               "$name must not be " . var_export( $value, true ) . "!"
-                       );
-               }
-
-               return $value;
-       }
-
-       /**
-        * @param mixed $value
-        * @param string $name
-        *
-        * @throws IncompleteRevisionException if $value is empty
-        * @return mixed $value, if $value is not null
-        */
-       private function failOnEmpty( $value, $name ) {
-               if ( $value === null || $value === 0 || $value === '' ) {
-                       throw new IncompleteRevisionException(
-                               "$name must not be " . var_export( $value, true ) . "!"
-                       );
-               }
-
-               return $value;
-       }
-
-       /**
-        * Insert a new revision into the database, returning the new revision record
-        * on success and dies horribly on failure.
-        *
-        * MCR migration note: this replaces Revision::insertOn
-        *
-        * @param RevisionRecord $rev
-        * @param IDatabase $dbw (master connection)
-        *
-        * @throws InvalidArgumentException
-        * @return RevisionRecord the new revision record.
-        */
-       public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
-               // TODO: pass in a DBTransactionContext instead of a database connection.
-               $this->checkDatabaseWikiId( $dbw );
-
-               $slotRoles = $rev->getSlotRoles();
-
-               // Make sure the main slot is always provided throughout migration
-               if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
-                       throw new InvalidArgumentException(
-                               'main slot must be provided'
-                       );
-               }
-
-               // If we are not writing into the new schema, we can't support extra slots.
-               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
-                       && $slotRoles !== [ SlotRecord::MAIN ]
-               ) {
-                       throw new InvalidArgumentException(
-                               'Only the main slot is supported when not writing to the MCR enabled schema!'
-                       );
-               }
-
-               // As long as we are not reading from the new schema, we don't want to write extra slots.
-               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
-                       && $slotRoles !== [ SlotRecord::MAIN ]
-               ) {
-                       throw new InvalidArgumentException(
-                               'Only the main slot is supported when not reading from the MCR enabled schema!'
-                       );
-               }
-
-               // Checks
-               $this->failOnNull( $rev->getSize(), 'size field' );
-               $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
-               $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
-               $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
-               $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
-               $this->failOnNull( $user->getId(), 'user field' );
-               $this->failOnEmpty( $user->getName(), 'user_text field' );
-
-               if ( !$rev->isReadyForInsertion() ) {
-                       // This is here for future-proofing. At the time this check being added, it
-                       // was redundant to the individual checks above.
-                       throw new IncompleteRevisionException( 'Revision is incomplete' );
-               }
-
-               // TODO: we shouldn't need an actual Title here.
-               $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
-               $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
-
-               $parentId = $rev->getParentId() === null
-                       ? $this->getPreviousRevisionId( $dbw, $rev )
-                       : $rev->getParentId();
-
-               /** @var RevisionRecord $rev */
-               $rev = $dbw->doAtomicSection(
-                       __METHOD__,
-                       function ( IDatabase $dbw, $fname ) use (
-                               $rev,
-                               $user,
-                               $comment,
-                               $title,
-                               $pageId,
-                               $parentId
-                       ) {
-                               return $this->insertRevisionInternal(
-                                       $rev,
-                                       $dbw,
-                                       $user,
-                                       $comment,
-                                       $title,
-                                       $pageId,
-                                       $parentId
-                               );
-                       }
-               );
-
-               // sanity checks
-               Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
-               Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
-               Assert::postcondition(
-                       $rev->getComment( RevisionRecord::RAW ) !== null,
-                       'revision must have a comment'
-               );
-               Assert::postcondition(
-                       $rev->getUser( RevisionRecord::RAW ) !== null,
-                       'revision must have a user'
-               );
-
-               // Trigger exception if the main slot is missing.
-               // Technically, this could go away after MCR migration: while
-               // calling code may require a main slot to exist, RevisionStore
-               // really should not know or care about that requirement.
-               $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
-
-               foreach ( $slotRoles as $role ) {
-                       $slot = $rev->getSlot( $role, RevisionRecord::RAW );
-                       Assert::postcondition(
-                               $slot->getContent() !== null,
-                               $role . ' slot must have content'
-                       );
-                       Assert::postcondition(
-                               $slot->hasRevision(),
-                               $role . ' slot must have a revision associated'
-                       );
-               }
-
-               Hooks::run( 'RevisionRecordInserted', [ $rev ] );
-
-               // TODO: deprecate in 1.32!
-               $legacyRevision = new Revision( $rev );
-               Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
-
-               return $rev;
-       }
-
-       private function insertRevisionInternal(
-               RevisionRecord $rev,
-               IDatabase $dbw,
-               User $user,
-               CommentStoreComment $comment,
-               Title $title,
-               $pageId,
-               $parentId
-       ) {
-               $slotRoles = $rev->getSlotRoles();
-
-               $revisionRow = $this->insertRevisionRowOn(
-                       $dbw,
-                       $rev,
-                       $title,
-                       $parentId
-               );
-
-               $revisionId = $revisionRow['rev_id'];
-
-               $blobHints = [
-                       BlobStore::PAGE_HINT => $pageId,
-                       BlobStore::REVISION_HINT => $revisionId,
-                       BlobStore::PARENT_HINT => $parentId,
-               ];
-
-               $newSlots = [];
-               foreach ( $slotRoles as $role ) {
-                       $slot = $rev->getSlot( $role, RevisionRecord::RAW );
-
-                       // If the SlotRecord already has a revision ID set, this means it already exists
-                       // in the database, and should already belong to the current revision.
-                       // However, a slot may already have a revision, but no content ID, if the slot
-                       // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
-                       // mode, and the respective archive row was not yet migrated to the new schema.
-                       // In that case, a new slot row (and content row) must be inserted even during
-                       // undeletion.
-                       if ( $slot->hasRevision() && $slot->hasContentId() ) {
-                               // TODO: properly abort transaction if the assertion fails!
-                               Assert::parameter(
-                                       $slot->getRevision() === $revisionId,
-                                       'slot role ' . $slot->getRole(),
-                                       'Existing slot should belong to revision '
-                                       . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
-                               );
-
-                               // Slot exists, nothing to do, move along.
-                               // This happens when restoring archived revisions.
-
-                               $newSlots[$role] = $slot;
-
-                               // Write the main slot's text ID to the revision table for backwards compatibility
-                               if ( $slot->getRole() === SlotRecord::MAIN
-                                       && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
-                               ) {
-                                       $blobAddress = $slot->getAddress();
-                                       $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
-                               }
-                       } else {
-                               $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
-                       }
-               }
-
-               $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
-
-               $rev = new RevisionStoreRecord(
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$revisionRow,
-                       new RevisionSlots( $newSlots ),
-                       $this->wikiId
-               );
-
-               return $rev;
-       }
-
-       /**
-        * @param IDatabase $dbw
-        * @param int $revisionId
-        * @param string &$blobAddress (may change!)
-        *
-        * @return int the text row id
-        */
-       private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
-               $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
-               if ( !$textId ) {
-                       throw new LogicException(
-                               'Blob address not supported in 1.29 database schema: ' . $blobAddress
-                       );
-               }
-
-               // getTextIdFromAddress() is free to insert something into the text table, so $textId
-               // may be a new value, not anything already contained in $blobAddress.
-               $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
-
-               $dbw->update(
-                       'revision',
-                       [ 'rev_text_id' => $textId ],
-                       [ 'rev_id' => $revisionId ],
-                       __METHOD__
-               );
-
-               return $textId;
-       }
-
-       /**
-        * @param IDatabase $dbw
-        * @param int $revisionId
-        * @param SlotRecord $protoSlot
-        * @param Title $title
-        * @param array $blobHints See the BlobStore::XXX_HINT constants
-        * @return SlotRecord
-        */
-       private function insertSlotOn(
-               IDatabase $dbw,
-               $revisionId,
-               SlotRecord $protoSlot,
-               Title $title,
-               array $blobHints = []
-       ) {
-               if ( $protoSlot->hasAddress() ) {
-                       $blobAddress = $protoSlot->getAddress();
-               } else {
-                       $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
-               }
-
-               $contentId = null;
-
-               // Write the main slot's text ID to the revision table for backwards compatibility
-               if ( $protoSlot->getRole() === SlotRecord::MAIN
-                       && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
-               ) {
-                       // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
-                       // with the real content ID below.
-                       $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
-                       $contentId = $this->emulateContentId( $textId );
-               }
-
-               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
-                       if ( $protoSlot->hasContentId() ) {
-                               $contentId = $protoSlot->getContentId();
-                       } else {
-                               $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
-                       }
-
-                       $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
-               }
-
-               $savedSlot = SlotRecord::newSaved(
-                       $revisionId,
-                       $contentId,
-                       $blobAddress,
-                       $protoSlot
-               );
-
-               return $savedSlot;
-       }
-
-       /**
-        * Insert IP revision into ip_changes for use when querying for a range.
-        * @param IDatabase $dbw
-        * @param User $user
-        * @param RevisionRecord $rev
-        * @param int $revisionId
-        */
-       private function insertIpChangesRow(
-               IDatabase $dbw,
-               User $user,
-               RevisionRecord $rev,
-               $revisionId
-       ) {
-               if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
-                       $ipcRow = [
-                               'ipc_rev_id'        => $revisionId,
-                               'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
-                               'ipc_hex'           => IP::toHex( $user->getName() ),
-                       ];
-                       $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
-               }
-       }
-
-       /**
-        * @param IDatabase $dbw
-        * @param RevisionRecord $rev
-        * @param Title $title
-        * @param int $parentId
-        *
-        * @return array a revision table row
-        *
-        * @throws MWException
-        * @throws MWUnknownContentModelException
-        */
-       private function insertRevisionRowOn(
-               IDatabase $dbw,
-               RevisionRecord $rev,
-               Title $title,
-               $parentId
-       ) {
-               $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
-
-               list( $commentFields, $commentCallback ) =
-                       $this->commentStore->insertWithTempTable(
-                               $dbw,
-                               'rev_comment',
-                               $rev->getComment( RevisionRecord::RAW )
-                       );
-               $revisionRow += $commentFields;
-
-               list( $actorFields, $actorCallback ) =
-                       $this->actorMigration->getInsertValuesWithTempTable(
-                               $dbw,
-                               'rev_user',
-                               $rev->getUser( RevisionRecord::RAW )
-                       );
-               $revisionRow += $actorFields;
-
-               $dbw->insert( 'revision', $revisionRow, __METHOD__ );
-
-               if ( !isset( $revisionRow['rev_id'] ) ) {
-                       // only if auto-increment was used
-                       $revisionRow['rev_id'] = intval( $dbw->insertId() );
-
-                       if ( $dbw->getType() === 'mysql' ) {
-                               // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
-                               // auto-increment value to disk, so on server restart it might reuse IDs from deleted
-                               // revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
-
-                               $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
-                               $table = 'archive';
-                               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
-                                       $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
-                                       if ( $maxRevId2 >= $maxRevId ) {
-                                               $maxRevId = $maxRevId2;
-                                               $table = 'slots';
-                                       }
-                               }
-
-                               if ( $maxRevId >= $revisionRow['rev_id'] ) {
-                                       $this->logger->debug(
-                                               '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
-                                                       . ' Trying to fix it.',
-                                               [
-                                                       'revid' => $revisionRow['rev_id'],
-                                                       'table' => $table,
-                                                       'maxrevid' => $maxRevId,
-                                               ]
-                                       );
-
-                                       if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
-                                               throw new MWException( 'Failed to get database lock for T202032' );
-                                       }
-                                       $fname = __METHOD__;
-                                       $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) {
-                                               $dbw->unlock( 'fix-for-T202032', $fname );
-                                       } );
-
-                                       $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
-
-                                       // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
-                                       // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
-                                       // inserts too, though, at least on MariaDB 10.1.29.
-                                       //
-                                       // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
-                                       // transactions in this code path thanks to the row lock from the original ->insert() above.
-                                       //
-                                       // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
-                                       // that's for non-MySQL DBs.
-                                       $row1 = $dbw->query(
-                                               $dbw->selectSqlText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
-                                       )->fetchObject();
-                                       if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
-                                               $row2 = $dbw->query(
-                                                       $dbw->selectSqlText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
-                                                               . ' FOR UPDATE'
-                                               )->fetchObject();
-                                       } else {
-                                               $row2 = null;
-                                       }
-                                       $maxRevId = max(
-                                               $maxRevId,
-                                               $row1 ? intval( $row1->v ) : 0,
-                                               $row2 ? intval( $row2->v ) : 0
-                                       );
-
-                                       // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
-                                       // transactions will throw a duplicate key error here. It doesn't seem worth trying
-                                       // to avoid that.
-                                       $revisionRow['rev_id'] = $maxRevId + 1;
-                                       $dbw->insert( 'revision', $revisionRow, __METHOD__ );
-                               }
-                       }
-               }
-
-               $commentCallback( $revisionRow['rev_id'] );
-               $actorCallback( $revisionRow['rev_id'], $revisionRow );
-
-               return $revisionRow;
-       }
-
-       /**
-        * @param IDatabase $dbw
-        * @param RevisionRecord $rev
-        * @param Title $title
-        * @param int $parentId
-        *
-        * @return array [ 0 => array $revisionRow, 1 => callable  ]
-        * @throws MWException
-        * @throws MWUnknownContentModelException
-        */
-       private function getBaseRevisionRow(
-               IDatabase $dbw,
-               RevisionRecord $rev,
-               Title $title,
-               $parentId
-       ) {
-               // Record the edit in revisions
-               $revisionRow = [
-                       'rev_page'       => $rev->getPageId(),
-                       'rev_parent_id'  => $parentId,
-                       'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
-                       'rev_timestamp'  => $dbw->timestamp( $rev->getTimestamp() ),
-                       'rev_deleted'    => $rev->getVisibility(),
-                       'rev_len'        => $rev->getSize(),
-                       'rev_sha1'       => $rev->getSha1(),
-               ];
-
-               if ( $rev->getId() !== null ) {
-                       // Needed to restore revisions with their original ID
-                       $revisionRow['rev_id'] = $rev->getId();
-               }
-
-               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
-                       // In non MCR mode this IF section will relate to the main slot
-                       $mainSlot = $rev->getSlot( SlotRecord::MAIN );
-                       $model = $mainSlot->getModel();
-                       $format = $mainSlot->getFormat();
-
-                       // MCR migration note: rev_content_model and rev_content_format will go away
-                       if ( $this->contentHandlerUseDB ) {
-                               $this->assertCrossWikiContentLoadingIsSafe();
-
-                               $defaultModel = ContentHandler::getDefaultModelFor( $title );
-                               $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
-
-                               $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
-                               $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
-                       }
-               }
-
-               return $revisionRow;
-       }
-
-       /**
-        * @param SlotRecord $slot
-        * @param Title $title
-        * @param array $blobHints See the BlobStore::XXX_HINT constants
-        *
-        * @throws MWException
-        * @return string the blob address
-        */
-       private function storeContentBlob(
-               SlotRecord $slot,
-               Title $title,
-               array $blobHints = []
-       ) {
-               $content = $slot->getContent();
-               $format = $content->getDefaultFormat();
-               $model = $content->getModel();
-
-               $this->checkContent( $content, $title );
-
-               return $this->blobStore->storeBlob(
-                       $content->serialize( $format ),
-                       // These hints "leak" some information from the higher abstraction layer to
-                       // low level storage to allow for optimization.
-                       array_merge(
-                               $blobHints,
-                               [
-                                       BlobStore::DESIGNATION_HINT => 'page-content',
-                                       BlobStore::ROLE_HINT => $slot->getRole(),
-                                       BlobStore::SHA1_HINT => $slot->getSha1(),
-                                       BlobStore::MODEL_HINT => $model,
-                                       BlobStore::FORMAT_HINT => $format,
-                               ]
-                       )
-               );
-       }
-
-       /**
-        * @param SlotRecord $slot
-        * @param IDatabase $dbw
-        * @param int $revisionId
-        * @param int $contentId
-        */
-       private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
-               $slotRow = [
-                       'slot_revision_id' => $revisionId,
-                       'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
-                       'slot_content_id' => $contentId,
-                       // If the slot has a specific origin use that ID, otherwise use the ID of the revision
-                       // that we just inserted.
-                       'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
-               ];
-               $dbw->insert( 'slots', $slotRow, __METHOD__ );
-       }
-
-       /**
-        * @param SlotRecord $slot
-        * @param IDatabase $dbw
-        * @param string $blobAddress
-        * @return int content row ID
-        */
-       private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
-               $contentRow = [
-                       'content_size' => $slot->getSize(),
-                       'content_sha1' => $slot->getSha1(),
-                       'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
-                       'content_address' => $blobAddress,
-               ];
-               $dbw->insert( 'content', $contentRow, __METHOD__ );
-               return intval( $dbw->insertId() );
-       }
-
-       /**
-        * MCR migration note: this corresponds to Revision::checkContentModel
-        *
-        * @param Content $content
-        * @param Title $title
-        *
-        * @throws MWException
-        * @throws MWUnknownContentModelException
-        */
-       private function checkContent( Content $content, Title $title ) {
-               // Note: may return null for revisions that have not yet been inserted
-
-               $model = $content->getModel();
-               $format = $content->getDefaultFormat();
-               $handler = $content->getContentHandler();
-
-               $name = "$title";
-
-               if ( !$handler->isSupportedFormat( $format ) ) {
-                       throw new MWException( "Can't use format $format with content model $model on $name" );
-               }
-
-               if ( !$this->contentHandlerUseDB ) {
-                       // if $wgContentHandlerUseDB is not set,
-                       // all revisions must use the default content model and format.
-
-                       $this->assertCrossWikiContentLoadingIsSafe();
-
-                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
-                       $defaultHandler = ContentHandler::getForModelID( $defaultModel );
-                       $defaultFormat = $defaultHandler->getDefaultFormat();
-
-                       if ( $model != $defaultModel ) {
-                               throw new MWException( "Can't save non-default content model with "
-                                       . "\$wgContentHandlerUseDB disabled: model is $model, "
-                                       . "default for $name is $defaultModel"
-                               );
-                       }
-
-                       if ( $format != $defaultFormat ) {
-                               throw new MWException( "Can't use non-default content format with "
-                                       . "\$wgContentHandlerUseDB disabled: format is $format, "
-                                       . "default for $name is $defaultFormat"
-                               );
-                       }
-               }
-
-               if ( !$content->isValid() ) {
-                       throw new MWException(
-                               "New content for $name is not valid! Content model is $model"
-                       );
-               }
-       }
-
-       /**
-        * Create a new null-revision for insertion into a page's
-        * history. This will not re-save the text, but simply refer
-        * to the text from the previous version.
-        *
-        * Such revisions can for instance identify page rename
-        * operations and other such meta-modifications.
-        *
-        * @note This method grabs a FOR UPDATE lock on the relevant row of the page table,
-        * to prevent a new revision from being inserted before the null revision has been written
-        * to the database.
-        *
-        * MCR migration note: this replaces Revision::newNullRevision
-        *
-        * @todo Introduce newFromParentRevision(). newNullRevision can then be based on that
-        * (or go away).
-        *
-        * @param IDatabase $dbw used for obtaining the lock on the page table row
-        * @param Title $title Title of the page to read from
-        * @param CommentStoreComment $comment RevisionRecord's summary
-        * @param bool $minor Whether the revision should be considered as minor
-        * @param User $user The user to attribute the revision to
-        *
-        * @return RevisionRecord|null RevisionRecord or null on error
-        */
-       public function newNullRevision(
-               IDatabase $dbw,
-               Title $title,
-               CommentStoreComment $comment,
-               $minor,
-               User $user
-       ) {
-               $this->checkDatabaseWikiId( $dbw );
-
-               $pageId = $title->getArticleID();
-
-               // T51581: Lock the page table row to ensure no other process
-               // is adding a revision to the page at the same time.
-               // Avoid locking extra tables, compare T191892.
-               $pageLatest = $dbw->selectField(
-                       'page',
-                       'page_latest',
-                       [ 'page_id' => $pageId ],
-                       __METHOD__,
-                       [ 'FOR UPDATE' ]
-               );
-
-               if ( !$pageLatest ) {
-                       return null;
-               }
-
-               // Fetch the actual revision row from master, without locking all extra tables.
-               $oldRevision = $this->loadRevisionFromConds(
-                       $dbw,
-                       [ 'rev_id' => intval( $pageLatest ) ],
-                       self::READ_LATEST,
-                       $title
-               );
-
-               if ( !$oldRevision ) {
-                       $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
-                       $this->logger->error(
-                               $msg,
-                               [ 'exception' => new RuntimeException( $msg ) ]
-                       );
-                       return null;
-               }
-
-               // Construct the new revision
-               $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
-               $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
-
-               $newRevision->setComment( $comment );
-               $newRevision->setUser( $user );
-               $newRevision->setTimestamp( $timestamp );
-               $newRevision->setMinorEdit( $minor );
-
-               return $newRevision;
-       }
-
-       /**
-        * MCR migration note: this replaces Revision::isUnpatrolled
-        *
-        * @todo This is overly specific, so move or kill this method.
-        *
-        * @param RevisionRecord $rev
-        *
-        * @return int Rcid of the unpatrolled row, zero if there isn't one
-        */
-       public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
-               $rc = $this->getRecentChange( $rev );
-               if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
-                       return $rc->getAttribute( 'rc_id' );
-               } else {
-                       return 0;
-               }
-       }
-
-       /**
-        * Get the RC object belonging to the current revision, if there's one
-        *
-        * MCR migration note: this replaces Revision::getRecentChange
-        *
-        * @todo move this somewhere else?
-        *
-        * @param RevisionRecord $rev
-        * @param int $flags (optional) $flags include:
-        *      IDBAccessObject::READ_LATEST: Select the data from the master
-        *
-        * @return null|RecentChange
-        */
-       public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
-               list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
-               $db = $this->getDBConnection( $dbType );
-
-               $userIdentity = $rev->getUser( RevisionRecord::RAW );
-
-               if ( !$userIdentity ) {
-                       // If the revision has no user identity, chances are it never went
-                       // into the database, and doesn't have an RC entry.
-                       return null;
-               }
-
-               // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
-               $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
-               $rc = RecentChange::newFromConds(
-                       [
-                               $actorWhere['conds'],
-                               'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
-                               'rc_this_oldid' => $rev->getId()
-                       ],
-                       __METHOD__,
-                       $dbType
-               );
-
-               $this->releaseDBConnection( $db );
-
-               // XXX: cache this locally? Glue it to the RevisionRecord?
-               return $rc;
-       }
-
-       /**
-        * Maps fields of the archive row to corresponding revision rows.
-        *
-        * @param object $archiveRow
-        *
-        * @return object a revision row object, corresponding to $archiveRow.
-        */
-       private static function mapArchiveFields( $archiveRow ) {
-               $fieldMap = [
-                       // keep with ar prefix:
-                       'ar_id'        => 'ar_id',
-
-                       // not the same suffix:
-                       'ar_page_id'        => 'rev_page',
-                       'ar_rev_id'         => 'rev_id',
-
-                       // same suffix:
-                       'ar_text_id'        => 'rev_text_id',
-                       'ar_timestamp'      => 'rev_timestamp',
-                       'ar_user_text'      => 'rev_user_text',
-                       'ar_user'           => 'rev_user',
-                       'ar_actor'          => 'rev_actor',
-                       'ar_minor_edit'     => 'rev_minor_edit',
-                       'ar_deleted'        => 'rev_deleted',
-                       'ar_len'            => 'rev_len',
-                       'ar_parent_id'      => 'rev_parent_id',
-                       'ar_sha1'           => 'rev_sha1',
-                       'ar_comment'        => 'rev_comment',
-                       'ar_comment_cid'    => 'rev_comment_cid',
-                       'ar_comment_id'     => 'rev_comment_id',
-                       'ar_comment_text'   => 'rev_comment_text',
-                       'ar_comment_data'   => 'rev_comment_data',
-                       'ar_comment_old'    => 'rev_comment_old',
-                       'ar_content_format' => 'rev_content_format',
-                       'ar_content_model'  => 'rev_content_model',
-               ];
-
-               $revRow = new stdClass();
-               foreach ( $fieldMap as $arKey => $revKey ) {
-                       if ( property_exists( $archiveRow, $arKey ) ) {
-                               $revRow->$revKey = $archiveRow->$arKey;
-                       }
-               }
-
-               return $revRow;
-       }
-
-       /**
-        * Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
-        *
-        * @param object|array $row Either a database row or an array
-        * @param int $queryFlags for callbacks
-        * @param Title $title
-        *
-        * @return SlotRecord The main slot, extracted from the MW 1.29 style row.
-        * @throws MWException
-        */
-       private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
-               $mainSlotRow = new stdClass();
-               $mainSlotRow->role_name = SlotRecord::MAIN;
-               $mainSlotRow->model_name = null;
-               $mainSlotRow->slot_revision_id = null;
-               $mainSlotRow->slot_content_id = null;
-               $mainSlotRow->content_address = null;
-
-               $content = null;
-               $blobData = null;
-               $blobFlags = null;
-
-               if ( is_object( $row ) ) {
-                       if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
-                               // Don't emulate from a row when using the new schema.
-                               // Emulating from an array is still OK.
-                               throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
-                       }
-
-                       // archive row
-                       if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
-                               $row = $this->mapArchiveFields( $row );
-                       }
-
-                       if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
-                               $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
-                                       $row->rev_text_id
-                               );
-                       }
-
-                       // This is used by null-revisions
-                       $mainSlotRow->slot_origin = isset( $row->slot_origin )
-                               ? intval( $row->slot_origin )
-                               : null;
-
-                       if ( isset( $row->old_text ) ) {
-                               // this happens when the text-table gets joined directly, in the pre-1.30 schema
-                               $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
-                               // Check against selects that might have not included old_flags
-                               if ( !property_exists( $row, 'old_flags' ) ) {
-                                       throw new InvalidArgumentException( 'old_flags was not set in $row' );
-                               }
-                               $blobFlags = $row->old_flags ?? '';
-                       }
-
-                       $mainSlotRow->slot_revision_id = intval( $row->rev_id );
-
-                       $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
-                       $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
-                       $mainSlotRow->model_name = isset( $row->rev_content_model )
-                               ? strval( $row->rev_content_model )
-                               : null;
-                       // XXX: in the future, we'll probably always use the default format, and drop content_format
-                       $mainSlotRow->format_name = isset( $row->rev_content_format )
-                               ? strval( $row->rev_content_format )
-                               : null;
-
-                       if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
-                               // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
-                               $mainSlotRow->slot_content_id
-                                       = $this->emulateContentId( intval( $row->rev_text_id ) );
-                       }
-               } elseif ( is_array( $row ) ) {
-                       $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
-
-                       $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
-                               ? intval( $row['slot_origin'] )
-                               : null;
-                       $mainSlotRow->content_address = isset( $row['text_id'] )
-                               ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
-                               : null;
-                       $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
-                       $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
-
-                       $mainSlotRow->model_name = isset( $row['content_model'] )
-                               ? strval( $row['content_model'] ) : null;  // XXX: must be a string!
-                       // XXX: in the future, we'll probably always use the default format, and drop content_format
-                       $mainSlotRow->format_name = isset( $row['content_format'] )
-                               ? strval( $row['content_format'] ) : null;
-                       $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
-                       // XXX: If the flags field is not set then $blobFlags should be null so that no
-                       // decoding will happen. An empty string will result in default decodings.
-                       $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
-
-                       // if we have a Content object, override mText and mContentModel
-                       if ( !empty( $row['content'] ) ) {
-                               if ( !( $row['content'] instanceof Content ) ) {
-                                       throw new MWException( 'content field must contain a Content object.' );
-                               }
-
-                               /** @var Content $content */
-                               $content = $row['content'];
-                               $handler = $content->getContentHandler();
-
-                               $mainSlotRow->model_name = $content->getModel();
-
-                               // XXX: in the future, we'll probably always use the default format.
-                               if ( $mainSlotRow->format_name === null ) {
-                                       $mainSlotRow->format_name = $handler->getDefaultFormat();
-                               }
-                       }
-
-                       if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
-                               // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
-                               $mainSlotRow->slot_content_id
-                                       = $this->emulateContentId( intval( $row['text_id'] ) );
-                       }
-               } else {
-                       throw new MWException( 'Revision constructor passed invalid row format.' );
-               }
-
-               // With the old schema, the content changes with every revision,
-               // except for null-revisions.
-               if ( !isset( $mainSlotRow->slot_origin ) ) {
-                       $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
-               }
-
-               if ( $mainSlotRow->model_name === null ) {
-                       $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
-                               $this->assertCrossWikiContentLoadingIsSafe();
-
-                               // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget!
-                               // TODO: MCR: deprecate $title->getModel().
-                               return ContentHandler::getDefaultModelFor( $title );
-                       };
-               }
-
-               if ( !$content ) {
-                       // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
-                       // is missing, but "empty revisions" with no content are used in some edge cases.
-
-                       $content = function ( SlotRecord $slot )
-                               use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
-                       {
-                               return $this->loadSlotContent(
-                                       $slot,
-                                       $blobData,
-                                       $blobFlags,
-                                       $mainSlotRow->format_name,
-                                       $queryFlags
-                               );
-                       };
-               }
-
-               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
-                       // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
-                       // the inherited slot to have the same content_id as the original slot. In that case,
-                       // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
-                       $mainSlotRow->slot_content_id =
-                               function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
-                                       $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
-                                       return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
-                               };
-               }
-
-               return new SlotRecord( $mainSlotRow, $content );
-       }
-
-       /**
-        * Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode,
-        * based on the revision's text ID (rev_text_id or ar_text_id, respectively).
-        * Note that in SCHEMA_COMPAT_WRITE_BOTH, a callback to findSlotContentId() should be used
-        * instead, since in that mode, some revision rows may already have a real content ID,
-        * while other's don't - and for the ones that don't, we should indicate that it
-        * is missing and cause SlotRecords::hasContentId() to return false.
-        *
-        * @param int $textId
-        * @return int The emulated content ID
-        */
-       private function emulateContentId( $textId ) {
-               // Return a negative number to ensure the ID is distinct from any real content IDs
-               // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
-               // mode.
-               return -$textId;
-       }
-
-       /**
-        * Loads a Content object based on a slot row.
-        *
-        * This method does not call $slot->getContent(), and may be used as a callback
-        * called by $slot->getContent().
-        *
-        * MCR migration note: this roughly corresponds to Revision::getContentInternal
-        *
-        * @param SlotRecord $slot The SlotRecord to load content for
-        * @param string|null $blobData The content blob, in the form indicated by $blobFlags
-        * @param string|null $blobFlags Flags indicating how $blobData needs to be processed.
-        *        Use null if no processing should happen. That is in constrast to the empty string,
-        *        which causes the blob to be decoded according to the configured legacy encoding.
-        * @param string|null $blobFormat MIME type indicating how $dataBlob is encoded
-        * @param int $queryFlags
-        *
-        * @throws RevisionAccessException
-        * @return Content
-        */
-       private function loadSlotContent(
-               SlotRecord $slot,
-               $blobData = null,
-               $blobFlags = null,
-               $blobFormat = null,
-               $queryFlags = 0
-       ) {
-               if ( $blobData !== null ) {
-                       Assert::parameterType( 'string', $blobData, '$blobData' );
-                       Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
-
-                       $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
-
-                       if ( $blobFlags === null ) {
-                               // No blob flags, so use the blob verbatim.
-                               $data = $blobData;
-                       } else {
-                               $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
-                               if ( $data === false ) {
-                                       throw new RevisionAccessException(
-                                               "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
-                                       );
-                               }
-                       }
-
-               } else {
-                       $address = $slot->getAddress();
-                       try {
-                               $data = $this->blobStore->getBlob( $address, $queryFlags );
-                       } catch ( BlobAccessException $e ) {
-                               throw new RevisionAccessException(
-                                       "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
-                               );
-                       }
-               }
-
-               // Unserialize content
-               $handler = ContentHandler::getForModelID( $slot->getModel() );
-
-               $content = $handler->unserializeContent( $data, $blobFormat );
-               return $content;
-       }
-
-       /**
-        * Load a page revision from a given revision ID number.
-        * Returns null if no such revision can be found.
-        *
-        * MCR migration note: this replaces Revision::newFromId
-        *
-        * $flags include:
-        *      IDBAccessObject::READ_LATEST: Select the data from the master
-        *      IDBAccessObject::READ_LOCKING : Select & lock the data from the master
-        *
-        * @param int $id
-        * @param int $flags (optional)
-        * @return RevisionRecord|null
-        */
-       public function getRevisionById( $id, $flags = 0 ) {
-               return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
-       }
-
-       /**
-        * Load either the current, or a specified, revision
-        * that's attached to a given link target. If not attached
-        * to that link target, will return null.
-        *
-        * MCR migration note: this replaces Revision::newFromTitle
-        *
-        * $flags include:
-        *      IDBAccessObject::READ_LATEST: Select the data from the master
-        *      IDBAccessObject::READ_LOCKING : Select & lock the data from the master
-        *
-        * @param LinkTarget $linkTarget
-        * @param int $revId (optional)
-        * @param int $flags Bitfield (optional)
-        * @return RevisionRecord|null
-        */
-       public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
-               $conds = [
-                       'page_namespace' => $linkTarget->getNamespace(),
-                       'page_title' => $linkTarget->getDBkey()
-               ];
-               if ( $revId ) {
-                       // Use the specified revision ID.
-                       // Note that we use newRevisionFromConds here because we want to retry
-                       // and fall back to master if the page is not found on a replica.
-                       // Since the caller supplied a revision ID, we are pretty sure the revision is
-                       // supposed to exist, so we should try hard to find it.
-                       $conds['rev_id'] = $revId;
-                       return $this->newRevisionFromConds( $conds, $flags );
-               } else {
-                       // Use a join to get the latest revision.
-                       // Note that we don't use newRevisionFromConds here because we don't want to retry
-                       // and fall back to master. The assumption is that we only want to force the fallback
-                       // if we are quite sure the revision exists because the caller supplied a revision ID.
-                       // If the page isn't found at all on a replica, it probably simply does not exist.
-                       $db = $this->getDBConnectionRefForQueryFlags( $flags );
-
-                       $conds[] = 'rev_id=page_latest';
-                       $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
-
-                       return $rev;
-               }
-       }
-
-       /**
-        * Load either the current, or a specified, revision
-        * that's attached to a given page ID.
-        * Returns null if no such revision can be found.
-        *
-        * MCR migration note: this replaces Revision::newFromPageId
-        *
-        * $flags include:
-        *      IDBAccessObject::READ_LATEST: Select the data from the master (since 1.20)
-        *      IDBAccessObject::READ_LOCKING : Select & lock the data from the master
-        *
-        * @param int $pageId
-        * @param int $revId (optional)
-        * @param int $flags Bitfield (optional)
-        * @return RevisionRecord|null
-        */
-       public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
-               $conds = [ 'page_id' => $pageId ];
-               if ( $revId ) {
-                       // Use the specified revision ID.
-                       // Note that we use newRevisionFromConds here because we want to retry
-                       // and fall back to master if the page is not found on a replica.
-                       // Since the caller supplied a revision ID, we are pretty sure the revision is
-                       // supposed to exist, so we should try hard to find it.
-                       $conds['rev_id'] = $revId;
-                       return $this->newRevisionFromConds( $conds, $flags );
-               } else {
-                       // Use a join to get the latest revision.
-                       // Note that we don't use newRevisionFromConds here because we don't want to retry
-                       // and fall back to master. The assumption is that we only want to force the fallback
-                       // if we are quite sure the revision exists because the caller supplied a revision ID.
-                       // If the page isn't found at all on a replica, it probably simply does not exist.
-                       $db = $this->getDBConnectionRefForQueryFlags( $flags );
-
-                       $conds[] = 'rev_id=page_latest';
-                       $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
-
-                       return $rev;
-               }
-       }
-
-       /**
-        * Load the revision for the given title with the given timestamp.
-        * WARNING: Timestamps may in some circumstances not be unique,
-        * so this isn't the best key to use.
-        *
-        * MCR migration note: this replaces Revision::loadFromTimestamp
-        *
-        * @param Title $title
-        * @param string $timestamp
-        * @return RevisionRecord|null
-        */
-       public function getRevisionByTimestamp( $title, $timestamp ) {
-               $db = $this->getDBConnection( DB_REPLICA );
-               return $this->newRevisionFromConds(
-                       [
-                               'rev_timestamp' => $db->timestamp( $timestamp ),
-                               'page_namespace' => $title->getNamespace(),
-                               'page_title' => $title->getDBkey()
-                       ],
-                       0,
-                       $title
-               );
-       }
-
-       /**
-        * @param int $revId The revision to load slots for.
-        * @param int $queryFlags
-        *
-        * @return SlotRecord[]
-        */
-       private function loadSlotRecords( $revId, $queryFlags ) {
-               $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
-
-               list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
-               $db = $this->getDBConnectionRef( $dbMode );
-
-               $res = $db->select(
-                       $revQuery['tables'],
-                       $revQuery['fields'],
-                       [
-                               'slot_revision_id' => $revId,
-                       ],
-                       __METHOD__,
-                       $dbOptions,
-                       $revQuery['joins']
-               );
-
-               $slots = [];
-
-               foreach ( $res as $row ) {
-                       // resolve role names and model names from in-memory cache, instead of joining.
-                       $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
-                       $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
-
-                       $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) {
-                               return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
-                       };
-
-                       $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
-               }
-
-               if ( !isset( $slots[SlotRecord::MAIN] ) ) {
-                       throw new RevisionAccessException(
-                               'Main slot of revision ' . $revId . ' not found in database!'
-                       );
-               };
-
-               return $slots;
-       }
-
-       /**
-        * Factory method for RevisionSlots.
-        *
-        * @note If other code has a need to construct RevisionSlots objects, this should be made
-        * public, since RevisionSlots instances should not be constructed directly.
-        *
-        * @param int $revId
-        * @param object $revisionRow
-        * @param int $queryFlags
-        * @param Title $title
-        *
-        * @return RevisionSlots
-        * @throws MWException
-        */
-       private function newRevisionSlots(
-               $revId,
-               $revisionRow,
-               $queryFlags,
-               Title $title
-       ) {
-               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
-                       $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
-                       $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
-               } else {
-                       // XXX: do we need the same kind of caching here
-                       // that getKnownCurrentRevision uses (if $revId == page_latest?)
-
-                       $slots = new RevisionSlots( function () use( $revId, $queryFlags ) {
-                               return $this->loadSlotRecords( $revId, $queryFlags );
-                       } );
-               }
-
-               return $slots;
-       }
-
-       /**
-        * Make a fake revision object from an archive table row. This is queried
-        * for permissions or even inserted (as in Special:Undelete)
-        *
-        * MCR migration note: this replaces Revision::newFromArchiveRow
-        *
-        * @param object $row
-        * @param int $queryFlags
-        * @param Title|null $title
-        * @param array $overrides associative array with fields of $row to override. This may be
-        *   used e.g. to force the parent revision ID or page ID. Keys in the array are fields
-        *   names from the archive table without the 'ar_' prefix, i.e. use 'parent_id' to
-        *   override ar_parent_id.
-        *
-        * @return RevisionRecord
-        * @throws MWException
-        */
-       public function newRevisionFromArchiveRow(
-               $row,
-               $queryFlags = 0,
-               Title $title = null,
-               array $overrides = []
-       ) {
-               Assert::parameterType( 'object', $row, '$row' );
-
-               // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
-               Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
-
-               if ( !$title && isset( $overrides['title'] ) ) {
-                       if ( !( $overrides['title'] instanceof Title ) ) {
-                               throw new MWException( 'title field override must contain a Title object.' );
-                       }
-
-                       $title = $overrides['title'];
-               }
-
-               if ( !isset( $title ) ) {
-                       if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
-                               $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
-                       } else {
-                               throw new InvalidArgumentException(
-                                       'A Title or ar_namespace and ar_title must be given'
-                               );
-                       }
-               }
-
-               foreach ( $overrides as $key => $value ) {
-                       $field = "ar_$key";
-                       $row->$field = $value;
-               }
-
-               try {
-                       $user = User::newFromAnyId(
-                               $row->ar_user ?? null,
-                               $row->ar_user_text ?? null,
-                               $row->ar_actor ?? null
-                       );
-               } catch ( InvalidArgumentException $ex ) {
-                       wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
-                       $user = new UserIdentityValue( 0, 'Unknown user', 0 );
-               }
-
-               $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
-               // Legacy because $row may have come from self::selectFields()
-               $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
-
-               $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
-
-               return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
-       }
-
-       /**
-        * @see RevisionFactory::newRevisionFromRow
-        *
-        * MCR migration note: this replaces Revision::newFromRow
-        *
-        * @param object $row
-        * @param int $queryFlags
-        * @param Title|null $title
-        *
-        * @return RevisionRecord
-        */
-       public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
-               Assert::parameterType( 'object', $row, '$row' );
-
-               if ( !$title ) {
-                       $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
-                       $revId = $row->rev_id ?? 0;
-
-                       $title = $this->getTitle( $pageId, $revId, $queryFlags );
-               }
-
-               if ( !isset( $row->page_latest ) ) {
-                       $row->page_latest = $title->getLatestRevID();
-                       if ( $row->page_latest === 0 && $title->exists() ) {
-                               wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
-                       }
-               }
-
-               try {
-                       $user = User::newFromAnyId(
-                               $row->rev_user ?? null,
-                               $row->rev_user_text ?? null,
-                               $row->rev_actor ?? null
-                       );
-               } catch ( InvalidArgumentException $ex ) {
-                       wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
-                       $user = new UserIdentityValue( 0, 'Unknown user', 0 );
-               }
-
-               $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
-               // Legacy because $row may have come from self::selectFields()
-               $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
-
-               $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
-
-               return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
-       }
-
-       /**
-        * Constructs a new MutableRevisionRecord based on the given associative array following
-        * the MW1.29 convention for the Revision constructor.
-        *
-        * MCR migration note: this replaces Revision::newFromRow
-        *
-        * @param array $fields
-        * @param int $queryFlags
-        * @param Title|null $title
-        *
-        * @return MutableRevisionRecord
-        * @throws MWException
-        * @throws RevisionAccessException
-        */
-       public function newMutableRevisionFromArray(
-               array $fields,
-               $queryFlags = 0,
-               Title $title = null
-       ) {
-               if ( !$title && isset( $fields['title'] ) ) {
-                       if ( !( $fields['title'] instanceof Title ) ) {
-                               throw new MWException( 'title field must contain a Title object.' );
-                       }
-
-                       $title = $fields['title'];
-               }
-
-               if ( !$title ) {
-                       $pageId = $fields['page'] ?? 0;
-                       $revId = $fields['id'] ?? 0;
-
-                       $title = $this->getTitle( $pageId, $revId, $queryFlags );
-               }
-
-               if ( !isset( $fields['page'] ) ) {
-                       $fields['page'] = $title->getArticleID( $queryFlags );
-               }
-
-               // if we have a content object, use it to set the model and type
-               if ( !empty( $fields['content'] ) ) {
-                       if ( !( $fields['content'] instanceof Content ) && !is_array( $fields['content'] ) ) {
-                               throw new MWException(
-                                       'content field must contain a Content object or an array of Content objects.'
-                               );
-                       }
-               }
-
-               if ( !empty( $fields['text_id'] ) ) {
-                       if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
-                               throw new MWException( "The text_id field is only available in the pre-MCR schema" );
-                       }
-
-                       if ( !empty( $fields['content'] ) ) {
-                               throw new MWException(
-                                       "Text already stored in external store (id {$fields['text_id']}), " .
-                                       "can't specify content object"
-                               );
-                       }
-               }
-
-               if (
-                       isset( $fields['comment'] )
-                       && !( $fields['comment'] instanceof CommentStoreComment )
-               ) {
-                       $commentData = $fields['comment_data'] ?? null;
-
-                       if ( $fields['comment'] instanceof Message ) {
-                               $fields['comment'] = CommentStoreComment::newUnsavedComment(
-                                       $fields['comment'],
-                                       $commentData
-                               );
-                       } else {
-                               $commentText = trim( strval( $fields['comment'] ) );
-                               $fields['comment'] = CommentStoreComment::newUnsavedComment(
-                                       $commentText,
-                                       $commentData
-                               );
-                       }
-               }
-
-               $revision = new MutableRevisionRecord( $title, $this->wikiId );
-               $this->initializeMutableRevisionFromArray( $revision, $fields );
-
-               if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
-                       foreach ( $fields['content'] as $role => $content ) {
-                               $revision->setContent( $role, $content );
-                       }
-               } else {
-                       $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
-                       $revision->setSlot( $mainSlot );
-               }
-
-               return $revision;
-       }
-
-       /**
-        * @param MutableRevisionRecord $record
-        * @param array $fields
-        */
-       private function initializeMutableRevisionFromArray(
-               MutableRevisionRecord $record,
-               array $fields
-       ) {
-               /** @var UserIdentity $user */
-               $user = null;
-
-               if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
-                       $user = $fields['user'];
-               } else {
-                       try {
-                               $user = User::newFromAnyId(
-                                       $fields['user'] ?? null,
-                                       $fields['user_text'] ?? null,
-                                       $fields['actor'] ?? null
-                               );
-                       } catch ( InvalidArgumentException $ex ) {
-                               $user = null;
-                       }
-               }
-
-               if ( $user ) {
-                       $record->setUser( $user );
-               }
-
-               $timestamp = isset( $fields['timestamp'] )
-                       ? strval( $fields['timestamp'] )
-                       : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
-
-               $record->setTimestamp( $timestamp );
-
-               if ( isset( $fields['page'] ) ) {
-                       $record->setPageId( intval( $fields['page'] ) );
-               }
-
-               if ( isset( $fields['id'] ) ) {
-                       $record->setId( intval( $fields['id'] ) );
-               }
-               if ( isset( $fields['parent_id'] ) ) {
-                       $record->setParentId( intval( $fields['parent_id'] ) );
-               }
-
-               if ( isset( $fields['sha1'] ) ) {
-                       $record->setSha1( $fields['sha1'] );
-               }
-               if ( isset( $fields['size'] ) ) {
-                       $record->setSize( intval( $fields['size'] ) );
-               }
-
-               if ( isset( $fields['minor_edit'] ) ) {
-                       $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
-               }
-               if ( isset( $fields['deleted'] ) ) {
-                       $record->setVisibility( intval( $fields['deleted'] ) );
-               }
-
-               if ( isset( $fields['comment'] ) ) {
-                       Assert::parameterType(
-                               CommentStoreComment::class,
-                               $fields['comment'],
-                               '$row[\'comment\']'
-                       );
-                       $record->setComment( $fields['comment'] );
-               }
-       }
-
-       /**
-        * Load a page revision from a given revision ID number.
-        * Returns null if no such revision can be found.
-        *
-        * MCR migration note: this corresponds to Revision::loadFromId
-        *
-        * @note direct use is deprecated!
-        * @todo remove when unused! there seem to be no callers of Revision::loadFromId
-        *
-        * @param IDatabase $db
-        * @param int $id
-        *
-        * @return RevisionRecord|null
-        */
-       public function loadRevisionFromId( IDatabase $db, $id ) {
-               return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
-       }
-
-       /**
-        * Load either the current, or a specified, revision
-        * that's attached to a given page. If not attached
-        * to that page, will return null.
-        *
-        * MCR migration note: this replaces Revision::loadFromPageId
-        *
-        * @note direct use is deprecated!
-        * @todo remove when unused!
-        *
-        * @param IDatabase $db
-        * @param int $pageid
-        * @param int $id
-        * @return RevisionRecord|null
-        */
-       public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
-               $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
-               if ( $id ) {
-                       $conds['rev_id'] = intval( $id );
-               } else {
-                       $conds[] = 'rev_id=page_latest';
-               }
-               return $this->loadRevisionFromConds( $db, $conds );
-       }
-
-       /**
-        * Load either the current, or a specified, revision
-        * that's attached to a given page. If not attached
-        * to that page, will return null.
-        *
-        * MCR migration note: this replaces Revision::loadFromTitle
-        *
-        * @note direct use is deprecated!
-        * @todo remove when unused!
-        *
-        * @param IDatabase $db
-        * @param Title $title
-        * @param int $id
-        *
-        * @return RevisionRecord|null
-        */
-       public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
-               if ( $id ) {
-                       $matchId = intval( $id );
-               } else {
-                       $matchId = 'page_latest';
-               }
-
-               return $this->loadRevisionFromConds(
-                       $db,
-                       [
-                               "rev_id=$matchId",
-                               'page_namespace' => $title->getNamespace(),
-                               'page_title' => $title->getDBkey()
-                       ],
-                       0,
-                       $title
-               );
-       }
-
-       /**
-        * Load the revision for the given title with the given timestamp.
-        * WARNING: Timestamps may in some circumstances not be unique,
-        * so this isn't the best key to use.
-        *
-        * MCR migration note: this replaces Revision::loadFromTimestamp
-        *
-        * @note direct use is deprecated! Use getRevisionFromTimestamp instead!
-        * @todo remove when unused!
-        *
-        * @param IDatabase $db
-        * @param Title $title
-        * @param string $timestamp
-        * @return RevisionRecord|null
-        */
-       public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
-               return $this->loadRevisionFromConds( $db,
-                       [
-                               'rev_timestamp' => $db->timestamp( $timestamp ),
-                               'page_namespace' => $title->getNamespace(),
-                               'page_title' => $title->getDBkey()
-                       ],
-                       0,
-                       $title
-               );
-       }
-
-       /**
-        * Given a set of conditions, fetch a revision
-        *
-        * This method should be used if we are pretty sure the revision exists.
-        * Unless $flags has READ_LATEST set, this method will first try to find the revision
-        * on a replica before hitting the master database.
-        *
-        * MCR migration note: this corresponds to Revision::newFromConds
-        *
-        * @param array $conditions
-        * @param int $flags (optional)
-        * @param Title|null $title
-        *
-        * @return RevisionRecord|null
-        */
-       private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
-               $db = $this->getDBConnectionRefForQueryFlags( $flags );
-               $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
-
-               $lb = $this->getDBLoadBalancer();
-
-               // 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 )
-                       && $lb->getServerCount() > 1
-                       && $lb->hasOrMadeRecentMasterChanges()
-               ) {
-                       $flags = self::READ_LATEST;
-                       $dbw = $this->getDBConnection( DB_MASTER );
-                       $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
-                       $this->releaseDBConnection( $dbw );
-               }
-
-               return $rev;
-       }
-
-       /**
-        * Given a set of conditions, fetch a revision from
-        * the given database connection.
-        *
-        * MCR migration note: this corresponds to Revision::loadFromConds
-        *
-        * @param IDatabase $db
-        * @param array $conditions
-        * @param int $flags (optional)
-        * @param Title|null $title
-        *
-        * @return RevisionRecord|null
-        */
-       private function loadRevisionFromConds(
-               IDatabase $db,
-               $conditions,
-               $flags = 0,
-               Title $title = null
-       ) {
-               $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
-               if ( $row ) {
-                       $rev = $this->newRevisionFromRow( $row, $flags, $title );
-
-                       return $rev;
-               }
-
-               return null;
-       }
-
-       /**
-        * Throws an exception if the given database connection does not belong to the wiki this
-        * RevisionStore is bound to.
-        *
-        * @param IDatabase $db
-        * @throws MWException
-        */
-       private function checkDatabaseWikiId( IDatabase $db ) {
-               $storeWiki = $this->wikiId;
-               $dbWiki = $db->getDomainID();
-
-               if ( $dbWiki === $storeWiki ) {
-                       return;
-               }
-
-               // XXX: we really want the default database ID...
-               $storeWiki = $storeWiki ?: wfWikiID();
-               $dbWiki = $dbWiki ?: wfWikiID();
-
-               if ( $dbWiki === $storeWiki ) {
-                       return;
-               }
-
-               // HACK: counteract encoding imposed by DatabaseDomain
-               $storeWiki = str_replace( '?h', '-', $storeWiki );
-               $dbWiki = str_replace( '?h', '-', $dbWiki );
-
-               if ( $dbWiki === $storeWiki ) {
-                       return;
-               }
-
-               throw new MWException( "RevisionStore for $storeWiki "
-                       . "cannot be used with a DB connection for $dbWiki" );
-       }
-
-       /**
-        * Given a set of conditions, return a row with the
-        * fields necessary to build RevisionRecord objects.
-        *
-        * MCR migration note: this corresponds to Revision::fetchFromConds
-        *
-        * @param IDatabase $db
-        * @param array $conditions
-        * @param int $flags (optional)
-        *
-        * @return object|false data row as a raw object
-        */
-       private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
-               $this->checkDatabaseWikiId( $db );
-
-               $revQuery = $this->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']
-               );
-       }
-
-       /**
-        * Finds the ID of a content row for a given revision and slot role.
-        * This can be used to re-use content rows even while the content ID
-        * is still missing from SlotRecords, when writing to both the old and
-        * the new schema during MCR schema migration.
-        *
-        * @todo remove after MCR schema migration is complete.
-        *
-        * @param IDatabase $db
-        * @param int $revId
-        * @param string $role
-        *
-        * @return int|null
-        */
-       private function findSlotContentId( IDatabase $db, $revId, $role ) {
-               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
-                       return null;
-               }
-
-               try {
-                       $roleId = $this->slotRoleStore->getId( $role );
-                       $conditions = [
-                               'slot_revision_id' => $revId,
-                               'slot_role_id' => $roleId,
-                       ];
-
-                       $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
-
-                       return $contentId ?: null;
-               } catch ( NameTableAccessException $ex ) {
-                       // If the role is missing from the slot_roles table,
-                       // the corresponding row in slots cannot exist.
-                       return null;
-               }
-       }
-
-       /**
-        * Return the tables, fields, and join conditions to be selected to create
-        * a new RevisionStoreRecord object.
-        *
-        * MCR migration note: this replaces Revision::getQueryInfo
-        *
-        * If the format of fields returned changes in any way then the cache key provided by
-        * self::getRevisionRowCacheKey should be updated.
-        *
-        * @since 1.31
-        *
-        * @param array $options Any combination of the following strings
-        *  - 'page': Join with the page table, and select fields to identify the page
-        *  - 'user': Join with the user table, and select the user name
-        *  - 'text': Join with the text table, and select fields to load page text. This
-        *    option is deprecated in MW 1.32 when the MCR migration flag SCHEMA_COMPAT_WRITE_NEW
-        *    is set, and disallowed when SCHEMA_COMPAT_READ_OLD is not set.
-        *
-        * @return array With three keys:
-        *  - tables: (string[]) to include in the `$table` to `IDatabase->select()`
-        *  - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
-        *  - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
-        */
-       public function getQueryInfo( $options = [] ) {
-               $ret = [
-                       'tables' => [],
-                       'fields' => [],
-                       'joins'  => [],
-               ];
-
-               $ret['tables'][] = 'revision';
-               $ret['fields'] = array_merge( $ret['fields'], [
-                       'rev_id',
-                       'rev_page',
-                       'rev_timestamp',
-                       'rev_minor_edit',
-                       'rev_deleted',
-                       'rev_len',
-                       'rev_parent_id',
-                       'rev_sha1',
-               ] );
-
-               $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
-               $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
-               $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
-               $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
-
-               $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
-               $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
-               $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
-               $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
-
-               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
-                       $ret['fields'][] = 'rev_text_id';
-
-                       if ( $this->contentHandlerUseDB ) {
-                               $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',
-                       ] );
-                       $u = $actorQuery['fields']['rev_user'];
-                       $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
-               }
-
-               if ( in_array( 'text', $options, true ) ) {
-                       if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
-                               throw new InvalidArgumentException( 'text table can no longer be joined directly' );
-                       } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
-                               // NOTE: even when this class is set to not read from the old schema, callers
-                               // should still be able to join against the text table, as long as we are still
-                               // writing the old schema for compatibility.
-                               // TODO: This should trigger a deprecation warning eventually (T200918), but not
-                               // before all known usages are removed (see T198341 and T201164).
-                               // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
-                       }
-
-                       $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 SlotRecord.
-        *
-        * @since 1.32
-        *
-        * @param array $options Any combination of the following strings
-        *  - 'content': Join with the content table, and select content meta-data fields
-        *  - 'model': Join with the content_models table, and select the model_name field.
-        *             Only applicable if 'content' is also set.
-        *  - 'role': Join with the slot_roles table, and select the role_name field
-        *
-        * @return array With three keys:
-        *  - tables: (string[]) to include in the `$table` to `IDatabase->select()`
-        *  - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
-        *  - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
-        */
-       public function getSlotsQueryInfo( $options = [] ) {
-               $ret = [
-                       'tables' => [],
-                       'fields' => [],
-                       'joins'  => [],
-               ];
-
-               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
-                       $db = $this->getDBConnectionRef( DB_REPLICA );
-                       $ret['tables']['slots'] = 'revision';
-
-                       $ret['fields']['slot_revision_id'] = 'slots.rev_id';
-                       $ret['fields']['slot_content_id'] = 'NULL';
-                       $ret['fields']['slot_origin'] = 'slots.rev_id';
-                       $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
-
-                       if ( in_array( 'content', $options, true ) ) {
-                               $ret['fields']['content_size'] = 'slots.rev_len';
-                               $ret['fields']['content_sha1'] = 'slots.rev_sha1';
-                               $ret['fields']['content_address']
-                                       = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] );
-
-                               if ( $this->contentHandlerUseDB ) {
-                                       $ret['fields']['model_name'] = 'slots.rev_content_model';
-                               } else {
-                                       $ret['fields']['model_name'] = 'NULL';
-                               }
-                       }
-               } else {
-                       $ret['tables'][] = 'slots';
-                       $ret['fields'] = array_merge( $ret['fields'], [
-                               'slot_revision_id',
-                               'slot_content_id',
-                               'slot_origin',
-                               'slot_role_id',
-                       ] );
-
-                       if ( in_array( 'role', $options, true ) ) {
-                               // Use left join to attach role name, so we still find the revision row even
-                               // if the role name is missing. This triggers a more obvious failure mode.
-                               $ret['tables'][] = 'slot_roles';
-                               $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
-                               $ret['fields'][] = 'role_name';
-                       }
-
-                       if ( in_array( 'content', $options, true ) ) {
-                               $ret['tables'][] = 'content';
-                               $ret['fields'] = array_merge( $ret['fields'], [
-                                       'content_size',
-                                       'content_sha1',
-                                       'content_address',
-                                       'content_model',
-                               ] );
-                               $ret['joins']['content'] = [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ];
-
-                               if ( in_array( 'model', $options, true ) ) {
-                                       // Use left join to attach model name, so we still find the revision row even
-                                       // if the model name is missing. This triggers a more obvious failure mode.
-                                       $ret['tables'][] = 'content_models';
-                                       $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
-                                       $ret['fields'][] = 'model_name';
-                               }
-
-                       }
-               }
-
-               return $ret;
-       }
-
-       /**
-        * Return the tables, fields, and join conditions to be selected to create
-        * a new RevisionArchiveRecord object.
-        *
-        * MCR migration note: this replaces Revision::getArchiveQueryInfo
-        *
-        * @since 1.31
-        *
-        * @return array With three keys:
-        *   - tables: (string[]) to include in the `$table` to `IDatabase->select()`
-        *   - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
-        *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
-        */
-       public function getArchiveQueryInfo() {
-               $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
-               $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
-               $ret = [
-                       'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
-                       'fields' => [
-                                       'ar_id',
-                                       'ar_page_id',
-                                       'ar_namespace',
-                                       'ar_title',
-                                       'ar_rev_id',
-                                       'ar_timestamp',
-                                       'ar_minor_edit',
-                                       'ar_deleted',
-                                       'ar_len',
-                                       'ar_parent_id',
-                                       'ar_sha1',
-                               ] + $commentQuery['fields'] + $actorQuery['fields'],
-                       'joins' => $commentQuery['joins'] + $actorQuery['joins'],
-               ];
-
-               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
-                       $ret['fields'][] = 'ar_text_id';
-
-                       if ( $this->contentHandlerUseDB ) {
-                               $ret['fields'][] = 'ar_content_format';
-                               $ret['fields'][] = 'ar_content_model';
-                       }
-               }
-
-               return $ret;
-       }
-
-       /**
-        * Do a batched query for the sizes of a set of revisions.
-        *
-        * MCR migration note: this replaces Revision::getParentLengths
-        *
-        * @param int[] $revIds
-        * @return int[] associative array mapping revision IDs from $revIds to the nominal size
-        *         of the corresponding revision.
-        */
-       public function getRevisionSizes( array $revIds ) {
-               return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds );
-       }
-
-       /**
-        * Do a batched query for the sizes of a set of revisions.
-        *
-        * MCR migration note: this replaces Revision::getParentLengths
-        *
-        * @deprecated use RevisionStore::getRevisionSizes instead.
-        *
-        * @param IDatabase $db
-        * @param int[] $revIds
-        * @return int[] associative array mapping revision IDs from $revIds to the nominal size
-        *         of the corresponding revision.
-        */
-       public function listRevisionSizes( IDatabase $db, array $revIds ) {
-               $this->checkDatabaseWikiId( $db );
-
-               $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] = intval( $row->rev_len );
-               }
-
-               return $revLens;
-       }
-
-       /**
-        * Get previous revision for this title
-        *
-        * MCR migration note: this replaces Revision::getPrevious
-        *
-        * @param RevisionRecord $rev
-        * @param Title|null $title if known (optional)
-        *
-        * @return RevisionRecord|null
-        */
-       public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
-               if ( $title === null ) {
-                       $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
-               }
-               $prev = $title->getPreviousRevisionID( $rev->getId() );
-               if ( $prev ) {
-                       return $this->getRevisionByTitle( $title, $prev );
-               }
-               return null;
-       }
-
-       /**
-        * Get next revision for this title
-        *
-        * MCR migration note: this replaces Revision::getNext
-        *
-        * @param RevisionRecord $rev
-        * @param Title|null $title if known (optional)
-        *
-        * @return RevisionRecord|null
-        */
-       public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
-               if ( $title === null ) {
-                       $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
-               }
-               $next = $title->getNextRevisionID( $rev->getId() );
-               if ( $next ) {
-                       return $this->getRevisionByTitle( $title, $next );
-               }
-               return null;
-       }
-
-       /**
-        * Get previous revision Id for this page_id
-        * This is used to populate rev_parent_id on save
-        *
-        * MCR migration note: this corresponds to Revision::getPreviousRevisionId
-        *
-        * @param IDatabase $db
-        * @param RevisionRecord $rev
-        *
-        * @return int
-        */
-       private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
-               $this->checkDatabaseWikiId( $db );
-
-               if ( $rev->getPageId() === null ) {
-                       return 0;
-               }
-               # Use page_latest if ID is not given
-               if ( !$rev->getId() ) {
-                       $prevId = $db->selectField(
-                               'page', 'page_latest',
-                               [ 'page_id' => $rev->getPageId() ],
-                               __METHOD__
-                       );
-               } else {
-                       $prevId = $db->selectField(
-                               'revision', 'rev_id',
-                               [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
-                               __METHOD__,
-                               [ 'ORDER BY' => 'rev_id DESC' ]
-                       );
-               }
-               return intval( $prevId );
-       }
-
-       /**
-        * Get rev_timestamp from rev_id, without loading the rest of the row
-        *
-        * MCR migration note: this replaces Revision::getTimestampFromId
-        *
-        * @param Title $title
-        * @param int $id
-        * @param int $flags
-        * @return string|bool False if not found
-        */
-       public function getTimestampFromId( $title, $id, $flags = 0 ) {
-               $db = $this->getDBConnectionRefForQueryFlags( $flags );
-
-               $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;
-       }
-
-       /**
-        * Get count of revisions per page...not very efficient
-        *
-        * MCR migration note: this replaces Revision::countByPageId
-        *
-        * @param IDatabase $db
-        * @param int $id Page id
-        * @return int
-        */
-       public function countRevisionsByPageId( IDatabase $db, $id ) {
-               $this->checkDatabaseWikiId( $db );
-
-               $row = $db->selectRow( 'revision',
-                       [ 'revCount' => 'COUNT(*)' ],
-                       [ 'rev_page' => $id ],
-                       __METHOD__
-               );
-               if ( $row ) {
-                       return intval( $row->revCount );
-               }
-               return 0;
-       }
-
-       /**
-        * Get count of revisions per page...not very efficient
-        *
-        * MCR migration note: this replaces Revision::countByTitle
-        *
-        * @param IDatabase $db
-        * @param Title $title
-        * @return int
-        */
-       public function countRevisionsByTitle( IDatabase $db, $title ) {
-               $id = $title->getArticleID();
-               if ( $id ) {
-                       return $this->countRevisionsByPageId( $db, $id );
-               }
-               return 0;
-       }
-
-       /**
-        * Check if no edits were made by other users since
-        * the time a user started editing the page. Limit to
-        * 50 revisions for the sake of performance.
-        *
-        * MCR migration note: this replaces Revision::userWasLastToEdit
-        *
-        * @deprecated since 1.31; Can possibly be removed, since the self-conflict suppression
-        *       logic in EditPage that uses this seems conceptually dubious. Revision::userWasLastToEdit
-        *       has been deprecated since 1.24.
-        *
-        * @param IDatabase $db The Database to perform the check on.
-        * @param int $pageId The ID of the page in question
-        * @param int $userId The ID of the user in question
-        * @param string $since Look at edits since this time
-        *
-        * @return bool True if the given user was the only one to edit since the given timestamp
-        */
-       public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
-               $this->checkDatabaseWikiId( $db );
-
-               if ( !$userId ) {
-                       return false;
-               }
-
-               $revQuery = $this->getQueryInfo();
-               $res = $db->select(
-                       $revQuery['tables'],
-                       [
-                               'rev_user' => $revQuery['fields']['rev_user'],
-                       ],
-                       [
-                               'rev_page' => $pageId,
-                               'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
-                       ],
-                       __METHOD__,
-                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
-                       $revQuery['joins']
-               );
-               foreach ( $res as $row ) {
-                       if ( $row->rev_user != $userId ) {
-                               return false;
-                       }
-               }
-               return true;
-       }
-
-       /**
-        * Load a revision based on a known page ID and current revision ID from the DB
-        *
-        * 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.
-        *
-        * MCR migration note: this replaces Revision::newKnownCurrent
-        *
-        * @param Title $title the associated page title
-        * @param int $revId current revision of this page. Defaults to $title->getLatestRevID().
-        *
-        * @return RevisionRecord|bool Returns false if missing
-        */
-       public function getKnownCurrentRevision( Title $title, $revId ) {
-               $db = $this->getDBConnectionRef( DB_REPLICA );
-
-               $pageId = $title->getArticleID();
-
-               if ( !$pageId ) {
-                       return false;
-               }
-
-               if ( !$revId ) {
-                       $revId = $title->getLatestRevID();
-               }
-
-               if ( !$revId ) {
-                       wfWarn(
-                               'No latest revision known for page ' . $title->getPrefixedDBkey()
-                               . ' even though it exists with page ID ' . $pageId
-                       );
-                       return false;
-               }
-
-               $row = $this->cache->getWithSetCallback(
-                       // Page/rev IDs passed in from DB to reflect history merges
-                       $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
-                       WANObjectCache::TTL_WEEK,
-                       function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
-                               $setOpts += Database::getCacheSetOptions( $db );
-
-                               $conds = [
-                                       'rev_page' => intval( $pageId ),
-                                       'page_id' => intval( $pageId ),
-                                       'rev_id' => intval( $revId ),
-                               ];
-
-                               $row = $this->fetchRevisionRowFromConds( $db, $conds );
-                               return $row ?: false; // don't cache negatives
-                       }
-               );
-
-               // Reflect revision deletion and user renames
-               if ( $row ) {
-                       return $this->newRevisionFromRow( $row, 0, $title );
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] )
-        * Caching rows without 'page' or 'user' could lead to issues.
-        * If the format of the rows returned by the query provided by getQueryInfo changes the
-        * cache key should be updated to avoid conflicts.
-        *
-        * @param IDatabase $db
-        * @param int $pageId
-        * @param int $revId
-        * @return string
-        */
-       private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
-               return $this->cache->makeGlobalKey(
-                       self::ROW_CACHE_KEY,
-                       $db->getDomainID(),
-                       $pageId,
-                       $revId
-               );
-       }
-
-       // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
-
-}
diff --git a/includes/Storage/RevisionStoreFactory.php b/includes/Storage/RevisionStoreFactory.php
deleted file mode 100644 (file)
index aaaafc1..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-<?php
-
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * Attribution notice: when this file was created, much of its content was taken
- * from the Revision.php file as present in release 1.30. Refer to the history
- * of that file for original authorship.
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use ActorMigration;
-use CommentStore;
-use MediaWiki\Logger\Spi as LoggerSpi;
-use WANObjectCache;
-use Wikimedia\Assert\Assert;
-use Wikimedia\Rdbms\ILBFactory;
-
-/**
- * Factory service for RevisionStore instances. This allows RevisionStores to be created for
- * cross-wiki access.
- *
- * @warning Beware compatibility issues with schema migration in the context of cross-wiki access!
- * This class assumes that all wikis are at compatible migration stages for all relevant schemas.
- * Relevant schemas are: revision storage (MCR), the revision comment table, and the actor table.
- * Migration stages are compatible as long as a) there are no wikis in the cluster that only write
- * the old schema or b) there are no wikis that read only the new schema.
- *
- * @since 1.32
- */
-class RevisionStoreFactory {
-
-       /** @var BlobStoreFactory */
-       private $blobStoreFactory;
-       /** @var ILBFactory */
-       private $dbLoadBalancerFactory;
-       /** @var WANObjectCache */
-       private $cache;
-       /** @var LoggerSpi */
-       private $loggerProvider;
-
-       /** @var CommentStore */
-       private $commentStore;
-       /** @var ActorMigration */
-       private $actorMigration;
-       /** @var int One of the MIGRATION_* constants */
-       private $mcrMigrationStage;
-       /**
-        * @var bool
-        * @see $wgContentHandlerUseDB
-        */
-       private $contentHandlerUseDB;
-
-       /** @var NameTableStoreFactory */
-       private $nameTables;
-
-       /**
-        * @param ILBFactory $dbLoadBalancerFactory
-        * @param BlobStoreFactory $blobStoreFactory
-        * @param NameTableStoreFactory $nameTables
-        * @param WANObjectCache $cache
-        * @param CommentStore $commentStore
-        * @param ActorMigration $actorMigration
-        * @param int $migrationStage
-        * @param LoggerSpi $loggerProvider
-        * @param bool $contentHandlerUseDB see {@link $wgContentHandlerUseDB}. Must be the same
-        *        for all wikis in the cluster. Will go away after MCR migration.
-        */
-       public function __construct(
-               ILBFactory $dbLoadBalancerFactory,
-               BlobStoreFactory $blobStoreFactory,
-               NameTableStoreFactory $nameTables,
-               WANObjectCache $cache,
-               CommentStore $commentStore,
-               ActorMigration $actorMigration,
-               $migrationStage,
-               LoggerSpi $loggerProvider,
-               $contentHandlerUseDB
-       ) {
-               Assert::parameterType( 'integer', $migrationStage, '$migrationStage' );
-               $this->dbLoadBalancerFactory = $dbLoadBalancerFactory;
-               $this->blobStoreFactory = $blobStoreFactory;
-               $this->nameTables = $nameTables;
-               $this->cache = $cache;
-               $this->commentStore = $commentStore;
-               $this->actorMigration = $actorMigration;
-               $this->mcrMigrationStage = $migrationStage;
-               $this->loggerProvider = $loggerProvider;
-               $this->contentHandlerUseDB = $contentHandlerUseDB;
-       }
-
-       /**
-        * @since 1.32
-        *
-        * @param bool|string $wikiId false for the current domain / wikid
-        *
-        * @return RevisionStore for the given wikiId with all necessary services and a logger
-        */
-       public function getRevisionStore( $wikiId = false ) {
-               Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
-
-               $store = new RevisionStore(
-                       $this->dbLoadBalancerFactory->getMainLB( $wikiId ),
-                       $this->blobStoreFactory->newSqlBlobStore( $wikiId ),
-                       $this->cache, // Pass local cache instance; Leave cache sharing to RevisionStore.
-                       $this->commentStore,
-                       $this->nameTables->getContentModels( $wikiId ),
-                       $this->nameTables->getSlotRoles( $wikiId ),
-                       $this->mcrMigrationStage,
-                       $this->actorMigration,
-                       $wikiId
-               );
-
-               $store->setLogger( $this->loggerProvider->getLogger( 'RevisionStore' ) );
-               $store->setContentHandlerUseDB( $this->contentHandlerUseDB );
-
-               return $store;
-       }
-}
diff --git a/includes/Storage/RevisionStoreRecord.php b/includes/Storage/RevisionStoreRecord.php
deleted file mode 100644 (file)
index 6148c44..0000000
+++ /dev/null
@@ -1,219 +0,0 @@
-<?php
-/**
- * A RevisionRecord representing an existing revision persisted in the revision table.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use CommentStoreComment;
-use InvalidArgumentException;
-use MediaWiki\User\UserIdentity;
-use Title;
-use User;
-use Wikimedia\Assert\Assert;
-
-/**
- * A RevisionRecord representing an existing revision persisted in the revision table.
- * RevisionStoreRecord has no optional fields, getters will never return null.
- *
- * @since 1.31
- */
-class RevisionStoreRecord extends RevisionRecord {
-
-       /** @var bool */
-       protected $mCurrent = false;
-
-       /**
-        * @note Avoid calling this constructor directly. Use the appropriate methods
-        * in RevisionStore instead.
-        *
-        * @param Title $title The title of the page this Revision is associated with.
-        * @param UserIdentity $user
-        * @param CommentStoreComment $comment
-        * @param object $row A row from the revision table. Use RevisionStore::getQueryInfo() to build
-        *        a query that yields the required fields.
-        * @param RevisionSlots $slots The slots of this revision.
-        * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
-        *        or false for the local site.
-        */
-       function __construct(
-               Title $title,
-               UserIdentity $user,
-               CommentStoreComment $comment,
-               $row,
-               RevisionSlots $slots,
-               $wikiId = false
-       ) {
-               parent::__construct( $title, $slots, $wikiId );
-               Assert::parameterType( 'object', $row, '$row' );
-
-               $this->mId = intval( $row->rev_id );
-               $this->mPageId = intval( $row->rev_page );
-               $this->mComment = $comment;
-
-               $timestamp = wfTimestamp( TS_MW, $row->rev_timestamp );
-               Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
-
-               $this->mUser = $user;
-               $this->mMinorEdit = boolval( $row->rev_minor_edit );
-               $this->mTimestamp = $timestamp;
-               $this->mDeleted = intval( $row->rev_deleted );
-
-               // NOTE: rev_parent_id = 0 indicates that there is no parent revision, while null
-               // indicates that the parent revision is unknown. As per MW 1.31, the database schema
-               // allows rev_parent_id to be NULL.
-               $this->mParentId = isset( $row->rev_parent_id ) ? intval( $row->rev_parent_id ) : null;
-               $this->mSize = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
-               $this->mSha1 = !empty( $row->rev_sha1 ) ? $row->rev_sha1 : null;
-
-               // NOTE: we must not call $this->mTitle->getLatestRevID() here, since the state of
-               // page_latest may be in limbo during revision creation. In that case, calling
-               // $this->mTitle->getLatestRevID() would cause a bad value to be cached in the Title
-               // object. During page creation, that bad value would be 0.
-               if ( isset( $row->page_latest ) ) {
-                       $this->mCurrent = ( $row->rev_id == $row->page_latest );
-               }
-
-               // sanity check
-               if (
-                       $this->mPageId && $this->mTitle->exists()
-                       && $this->mPageId !== $this->mTitle->getArticleID()
-               ) {
-                       throw new InvalidArgumentException(
-                               'The given Title does not belong to page ID ' . $this->mPageId .
-                               ' but actually belongs to ' . $this->mTitle->getArticleID()
-                       );
-               }
-       }
-
-       /**
-        * MCR migration note: this replaces Revision::isCurrent
-        *
-        * @return bool
-        */
-       public function isCurrent() {
-               return $this->mCurrent;
-       }
-
-       /**
-        * MCR migration note: this replaces Revision::isDeleted
-        *
-        * @param int $field One of DELETED_* bitfield constants
-        *
-        * @return bool
-        */
-       public function 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 parent::isDeleted( $field );
-       }
-
-       protected function userCan( $field, User $user ) {
-               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 true; // no need to check
-               }
-
-               return parent::userCan( $field, $user );
-       }
-
-       /**
-        * @return int The revision id, never null.
-        */
-       public function getId() {
-               // overwritten just to add a guarantee to the contract
-               return parent::getId();
-       }
-
-       /**
-        * @throws RevisionAccessException if the size was unknown and could not be calculated.
-        * @return string The nominal revision size, never null. May be computed on the fly.
-        */
-       public function getSize() {
-               // If length is null, calculate and remember it (potentially SLOW!).
-               // This is for compatibility with old database rows that don't have the field set.
-               if ( $this->mSize === null ) {
-                       $this->mSize = $this->mSlots->computeSize();
-               }
-
-               return $this->mSize;
-       }
-
-       /**
-        * @throws RevisionAccessException if the hash was unknown and could not be calculated.
-        * @return string The revision hash, never null. May be computed on the fly.
-        */
-       public function getSha1() {
-               // If hash is null, calculate it and remember (potentially SLOW!)
-               // This is for compatibility with old database rows that don't have the field set.
-               if ( $this->mSha1 === null ) {
-                       $this->mSha1 = $this->mSlots->computeSha1();
-               }
-
-               return $this->mSha1;
-       }
-
-       /**
-        * @param int $audience
-        * @param User|null $user
-        *
-        * @return UserIdentity The identity of the revision author, null if access is forbidden.
-        */
-       public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
-               // overwritten just to add a guarantee to the contract
-               return parent::getUser( $audience, $user );
-       }
-
-       /**
-        * @param int $audience
-        * @param User|null $user
-        *
-        * @return CommentStoreComment The revision comment, null if access is forbidden.
-        */
-       public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
-               // overwritten just to add a guarantee to the contract
-               return parent::getComment( $audience, $user );
-       }
-
-       /**
-        * @return string timestamp, never null
-        */
-       public function getTimestamp() {
-               // overwritten just to add a guarantee to the contract
-               return parent::getTimestamp();
-       }
-
-       /**
-        * @see RevisionStore::isComplete
-        *
-        * @return bool always true.
-        */
-       public function isReadyForInsertion() {
-               return true;
-       }
-
-}
diff --git a/includes/Storage/SlotRecord.php b/includes/Storage/SlotRecord.php
deleted file mode 100644 (file)
index ee36d44..0000000
+++ /dev/null
@@ -1,658 +0,0 @@
-<?php
-/**
- * Value object representing a content slot associated with a page revision.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use Content;
-use InvalidArgumentException;
-use LogicException;
-use OutOfBoundsException;
-use Wikimedia\Assert\Assert;
-
-/**
- * Value object representing a content slot associated with a page revision.
- * SlotRecord provides direct access to a Content object.
- * That access may be implemented through a callback.
- *
- * @since 1.31
- */
-class SlotRecord {
-
-       const MAIN = 'main';
-
-       /**
-        * @var object database result row, as a raw object. Callbacks are supported for field values,
-        *      to enable on-demand emulation of these values. This is primarily intended for use
-        *      during schema migration.
-        */
-       private $row;
-
-       /**
-        * @var Content|callable
-        */
-       private $content;
-
-       /**
-        * Returns a new SlotRecord just like the given $slot, except that calling getContent()
-        * will fail with an exception.
-        *
-        * @param SlotRecord $slot
-        *
-        * @return SlotRecord
-        */
-       public static function newWithSuppressedContent( SlotRecord $slot ) {
-               $row = $slot->row;
-
-               return new SlotRecord( $row, function () {
-                       throw new SuppressedDataException( 'Content suppressed!' );
-               } );
-       }
-
-       /**
-        * Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
-        * The slot's content cannot be overwritten.
-        *
-        * @param SlotRecord $slot
-        * @param array $overrides
-        *
-        * @return SlotRecord
-        */
-       private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
-               $row = clone $slot->row;
-               $row->slot_id = null; // never copy the row ID!
-
-               foreach ( $overrides as $key => $value ) {
-                       $row->$key = $value;
-               }
-
-               return new SlotRecord( $row, $slot->content );
-       }
-
-       /**
-        * Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord
-        * of a previous revision.
-        *
-        * Note that a SlotRecord constructed this way are intended as prototypes,
-        * to be used wit newSaved(). They are incomplete, so some getters such as
-        * getRevision() will fail.
-        *
-        * @param SlotRecord $slot
-        *
-        * @return SlotRecord
-        */
-       public static function newInherited( SlotRecord $slot ) {
-               // Sanity check - we can't inherit from a Slot that's not attached to a revision.
-               $slot->getRevision();
-               $slot->getOrigin();
-               $slot->getAddress();
-
-               // NOTE: slot_origin and content_address are copied from $slot.
-               return self::newDerived( $slot, [
-                       'slot_revision_id' => null,
-               ] );
-       }
-
-       /**
-        * Constructs a new Slot from a Content object for a new revision.
-        * This is the preferred way to construct a slot for storing Content that
-        * resulted from a user edit. The slot is assumed to be not inherited.
-        *
-        * Note that a SlotRecord constructed this way are intended as prototypes,
-        * to be used wit newSaved(). They are incomplete, so some getters such as
-        * getAddress() will fail.
-        *
-        * @param string $role
-        * @param Content $content
-        *
-        * @return SlotRecord An incomplete proto-slot object, to be used with newSaved() later.
-        */
-       public static function newUnsaved( $role, Content $content ) {
-               Assert::parameterType( 'string', $role, '$role' );
-
-               $row = [
-                       'slot_id' => null, // not yet known
-                       'slot_revision_id' => null, // not yet known
-                       'slot_origin' => null, // not yet known, will be set in newSaved()
-                       'content_size' => null, // compute later
-                       'content_sha1' => null, // compute later
-                       'slot_content_id' => null, // not yet known, will be set in newSaved()
-                       'content_address' => null, // not yet known, will be set in newSaved()
-                       'role_name' => $role,
-                       'model_name' => $content->getModel(),
-               ];
-
-               return new SlotRecord( (object)$row, $content );
-       }
-
-       /**
-        * Constructs a complete SlotRecord for a newly saved revision, based on the incomplete
-        * proto-slot. This adds information that has only become available during saving,
-        * particularly the revision ID, content ID and content address.
-        *
-        * @param int $revisionId the revision the slot is to be associated with (field slot_revision_id).
-        *        If $protoSlot already has a revision, it must be the same.
-        * @param int|null $contentId the ID of the row in the content table describing the content
-        *        referenced by $contentAddress (field slot_content_id).
-        *        If $protoSlot already has a content ID, it must be the same.
-        * @param string $contentAddress the slot's content address (field content_address).
-        *        If $protoSlot already has an address, it must be the same.
-        * @param SlotRecord $protoSlot The proto-slot that was provided as input for creating a new
-        *        revision. $protoSlot must have a content address if inherited.
-        *
-        * @return SlotRecord If the state of $protoSlot is inappropriate for saving a new revision.
-        */
-       public static function newSaved(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               Assert::parameterType( 'integer', $revisionId, '$revisionId' );
-               // TODO once migration is over $contentId must be an integer
-               Assert::parameterType( 'integer|null', $contentId, '$contentId' );
-               Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
-
-               if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
-                       throw new LogicException(
-                               "Mismatching revision ID $revisionId: "
-                               . "The slot already belongs to revision {$protoSlot->getRevision()}. "
-                               . "Use SlotRecord::newInherited() to re-use content between revisions."
-                       );
-               }
-
-               if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
-                       throw new LogicException(
-                               "Mismatching blob address $contentAddress: "
-                               . "The slot already has content at {$protoSlot->getAddress()}."
-                       );
-               }
-
-               if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
-                       throw new LogicException(
-                               "Mismatching content ID $contentId: "
-                               . "The slot already has content row {$protoSlot->getContentId()} associated."
-                       );
-               }
-
-               if ( $protoSlot->isInherited() ) {
-                       if ( !$protoSlot->hasAddress() ) {
-                               throw new InvalidArgumentException(
-                                       "An inherited blob should have a content address!"
-                               );
-                       }
-                       if ( !$protoSlot->hasField( 'slot_origin' ) ) {
-                               throw new InvalidArgumentException(
-                                       "A saved inherited slot should have an origin set!"
-                               );
-                       }
-                       $origin = $protoSlot->getOrigin();
-               } else {
-                       $origin = $revisionId;
-               }
-
-               return self::newDerived( $protoSlot, [
-                       'slot_revision_id' => $revisionId,
-                       'slot_content_id' => $contentId,
-                       'slot_origin' => $origin,
-                       'content_address' => $contentAddress,
-               ] );
-       }
-
-       /**
-        * SlotRecord constructor.
-        *
-        * The following fields are supported by the $row parameter:
-        *
-        *   $row->blob_data
-        *   $row->blob_address
-        *
-        * @param object $row A database row composed of fields of the slot and content tables,
-        *        as a raw object. Any field value can be a callback that produces the field value
-        *        given this SlotRecord as a parameter. However, plain strings cannot be used as
-        *        callbacks here, for security reasons.
-        * @param Content|callable $content The content object associated with the slot, or a
-        *        callback that will return that Content object, given this SlotRecord as a parameter.
-        */
-       public function __construct( $row, $content ) {
-               Assert::parameterType( 'object', $row, '$row' );
-               Assert::parameterType( 'Content|callable', $content, '$content' );
-
-               Assert::parameter(
-                       property_exists( $row, 'slot_revision_id' ),
-                       '$row->slot_revision_id',
-                       'must exist'
-               );
-               Assert::parameter(
-                       property_exists( $row, 'slot_content_id' ),
-                       '$row->slot_content_id',
-                       'must exist'
-               );
-               Assert::parameter(
-                       property_exists( $row, 'content_address' ),
-                       '$row->content_address',
-                       'must exist'
-               );
-               Assert::parameter(
-                       property_exists( $row, 'model_name' ),
-                       '$row->model_name',
-                       'must exist'
-               );
-               Assert::parameter(
-                       property_exists( $row, 'slot_origin' ),
-                       '$row->slot_origin',
-                       'must exist'
-               );
-               Assert::parameter(
-                       !property_exists( $row, 'slot_inherited' ),
-                       '$row->slot_inherited',
-                       'must not exist'
-               );
-               Assert::parameter(
-                       !property_exists( $row, 'slot_revision' ),
-                       '$row->slot_revision',
-                       'must not exist'
-               );
-
-               $this->row = $row;
-               $this->content = $content;
-       }
-
-       /**
-        * Implemented to defy serialization.
-        *
-        * @throws LogicException always
-        */
-       public function __sleep() {
-               throw new LogicException( __CLASS__ . ' is not serializable.' );
-       }
-
-       /**
-        * Returns the Content of the given slot.
-        *
-        * @note This is free to load Content from whatever subsystem is necessary,
-        * performing potentially expensive operations and triggering I/O-related
-        * failure modes.
-        *
-        * @note This method does not apply audience filtering.
-        *
-        * @throws SuppressedDataException if access to the content is not allowed according
-        * to the audience check performed by RevisionRecord::getSlot().
-        *
-        * @return Content The slot's content. This is a direct reference to the internal instance,
-        * copy before exposing to application logic!
-        */
-       public function getContent() {
-               if ( $this->content instanceof Content ) {
-                       return $this->content;
-               }
-
-               $obj = call_user_func( $this->content, $this );
-
-               Assert::postcondition(
-                       $obj instanceof Content,
-                       'Slot content callback should return a Content object'
-               );
-
-               $this->content = $obj;
-
-               return $this->content;
-       }
-
-       /**
-        * Returns the string value of a data field from the database row supplied to the constructor.
-        * If the field was set to a callback, that callback is invoked and the result returned.
-        *
-        * @param string $name
-        *
-        * @throws OutOfBoundsException
-        * @throws IncompleteRevisionException
-        * @return mixed Returns the field's value, never null.
-        */
-       private function getField( $name ) {
-               if ( !isset( $this->row->$name ) ) {
-                       // distinguish between unknown and uninitialized fields
-                       if ( property_exists( $this->row, $name ) ) {
-                               throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
-                       } else {
-                               throw new OutOfBoundsException( 'No such field: ' . $name );
-                       }
-               }
-
-               $value = $this->row->$name;
-
-               // NOTE: allow callbacks, but don't trust plain string callables from the database!
-               if ( !is_string( $value ) && is_callable( $value ) ) {
-                       $value = call_user_func( $value, $this );
-                       $this->setField( $name, $value );
-               }
-
-               return $value;
-       }
-
-       /**
-        * Returns the string value of a data field from the database row supplied to the constructor.
-        *
-        * @param string $name
-        *
-        * @throws OutOfBoundsException
-        * @throws IncompleteRevisionException
-        * @return string Returns the string value
-        */
-       private function getStringField( $name ) {
-               return strval( $this->getField( $name ) );
-       }
-
-       /**
-        * Returns the int value of a data field from the database row supplied to the constructor.
-        *
-        * @param string $name
-        *
-        * @throws OutOfBoundsException
-        * @throws IncompleteRevisionException
-        * @return int Returns the int value
-        */
-       private function getIntField( $name ) {
-               return intval( $this->getField( $name ) );
-       }
-
-       /**
-        * @param string $name
-        * @return bool whether this record contains the given field
-        */
-       private function hasField( $name ) {
-               if ( isset( $this->row->$name ) ) {
-                       // if the field is a callback, resolve first, then re-check
-                       if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
-                               $this->getField( $name );
-                       }
-               }
-
-               return isset( $this->row->$name );
-       }
-
-       /**
-        * Returns the ID of the revision this slot is associated with.
-        *
-        * @return int
-        */
-       public function getRevision() {
-               return $this->getIntField( 'slot_revision_id' );
-       }
-
-       /**
-        * Returns the revision ID of the revision that originated the slot's content.
-        *
-        * @return int
-        */
-       public function getOrigin() {
-               return $this->getIntField( 'slot_origin' );
-       }
-
-       /**
-        * Whether this slot was inherited from an older revision.
-        *
-        * If this SlotRecord is already attached to a revision, this returns true
-        * if the slot's revision of origin is the same as the revision it belongs to.
-        *
-        * If this SlotRecord is not yet attached to a revision, this returns true
-        * if the slot already has an address.
-        *
-        * @return bool
-        */
-       public function isInherited() {
-               if ( $this->hasRevision() ) {
-                       return $this->getRevision() !== $this->getOrigin();
-               } else {
-                       return $this->hasAddress();
-               }
-       }
-
-       /**
-        * Whether this slot has an address. Slots will have an address if their
-        * content has been stored. While building a new revision,
-        * SlotRecords will not have an address associated.
-        *
-        * @return bool
-        */
-       public function hasAddress() {
-               return $this->hasField( 'content_address' );
-       }
-
-       /**
-        * Whether this slot has an origin (revision ID that originated the slot's content.
-        *
-        * @since 1.32
-        *
-        * @return bool
-        */
-       public function hasOrigin() {
-               return $this->hasField( 'slot_origin' );
-       }
-
-       /**
-        * Whether this slot has a content ID. Slots will have a content ID if their
-        * content has been stored in the content table. While building a new revision,
-        * SlotRecords will not have an ID associated.
-        *
-        * Also, during schema migration, hasContentId() may return false when encountering an
-        * un-migrated database entry in SCHEMA_COMPAT_WRITE_BOTH mode.
-        * It will however always return true for saved revisions on SCHEMA_COMPAT_READ_NEW mode,
-        * or without SCHEMA_COMPAT_WRITE_NEW mode. In the latter case, an emulated content ID
-        * is used, derived from the revision's text ID.
-        *
-        * Note that hasContentId() returning false while hasRevision() returns true always
-        * indicates an unmigrated row in SCHEMA_COMPAT_WRITE_BOTH mode, as described above.
-        * For an unsaved slot, both these methods would return false.
-        *
-        * @since 1.32
-        *
-        * @return bool
-        */
-       public function hasContentId() {
-               return $this->hasField( 'slot_content_id' );
-       }
-
-       /**
-        * Whether this slot has revision ID associated. Slots will have a revision ID associated
-        * only if they were loaded as part of an existing revision. While building a new revision,
-        * Slotrecords will not have a revision ID associated.
-        *
-        * @return bool
-        */
-       public function hasRevision() {
-               return $this->hasField( 'slot_revision_id' );
-       }
-
-       /**
-        * Returns the role of the slot.
-        *
-        * @return string
-        */
-       public function getRole() {
-               return $this->getStringField( 'role_name' );
-       }
-
-       /**
-        * Returns the address of this slot's content.
-        * This address can be used with BlobStore to load the Content object.
-        *
-        * @return string
-        */
-       public function getAddress() {
-               return $this->getStringField( 'content_address' );
-       }
-
-       /**
-        * Returns the ID of the content meta data row associated with the slot.
-        * This information should be irrelevant to application logic, it is here to allow
-        * the construction of a full row for the revision table.
-        *
-        * Note that this method may return an emulated value during schema migration in
-        * SCHEMA_COMPAT_WRITE_OLD mode. See RevisionStore::emulateContentId for more information.
-        *
-        * @return int
-        */
-       public function getContentId() {
-               return $this->getIntField( 'slot_content_id' );
-       }
-
-       /**
-        * Returns the content size
-        *
-        * @return int size of the content, in bogo-bytes, as reported by Content::getSize.
-        */
-       public function getSize() {
-               try {
-                       $size = $this->getIntField( 'content_size' );
-               } catch ( IncompleteRevisionException $ex ) {
-                       $size = $this->getContent()->getSize();
-                       $this->setField( 'content_size', $size );
-               }
-
-               return $size;
-       }
-
-       /**
-        * Returns the content size
-        *
-        * @return string hash of the content.
-        */
-       public function getSha1() {
-               try {
-                       $sha1 = $this->getStringField( 'content_sha1' );
-               } catch ( IncompleteRevisionException $ex ) {
-                       $format = $this->hasField( 'format_name' )
-                               ? $this->getStringField( 'format_name' )
-                               : null;
-
-                       $data = $this->getContent()->serialize( $format );
-                       $sha1 = self::base36Sha1( $data );
-                       $this->setField( 'content_sha1', $sha1 );
-               }
-
-               return $sha1;
-       }
-
-       /**
-        * Returns the content model. This is the model name that decides
-        * which ContentHandler is appropriate for interpreting the
-        * data of the blob referenced by the address returned by getAddress().
-        *
-        * @return string the content model of the content
-        */
-       public function getModel() {
-               try {
-                       $model = $this->getStringField( 'model_name' );
-               } catch ( IncompleteRevisionException $ex ) {
-                       $model = $this->getContent()->getModel();
-                       $this->setField( 'model_name', $model );
-               }
-
-               return $model;
-       }
-
-       /**
-        * Returns the blob serialization format as a MIME type.
-        *
-        * @note When this method returns null, the caller is expected
-        * to auto-detect the serialization format, or to rely on
-        * the default format associated with the content model.
-        *
-        * @return string|null
-        */
-       public function getFormat() {
-               // XXX: we currently do not plan to store the format for each slot!
-
-               if ( $this->hasField( 'format_name' ) ) {
-                       return $this->getStringField( 'format_name' );
-               }
-
-               return null;
-       }
-
-       /**
-        * @param string $name
-        * @param string|int|null $value
-        */
-       private function setField( $name, $value ) {
-               $this->row->$name = $value;
-       }
-
-       /**
-        * Get the base 36 SHA-1 value for a string of text
-        *
-        * MCR migration note: this replaces Revision::base36Sha1
-        *
-        * @param string $blob
-        * @return string
-        */
-       public static function base36Sha1( $blob ) {
-               return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
-       }
-
-       /**
-        * Returns true if $other has the same content as this slot.
-        * The check is performed based on the model, address size, and hash.
-        * Two slots can have the same content if they use different content addresses,
-        * but if they have the same address and the same model, they have the same content.
-        * Two slots can have the same content if they belong to different
-        * revisions or pages.
-        *
-        * Note that hasSameContent() may return false even if Content::equals returns true for
-        * the content of two slots. This may happen if the two slots have different serializations
-        * representing equivalent Content. Such false negatives are considered acceptable. Code
-        * that has to be absolutely sure the Content is really not the same if hasSameContent()
-        * returns false should call getContent() and compare the Content objects directly.
-        *
-        * @since 1.32
-        *
-        * @param SlotRecord $other
-        * @return bool
-        */
-       public function hasSameContent( SlotRecord $other ) {
-               if ( $other === $this ) {
-                       return true;
-               }
-
-               if ( $this->getModel() !== $other->getModel() ) {
-                       return false;
-               }
-
-               if ( $this->hasAddress()
-                       && $other->hasAddress()
-                       && $this->getAddress() == $other->getAddress()
-               ) {
-                       return true;
-               }
-
-               if ( $this->getSize() !== $other->getSize() ) {
-                       return false;
-               }
-
-               if ( $this->getSha1() !== $other->getSha1() ) {
-                       return false;
-               }
-
-               return true;
-       }
-
-}
diff --git a/includes/Storage/SuppressedDataException.php b/includes/Storage/SuppressedDataException.php
deleted file mode 100644 (file)
index 24f16a6..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-/**
- * Exception representing a failure to look up a revision.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-/**
- * Exception raised in response to an audience check when attempting to
- * access suppressed information without permission.
- *
- * @since 1.31
- */
-class SuppressedDataException extends RevisionAccessException {
-
-}
index 1cc4288..d014503 100644 (file)
@@ -739,27 +739,18 @@ class InfoAction extends FormlessAction {
                                $dbrWatchlist = wfGetDB( DB_REPLICA, 'watchlist' );
                                $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
 
-                               if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                                        $tables = [ 'revision_actor_temp' ];
                                        $field = 'revactor_actor';
                                        $pageField = 'revactor_page';
                                        $tsField = 'revactor_timestamp';
                                        $joins = [];
-                               } elseif ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+                               } else {
                                        $tables = [ 'revision' ];
                                        $field = 'rev_user_text';
                                        $pageField = 'rev_page';
                                        $tsField = 'rev_timestamp';
                                        $joins = [];
-                               } else {
-                                       $tables = [ 'revision', 'revision_actor_temp', 'actor' ];
-                                       $field = 'COALESCE( actor_name, rev_user_text)';
-                                       $pageField = 'rev_page';
-                                       $tsField = 'rev_timestamp';
-                                       $joins = [
-                                               'revision_actor_temp' => [ 'LEFT JOIN', 'revactor_rev = rev_id' ],
-                                               'actor' => [ 'LEFT JOIN', 'revactor_actor = actor_id' ],
-                                       ];
                                }
 
                                $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
index 6309362..12fd4f4 100644 (file)
@@ -6,9 +6,9 @@
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 
 /**
  * Temporary action for MCR undos
index ac07eeb..c2e37e0 100644 (file)
@@ -2247,7 +2247,7 @@ abstract class ApiBase extends ContextSource {
 
                // Avoid PHP 7.1 warning of passing $this by reference
                $apiModule = $this;
-               Hooks::run( 'APIGetDescription', [ &$apiModule, &$desc ] );
+               Hooks::run( 'APIGetDescription', [ &$apiModule, &$desc ], '1.25' );
                $desc = self::escapeWikiText( $desc );
                if ( is_array( $desc ) ) {
                        $desc = implode( "\n", $desc );
@@ -2337,7 +2337,7 @@ abstract class ApiBase extends ContextSource {
 
                // Avoid PHP 7.1 warning of passing $this by reference
                $apiModule = $this;
-               Hooks::run( 'APIGetParamDescription', [ &$apiModule, &$desc ] );
+               Hooks::run( 'APIGetParamDescription', [ &$apiModule, &$desc ], '1.25' );
 
                if ( !$desc ) {
                        $desc = [];
index c5a2234..76b7bce 100644 (file)
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
 
 class ApiComparePages extends ApiBase {
 
index 2b2b32c..5bf8da9 100644 (file)
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
 
 /**
  * @ingroup API
index 14491da..8f2e759 100644 (file)
@@ -130,7 +130,7 @@ class ApiLogin extends ApiBase {
                                $session = $status->getValue();
                                $authRes = 'Success';
                                $loginType = 'BotPassword';
-                       } elseif ( !$botLoginData[2] ||
+                       } elseif (
                                $status->hasMessage( 'login-throttled' ) ||
                                $status->hasMessage( 'botpasswords-needs-reset' ) ||
                                $status->hasMessage( 'botpasswords-locked' )
@@ -141,6 +141,7 @@ class ApiLogin extends ApiBase {
                                        'BotPassword login failed: ' . $status->getWikiText( false, false, 'en' )
                                );
                        }
+                       // For other errors, let's see if it's a valid non-bot login
                }
 
                if ( $authRes === false ) {
@@ -220,15 +221,15 @@ class ApiLogin extends ApiBase {
                                );
                                break;
 
+                       // @codeCoverageIgnoreStart
+                       // Unreachable
                        default:
                                ApiBase::dieDebug( __METHOD__, "Unhandled case value: {$authRes}" );
+                       // @codeCoverageIgnoreEnd
                }
 
                $this->getResult()->addValue( null, 'login', $result );
 
-               if ( $loginType === 'LoginForm' && isset( LoginForm::$statusCodes[$authRes] ) ) {
-                       $authRes = LoginForm::$statusCodes[$authRes];
-               }
                LoggerFactory::getInstance( 'authevents' )->info( 'Login attempt', [
                        'event' => 'login',
                        'successful' => $authRes === 'Success',
index bae6885..7b4f15e 100644 (file)
@@ -24,8 +24,8 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
 
 /**
  * Query module to enumerate all deleted revisions.
index 833e2e4..6a26eff 100644 (file)
@@ -21,7 +21,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 
 /**
  * Query module to enumerate all revisions.
index 9652f81..7d5f6e2 100644 (file)
@@ -182,19 +182,12 @@ class ApiQueryAllUsers extends ApiQueryBase {
                        // Actually count the actions using a subquery (T66505 and T66507)
                        $tables = [ 'recentchanges' ];
                        $joins = [];
-                       if ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD ) {
                                $userCond = 'rc_user_text = user_name';
                        } else {
                                $tables[] = 'actor';
-                               $joins['actor'] = [
-                                       $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
-                                       'rc_actor = actor_id'
-                               ];
-                               if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
-                                       $userCond = 'actor_user = user_id';
-                               } else {
-                                       $userCond = 'actor_user = user_id OR (rc_actor = 0 AND rc_user_text = user_name)';
-                               }
+                               $joins['actor'] = [ 'JOIN', 'rc_actor = actor_id' ];
+                               $userCond = 'actor_user = user_id';
                        }
                        $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds );
                        $this->addFields( [
index e39afac..642c9ac 100644 (file)
@@ -24,7 +24,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 
 /**
  * A query module to show contributors to a page
@@ -80,12 +80,13 @@ class ApiQueryContributors extends ApiQueryBase {
                $result = $this->getResult();
                $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo();
 
-               // For MIGRATION_NEW, target indexes on the revision_actor_temp table.
-               // Otherwise, revision is fine because it'll have to check all revision rows anyway.
-               $pageField = $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'revactor_page' : 'rev_page';
-               $idField = $wgActorTableSchemaMigrationStage === MIGRATION_NEW
+               // For SCHEMA_COMPAT_READ_NEW, target indexes on the
+               // revision_actor_temp table, otherwise on the revision table.
+               $pageField = ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW )
+                       ? 'revactor_page' : 'rev_page';
+               $idField = ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW )
                        ? 'revactor_actor' : $revQuery['fields']['rev_user'];
-               $countField = $wgActorTableSchemaMigrationStage === MIGRATION_NEW
+               $countField = ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW )
                        ? 'revactor_actor' : $revQuery['fields']['rev_user_text'];
 
                // First, count anons
index 47b746a..8f71c1c 100644 (file)
@@ -24,8 +24,8 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
 
 /**
  * Query module to enumerate deleted revisions for pages.
index a6a3251..8e464d0 100644 (file)
@@ -24,7 +24,7 @@
  * @file
  */
 
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 
 /**
  * Query module to enumerate all deleted files.
index 01b9c9a..b1dcf0d 100644 (file)
@@ -21,8 +21,8 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
 
 /**
  * A query action to enumerate the recent changes that were done to the wiki.
index 9109a5e..b8a180f 100644 (file)
@@ -21,8 +21,8 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
 
 /**
  * A query action to enumerate revisions of a given page, or show top revisions
index e5d7748..c9f528c 100644 (file)
@@ -20,9 +20,9 @@
  * @file
  */
 
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\MediaWikiServices;
 
 /**
index 75670dd..f16f958 100644 (file)
@@ -21,8 +21,8 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
 
 /**
  * This query action adds a list of a specified user's contributions to the output.
@@ -102,10 +102,8 @@ class ApiQueryUserContribs extends ApiQueryBase {
                                        $from = $fromName ? "$op= " . $dbSecondary->addQuotes( $fromName ) : false;
 
                                        // For the new schema, pull from the actor table. For the
-                                       // old, pull from rev_user. For migration a FULL [OUTER]
-                                       // JOIN would be what we want, except MySQL doesn't support
-                                       // that so we have to UNION instead.
-                                       if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                                       // old, pull from rev_user.
+                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                                                $res = $dbSecondary->select(
                                                        'actor',
                                                        [ 'actor_id', 'user_id' => 'COALESCE(actor_user,0)', 'user_name' => 'actor_name' ],
@@ -113,7 +111,7 @@ class ApiQueryUserContribs extends ApiQueryBase {
                                                        $fname,
                                                        [ 'ORDER BY' => [ "user_name $sort" ], 'LIMIT' => $limit ]
                                                );
-                                       } elseif ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+                                       } else {
                                                $res = $dbSecondary->select(
                                                        'revision',
                                                        [ 'actor_id' => 'NULL', 'user_id' => 'rev_user', 'user_name' => 'rev_user_text' ],
@@ -121,46 +119,6 @@ class ApiQueryUserContribs extends ApiQueryBase {
                                                        $fname,
                                                        [ 'DISTINCT', 'ORDER BY' => [ "rev_user_text $sort" ], 'LIMIT' => $limit ]
                                                );
-                                       } else {
-                                               // There are three queries we have to combine to be sure of getting all results:
-                                               //  - actor table (any rows that have been migrated will have empty rev_user_text)
-                                               //  - revision+actor by user id
-                                               //  - revision+actor by name for anons
-                                               $options = $dbSecondary->unionSupportsOrderAndLimit()
-                                                       ? [ 'ORDER BY' => [ "user_name $sort" ], 'LIMIT' => $limit ] : [];
-                                               $subsql = [];
-                                               $subsql[] = $dbSecondary->selectSQLText(
-                                                       'actor',
-                                                       [ 'actor_id', 'user_id' => 'COALESCE(actor_user,0)', 'user_name' => 'actor_name' ],
-                                                       array_merge( [ "actor_name$like" ], $from ? [ "actor_name $from" ] : [] ),
-                                                       $fname,
-                                                       $options
-                                               );
-                                               $subsql[] = $dbSecondary->selectSQLText(
-                                                       [ 'revision', 'actor' ],
-                                                       [ 'actor_id', 'user_id' => 'rev_user', 'user_name' => 'rev_user_text' ],
-                                                       array_merge(
-                                                               [ "rev_user_text$like", 'rev_user != 0' ],
-                                                               $from ? [ "rev_user_text $from" ] : []
-                                                       ),
-                                                       $fname,
-                                                       array_merge( [ 'DISTINCT' ], $options ),
-                                                       [ 'actor' => [ 'LEFT JOIN', 'rev_user = actor_user' ] ]
-                                               );
-                                               $subsql[] = $dbSecondary->selectSQLText(
-                                                       [ 'revision', 'actor' ],
-                                                       [ 'actor_id', 'user_id' => 'rev_user', 'user_name' => 'rev_user_text' ],
-                                                       array_merge(
-                                                               [ "rev_user_text$like", 'rev_user = 0' ],
-                                                               $from ? [ "rev_user_text $from" ] : []
-                                                       ),
-                                                       $fname,
-                                                       array_merge( [ 'DISTINCT' ], $options ),
-                                                       [ 'actor' => [ 'LEFT JOIN', 'rev_user_text = actor_name' ] ]
-                                               );
-                                               $sql = $dbSecondary->unionQueries( $subsql, false ) . " ORDER BY user_name $sort";
-                                               $sql = $dbSecondary->limitResult( $sql, $limit );
-                                               $res = $dbSecondary->query( $sql, $fname );
                                        }
 
                                        $count = 0;
@@ -205,9 +163,8 @@ class ApiQueryUserContribs extends ApiQueryBase {
                        }
 
                        // For the new schema, just select from the actor table. For the
-                       // old and transitional schemas, select from user and left join
-                       // actor if it exists.
-                       if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                       // old, select from user.
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                                $res = $dbSecondary->select(
                                        'actor',
                                        [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ],
@@ -215,7 +172,7 @@ class ApiQueryUserContribs extends ApiQueryBase {
                                        __METHOD__,
                                        [ 'ORDER BY' => "user_id $sort" ]
                                );
-                       } elseif ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+                       } else {
                                $res = $dbSecondary->select(
                                        'user',
                                        [ 'actor_id' => 'NULL', 'user_id' => 'user_id', 'user_name' => 'user_name' ],
@@ -223,15 +180,6 @@ class ApiQueryUserContribs extends ApiQueryBase {
                                        __METHOD__,
                                        [ 'ORDER BY' => "user_id $sort" ]
                                );
-                       } else {
-                               $res = $dbSecondary->select(
-                                       [ 'user', 'actor' ],
-                                       [ 'actor_id', 'user_id', 'user_name' ],
-                                       array_merge( [ 'user_id' => $ids ], $from ? [ "user_id $from" ] : [] ),
-                                       __METHOD__,
-                                       [ 'ORDER BY' => "user_id $sort" ],
-                                       [ 'actor' => [ 'LEFT JOIN', 'actor_user = user_id' ] ]
-                               );
                        }
                        $userIter = UserArray::newFromResult( $res );
                        $batchSize = count( $ids );
@@ -278,9 +226,8 @@ class ApiQueryUserContribs extends ApiQueryBase {
                        }
 
                        // For the new schema, just select from the actor table. For the
-                       // old and transitional schemas, select from user and left join
-                       // actor if it exists then merge in any unknown users (IPs and imports).
-                       if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                       // old, select from user then merge in any unknown users (IPs and imports).
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                                $res = $dbSecondary->select(
                                        'actor',
                                        [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ],
@@ -290,23 +237,12 @@ class ApiQueryUserContribs extends ApiQueryBase {
                                );
                                $userIter = UserArray::newFromResult( $res );
                        } else {
-                               if ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
-                                       $res = $dbSecondary->select(
-                                               'user',
-                                               [ 'actor_id' => 'NULL', 'user_id', 'user_name' ],
-                                               array_merge( [ 'user_name' => array_keys( $names ) ], $from ? [ "user_name $from" ] : [] ),
-                                               __METHOD__
-                                       );
-                               } else {
-                                       $res = $dbSecondary->select(
-                                               [ 'user', 'actor' ],
-                                               [ 'actor_id', 'user_id', 'user_name' ],
-                                               array_merge( [ 'user_name' => array_keys( $names ) ], $from ? [ "user_name $from" ] : [] ),
-                                               __METHOD__,
-                                               [],
-                                               [ 'actor' => [ 'LEFT JOIN', 'actor_user = user_id' ] ]
-                                       );
-                               }
+                               $res = $dbSecondary->select(
+                                       'user',
+                                       [ 'actor_id' => 'NULL', 'user_id', 'user_name' ],
+                                       array_merge( [ 'user_name' => array_keys( $names ) ], $from ? [ "user_name $from" ] : [] ),
+                                       __METHOD__
+                               );
                                foreach ( $res as $row ) {
                                        $names[$row->user_name] = $row;
                                }
@@ -328,17 +264,8 @@ class ApiQueryUserContribs extends ApiQueryBase {
                        $batchSize = count( $names );
                }
 
-               // During migration, force ordering on the client side because we're
-               // having to combine multiple queries that would otherwise have
-               // different sort orders.
-               if ( $wgActorTableSchemaMigrationStage === MIGRATION_WRITE_BOTH ||
-                       $wgActorTableSchemaMigrationStage === MIGRATION_WRITE_NEW
-               ) {
-                       $batchSize = 1;
-               }
-
                // With the new schema, the DB query will order by actor so update $this->orderBy to match.
-               if ( $batchSize > 1 && $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+               if ( $batchSize > 1 && ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ) {
                        $this->orderBy = 'actor';
                }
 
@@ -352,64 +279,22 @@ class ApiQueryUserContribs extends ApiQueryBase {
                                $userIter->next();
                        }
 
-                       // Ugh. We have to run the query three times, once for each
-                       // possible 'orcond' from ActorMigration, and then merge them all
-                       // together in the proper order. And preserving the correct
-                       // $hookData for each one.
-                       // @todo When ActorMigration is removed, this can go back to a
-                       //  single prepare and select.
-                       $merged = [];
-                       foreach ( [ 'actor', 'userid', 'username' ] as $which ) {
-                               if ( $this->prepareQuery( $users, $limit - $count, $which ) ) {
-                                       $hookData = [];
-                                       $res = $this->select( __METHOD__, [], $hookData );
-                                       foreach ( $res as $row ) {
-                                               $merged[] = [ $row, &$hookData ];
-                                       }
-                               }
-                       }
-                       $neg = $this->params['dir'] == 'newer' ? 1 : -1;
-                       usort( $merged, function ( $a, $b ) use ( $neg, $batchSize ) {
-                               if ( $batchSize === 1 ) { // One user, can't be different
-                                       $ret = 0;
-                               } elseif ( $this->orderBy === 'id' ) {
-                                       $ret = $a[0]->rev_user <=> $b[0]->rev_user;
-                               } elseif ( $this->orderBy === 'name' ) {
-                                       $ret = strcmp( $a[0]->rev_user_text, $b[0]->rev_user_text );
-                               } else {
-                                       $ret = $a[0]->rev_actor <=> $b[0]->rev_actor;
-                               }
-
-                               if ( !$ret ) {
-                                       $ret = strcmp(
-                                               wfTimestamp( TS_MW, $a[0]->rev_timestamp ),
-                                               wfTimestamp( TS_MW, $b[0]->rev_timestamp )
-                                       );
-                               }
-
-                               if ( !$ret ) {
-                                       $ret = $a[0]->rev_id <=> $b[0]->rev_id;
-                               }
-
-                               return $neg * $ret;
-                       } );
-                       $merged = array_slice( $merged, 0, $limit - $count + 1 );
-                       // (end "Ugh")
+                       $hookData = [];
+                       $this->prepareQuery( $users, $limit - $count );
+                       $res = $this->select( __METHOD__, [], $hookData );
 
                        if ( $this->fld_sizediff ) {
                                $revIds = [];
-                               foreach ( $merged as $data ) {
-                                       if ( $data[0]->rev_parent_id ) {
-                                               $revIds[] = $data[0]->rev_parent_id;
+                               foreach ( $res as $row ) {
+                                       if ( $row->rev_parent_id ) {
+                                               $revIds[] = $row->rev_parent_id;
                                        }
                                }
                                $this->parentLens = MediaWikiServices::getInstance()->getRevisionStore()
                                        ->listRevisionSizes( $dbSecondary, $revIds );
                        }
 
-                       foreach ( $merged as $data ) {
-                               $row = $data[0];
-                               $hookData = &$data[1];
+                       foreach ( $res as $row ) {
                                if ( ++$count > $limit ) {
                                        // We've reached the one extra which shows that there are
                                        // additional pages to be had. Stop here...
@@ -434,10 +319,8 @@ class ApiQueryUserContribs extends ApiQueryBase {
         * Prepares the query and returns the limit of rows requested
         * @param User[] $users
         * @param int $limit
-        * @param string $which 'actor', 'userid', or 'username'
-        * @return bool
         */
-       private function prepareQuery( array $users, $limit, $which ) {
+       private function prepareQuery( array $users, $limit ) {
                global $wgActorTableSchemaMigrationStage, $wgChangeTagsSchemaMigrationStage;
 
                $this->resetQueryParams();
@@ -448,27 +331,26 @@ class ApiQueryUserContribs extends ApiQueryBase {
                $this->addJoinConds( $revQuery['joins'] );
                $this->addFields( $revQuery['fields'] );
 
-               $revWhere = ActorMigration::newMigration()->getWhere( $db, 'rev_user', $users );
-               if ( !isset( $revWhere['orconds'][$which] ) ) {
-                       return false;
-               }
-               $this->addWhere( $revWhere['orconds'][$which] );
-
-               if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
+                       $revWhere = ActorMigration::newMigration()->getWhere( $db, 'rev_user', $users );
                        $orderUserField = 'rev_actor';
                        $userField = $this->orderBy === 'actor' ? 'revactor_actor' : 'actor_name';
-               } else {
-                       $orderUserField = $this->orderBy === 'id' ? 'rev_user' : 'rev_user_text';
-                       $userField = $revQuery['fields'][$orderUserField];
-               }
-               if ( $which === 'actor' ) {
                        $tsField = 'revactor_timestamp';
                        $idField = 'revactor_rev';
                } else {
+                       // If we're dealing with user names (rather than IDs) in read-old mode,
+                       // pass false for ActorMigration::getWhere()'s $useId parameter so
+                       // $revWhere['conds'] isn't an OR.
+                       $revWhere = ActorMigration::newMigration()
+                               ->getWhere( $db, 'rev_user', $users, $this->orderBy === 'id' );
+                       $orderUserField = $this->orderBy === 'id' ? 'rev_user' : 'rev_user_text';
+                       $userField = $revQuery['fields'][$orderUserField];
                        $tsField = 'rev_timestamp';
                        $idField = 'rev_id';
                }
 
+               $this->addWhere( $revWhere['conds'] );
+
                // Handle continue parameter
                if ( !is_null( $this->params['continue'] ) ) {
                        $continue = explode( '|', $this->params['continue'] );
@@ -620,8 +502,6 @@ class ApiQueryUserContribs extends ApiQueryBase {
                                $this->addWhereFld( 'ct_tag', $this->params['tag'] );
                        }
                }
-
-               return true;
        }
 
        /**
index 5dd247a..95fbd3d 100644 (file)
@@ -21,7 +21,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 
 /**
  * This query action allows clients to retrieve a list of recently modified pages
index 6121c3d..c636ba1 100644 (file)
@@ -21,7 +21,7 @@
  * @since 1.23
  */
 
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 
 /**
  * API interface to RevDel. The API equivalent of Special:RevisionDelete.
index e0c7a28..82cf986 100644 (file)
@@ -20,7 +20,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Revision\RevisionStore;
 
 /**
  * @ingroup API
index e8a276c..ad88564 100644 (file)
@@ -56,7 +56,7 @@ class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvi
                }
 
                $msg = null;
-               if ( !\Hooks::run( 'LoginUserMigrated', [ $user, &$msg ] ) ) {
+               if ( !\Hooks::run( 'LoginUserMigrated', [ $user, &$msg ], '1.27' ) ) {
                        return $this->makeFailResponse(
                                $user, LoginForm::USER_MIGRATED, $msg, 'LoginUserMigrated'
                        );
@@ -64,7 +64,7 @@ class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvi
 
                $abort = LoginForm::ABORTED;
                $msg = null;
-               if ( !\Hooks::run( 'AbortLogin', [ $user, $password, &$abort, &$msg ] ) ) {
+               if ( !\Hooks::run( 'AbortLogin', [ $user, $password, &$abort, &$msg ], '1.27' ) ) {
                        return $this->makeFailResponse( $user, $abort, $msg, 'AbortLogin' );
                }
 
@@ -74,7 +74,7 @@ class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvi
        public function testForAccountCreation( $user, $creator, array $reqs ) {
                $abortError = '';
                $abortStatus = null;
-               if ( !\Hooks::run( 'AbortNewAccount', [ $user, &$abortError, &$abortStatus ] ) ) {
+               if ( !\Hooks::run( 'AbortNewAccount', [ $user, &$abortError, &$abortStatus ], '1.27' ) ) {
                        // Hook point to add extra creation throttles and blocks
                        $this->logger->debug( __METHOD__ . ': a hook blocked creation' );
                        if ( $abortStatus === null ) {
@@ -99,7 +99,7 @@ class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvi
        public function testUserForCreation( $user, $autocreate, array $options = [] ) {
                if ( $autocreate !== false ) {
                        $abortError = '';
-                       if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortError ] ) ) {
+                       if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortError ], '1.27' ) ) {
                                // Hook point to add extra creation throttles and blocks
                                $this->logger->debug( __METHOD__ . ": a hook blocked auto-creation: $abortError\n" );
                                return $this->makeFailResponse(
index 2828b9a..8f816d9 100644 (file)
@@ -105,11 +105,13 @@ class UserCache {
                        $fields = [ 'user_name', 'user_real_name', 'user_registration', 'user_id' ];
                        $joinConds = [];
 
-                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW,
+                       // but it does little harm and might be needed for write callers loading a User.
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) {
                                $tables[] = 'actor';
                                $fields[] = 'actor_id';
                                $joinConds['actor'] = [
-                                       $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+                                       ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ? 'JOIN' : 'LEFT JOIN',
                                        [ 'actor_user = user_id' ]
                                ];
                        }
@@ -125,7 +127,7 @@ class UserCache {
                                $this->cache[$userId]['name'] = $row->user_name;
                                $this->cache[$userId]['real_name'] = $row->user_real_name;
                                $this->cache[$userId]['registration'] = $row->user_registration;
-                               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) {
                                        $this->cache[$userId]['actor'] = $row->actor_id;
                                }
                                $usersToCheck[$userId] = $row->user_name;
index 819f170..7f7d77d 100644 (file)
@@ -228,13 +228,14 @@ class RecentChange {
                global $wgActorTableSchemaMigrationStage;
 
                wfDeprecated( __METHOD__, '1.31' );
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                        // If code is using this instead of self::getQueryInfo(), there's a
                        // decent chance it's going to try to directly access
                        // $row->rc_user or $row->rc_user_text and we can't give it
-                       // useful values here once those aren't being written anymore.
+                       // useful values here once those aren't being used anymore.
                        throw new BadMethodCallException(
-                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                               'Cannot use ' . __METHOD__
+                                       . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW'
                        );
                }
 
index 876b9bb..6d921b9 100644 (file)
 
 use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DatabaseDomain;
 use Wikimedia\Rdbms\Blob;
 use Wikimedia\Rdbms\ResultWrapper;
 use Wikimedia\Rdbms\DBConnectionError;
 use Wikimedia\Rdbms\DBUnexpectedError;
+use Wikimedia\Rdbms\DBExpectedError;
 
 /**
  * @ingroup Database
@@ -81,8 +83,9 @@ class DatabaseOracle extends Database {
                return false;
        }
 
-       function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                global $wgDBOracleDRCP;
+
                if ( !function_exists( 'oci_connect' ) ) {
                        throw new DBConnectionError(
                                $this,
@@ -94,20 +97,15 @@ class DatabaseOracle extends Database {
                $this->close();
                $this->user = $user;
                $this->password = $password;
-               // changed internal variables functions
-               // mServer now holds the TNS endpoint
-               // mDBname is schema name if different from username
                if ( !$server ) {
-                       // backward compatibillity (server used to be null and TNS was supplied in dbname)
+                       // Backward compatibility (server used to be null and TNS was supplied in dbname)
                        $this->server = $dbName;
-                       $this->dbName = $user;
+                       $realDatabase = $user;
                } else {
+                       // $server now holds the TNS endpoint
                        $this->server = $server;
-                       if ( !$dbName ) {
-                               $this->dbName = $user;
-                       } else {
-                               $this->dbName = $dbName;
-                       }
+                       // $dbName is schema name if different from username
+                       $realDatabase = $dbName ?: $user;
                }
 
                if ( !strlen( $user ) ) { # e.g. the class is being loaded
@@ -148,9 +146,15 @@ class DatabaseOracle extends Database {
                }
                Wikimedia\restoreWarnings();
 
-               if ( $this->user != $this->dbName ) {
+               if ( $this->user != $realDatabase ) {
                        // change current schema in session
-                       $this->selectDB( $this->dbName );
+                       $this->selectDB( $realDatabase );
+               } else {
+                       $this->currentDomain = new DatabaseDomain(
+                               $realDatabase,
+                               null,
+                               $tablePrefix
+                       );
                }
 
                if ( !$this->conn ) {
@@ -654,8 +658,8 @@ class DatabaseOracle extends Database {
                                        atc.table_name
                                ) || '_' ||
                                atc.column_name || '_SEQ' = '{$this->tablePrefix}' || asq.sequence_name
-                               AND asq.sequence_owner = upper('{$this->dbName}')
-                               AND atc.owner = upper('{$this->dbName}')" );
+                               AND asq.sequence_owner = upper('{$this->getDBname()}')
+                               AND atc.owner = upper('{$this->getDBname()}')" );
 
                        while ( ( $row = $result->fetchRow() ) !== false ) {
                                $this->sequenceData[$row[1]] = [
@@ -735,7 +739,7 @@ class DatabaseOracle extends Database {
                        $listWhere = ' AND table_name LIKE \'' . strtoupper( $prefix ) . '%\'';
                }
 
-               $owner = strtoupper( $this->dbName );
+               $owner = strtoupper( $this->getDBname() );
                $result = $this->doQuery( "SELECT table_name FROM all_tables " .
                        "WHERE owner='$owner' AND table_name NOT LIKE '%!_IDX\$_' ESCAPE '!' $listWhere" );
 
@@ -813,7 +817,7 @@ class DatabaseOracle extends Database {
                $table = $this->tableName( $table );
                $table = strtoupper( $this->removeIdentifierQuotes( $table ) );
                $index = strtoupper( $index );
-               $owner = strtoupper( $this->dbName );
+               $owner = strtoupper( $this->getDBname() );
                $sql = "SELECT 1 FROM all_indexes WHERE owner='$owner' AND index_name='{$table}_{$index}'";
                $res = $this->doQuery( $sql );
                if ( $res ) {
@@ -835,7 +839,7 @@ class DatabaseOracle extends Database {
        function tableExists( $table, $fname = __METHOD__ ) {
                $table = $this->tableName( $table );
                $table = $this->addQuotes( strtoupper( $this->removeIdentifierQuotes( $table ) ) );
-               $owner = $this->addQuotes( strtoupper( $this->dbName ) );
+               $owner = $this->addQuotes( strtoupper( $this->getDBname() ) );
                $sql = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table";
                $res = $this->doQuery( $sql );
                if ( $res && $res->numRows() > 0 ) {
@@ -1031,23 +1035,33 @@ class DatabaseOracle extends Database {
                return true;
        }
 
-       function selectDB( $db ) {
-               $this->dbName = $db;
-               if ( $db == null || $db == $this->user ) {
+       protected function doSelectDomain( DatabaseDomain $domain ) {
+               if ( $domain->getSchema() !== null ) {
+                       // We use the *database* aspect of $domain for schema, not the domain schema
+                       throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+               }
+
+               $database = $domain->getDatabase();
+               if ( $database === null || $database === $this->user ) {
+                       // Backward compatibility
+                       $this->currentDomain = $domain;
+
                        return true;
                }
-               $sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper( $db );
+
+               // https://docs.oracle.com/javadb/10.8.3.0/ref/rrefsqlj32268.html
+               $encDatabase = $this->addIdentifierQuotes( strtoupper( $database ) );
+               $sql = "ALTER SESSION SET CURRENT_SCHEMA=$encDatabase";
                $stmt = oci_parse( $this->conn, $sql );
                Wikimedia\suppressWarnings();
                $success = oci_execute( $stmt );
                Wikimedia\restoreWarnings();
-               if ( !$success ) {
+               if ( $success ) {
+                       // Update that domain fields on success (no exception thrown)
+                       $this->currentDomain = $domain;
+               } else {
                        $e = oci_error( $stmt );
-                       if ( $e['code'] != '1435' ) {
-                               $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
-                       }
-
-                       return false;
+                       $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
                }
 
                return true;
@@ -1179,7 +1193,9 @@ class DatabaseOracle extends Database {
                // all deletions on these tables have transactions so final failure rollbacks these updates
                // @todo: Normalize the schema to match MySQL, no special FKs and such
                $table = $this->tableName( $table );
-               if ( $table == $this->tableName( 'user' ) && $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+               if ( $table == $this->tableName( 'user' ) &&
+                       ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD )
+               ) {
                        $this->update( 'archive', [ 'ar_user' => 0 ],
                                [ 'ar_user' => $conds['user_id'] ], $fname );
                        $this->update( 'ipblocks', [ 'ipb_user' => 0 ],
@@ -1332,10 +1348,6 @@ class DatabaseOracle extends Database {
                return 'BITOR(' . $fieldLeft . ', ' . $fieldRight . ')';
        }
 
-       function getDBname() {
-               return $this->dbName;
-       }
-
        function getServer() {
                return $this->server;
        }
index 936f6bf..f8f3d1c 100644 (file)
@@ -21,8 +21,8 @@
  * @ingroup DifferenceEngine
  */
 
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 
 /**
  * DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
index 5589c68..4a84cff 100644 (file)
@@ -221,13 +221,14 @@ class ArchivedFile {
        static function selectFields() {
                global $wgActorTableSchemaMigrationStage;
 
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                        // If code is using this instead of self::getQueryInfo(), there's a
                        // decent chance it's going to try to directly access
                        // $row->fa_user or $row->fa_user_text and we can't give it
-                       // useful values here once those aren't being written anymore.
+                       // useful values here once those aren't being used anymore.
                        throw new BadMethodCallException(
-                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                               'Cannot use ' . __METHOD__
+                                       . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW'
                        );
                }
 
@@ -248,7 +249,7 @@ class ArchivedFile {
                        'fa_minor_mime',
                        'fa_user',
                        'fa_user_text',
-                       'fa_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'fa_actor' : 'NULL',
+                       'fa_actor' => 'NULL',
                        'fa_timestamp',
                        'fa_deleted',
                        'fa_deleted_timestamp', /* Used by LocalFileRestoreBatch */
index ec01869..254ceff 100644 (file)
@@ -201,13 +201,14 @@ class LocalFile extends File {
                global $wgActorTableSchemaMigrationStage;
 
                wfDeprecated( __METHOD__, '1.31' );
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                        // If code is using this instead of self::getQueryInfo(), there's a
                        // decent chance it's going to try to directly access
                        // $row->img_user or $row->img_user_text and we can't give it
-                       // useful values here once those aren't being written anymore.
+                       // useful values here once those aren't being used anymore.
                        throw new BadMethodCallException(
-                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                               'Cannot use ' . __METHOD__
+                                       . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW'
                        );
                }
 
@@ -223,7 +224,7 @@ class LocalFile extends File {
                        'img_minor_mime',
                        'img_user',
                        'img_user_text',
-                       'img_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'img_actor' : 'NULL',
+                       'img_actor' => 'NULL',
                        'img_timestamp',
                        'img_sha1',
                ] + MediaWikiServices::getInstance()->getCommentStore()->getFields( 'img_description' );
@@ -1573,16 +1574,16 @@ class LocalFile extends File {
                                }
                        }
 
-                       if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                $fields['oi_user'] = 'img_user';
                                $fields['oi_user_text'] = 'img_user_text';
                        }
-                       if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                $fields['oi_actor'] = 'img_actor';
                        }
 
-                       if ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD &&
-                               $wgActorTableSchemaMigrationStage !== MIGRATION_NEW
+                       if (
+                               ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_BOTH ) === SCHEMA_COMPAT_WRITE_BOTH
                        ) {
                                // Upgrade any rows that are still old-style. Otherwise an upgrade
                                // might be missed if a deletion happens while the migration script
@@ -2569,16 +2570,16 @@ class LocalFileDeleteBatch {
                                }
                        }
 
-                       if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                $fields['fa_user'] = 'img_user';
                                $fields['fa_user_text'] = 'img_user_text';
                        }
-                       if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                $fields['fa_actor'] = 'img_actor';
                        }
 
-                       if ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD &&
-                               $wgActorTableSchemaMigrationStage !== MIGRATION_NEW
+                       if (
+                               ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_BOTH ) === SCHEMA_COMPAT_WRITE_BOTH
                        ) {
                                // Upgrade any rows that are still old-style. Otherwise an upgrade
                                // might be missed if a deletion happens while the migration script
index 9759b79..f103afa 100644 (file)
@@ -115,13 +115,14 @@ class OldLocalFile extends LocalFile {
                global $wgActorTableSchemaMigrationStage;
 
                wfDeprecated( __METHOD__, '1.31' );
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                        // If code is using this instead of self::getQueryInfo(), there's a
                        // decent chance it's going to try to directly access
                        // $row->oi_user or $row->oi_user_text and we can't give it
-                       // useful values here once those aren't being written anymore.
+                       // useful values here once those aren't being used anymore.
                        throw new BadMethodCallException(
-                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                               'Cannot use ' . __METHOD__
+                                       . ' when $wgActorTableSchemaMigrationStage has SCHEMA_COMPAT_READ_NEW'
                        );
                }
 
@@ -138,7 +139,7 @@ class OldLocalFile extends LocalFile {
                        'oi_minor_mime',
                        'oi_user',
                        'oi_user_text',
-                       'oi_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'oi_actor' : 'NULL',
+                       'oi_actor' => 'NULL',
                        'oi_timestamp',
                        'oi_deleted',
                        'oi_sha1',
index a98f112..c0dbf50 100644 (file)
@@ -83,7 +83,8 @@ class HTMLInfoField extends HTMLFormField {
        public function getOOUI( $value ) {
                if ( !empty( $this->mParams['rawrow'] ) ) {
                        if ( !( $value instanceof OOUI\FieldLayout ) ) {
-                               throw new Exception( "'default' must be a FieldLayout or subclass when using 'rawrow'" );
+                               wfDeprecated( "'default' parameter as a string when using 'rawrow' " .
+                                       "(must be a FieldLayout or subclass)", '1.32' );
                        }
                        return $value;
                }
index 8634f89..0194822 100644 (file)
@@ -1300,7 +1300,7 @@ abstract class DatabaseUpdater {
         */
        protected function migrateActors() {
                global $wgActorTableSchemaMigrationStage;
-               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_NEW &&
+               if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) &&
                        !$this->updateRowExists( 'MigrateActors' )
                ) {
                        $this->output(
diff --git a/includes/jobqueue/jobs/DeletePageJob.php b/includes/jobqueue/jobs/DeletePageJob.php
new file mode 100644 (file)
index 0000000..9b5cef4
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * Class DeletePageJob
+ */
+class DeletePageJob extends Job {
+       public function __construct( $title, $params ) {
+               parent::__construct( 'deletePage', $title, $params );
+       }
+
+       /**
+        * Execute the job
+        *
+        * @return bool
+        */
+       public function run() {
+               // Failure to load the page is not job failure.
+               // A parallel deletion operation may have already completed the page deletion.
+               $wikiPage = WikiPage::newFromID( $this->params['wikiPageId'] );
+               if ( $wikiPage ) {
+                       $wikiPage->doDeleteArticleBatched(
+                               $this->params['reason'],
+                               $this->params['suppress'],
+                               User::newFromId( $this->params['userId'] ),
+                               json_decode( $this->params['tags'] ),
+                               $this->params['logsubtype'],
+                               false,
+                               $this->getRequestId() );
+               }
+               return true;
+       }
+}
index 66f54b9..3488eb6 100644 (file)
@@ -21,7 +21,7 @@
  * @ingroup JobQueue
  */
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 
 /**
  * Job to update link tables for pages
index ba251ba..f693dd5 100644 (file)
@@ -375,6 +375,11 @@ class DBConnRef implements IDatabase {
                throw new DBUnexpectedError( $this, "Database selection is disallowed to enable reuse." );
        }
 
+       public function selectDomain( $domain ) {
+               // Disallow things that might confuse the LoadBalancer tracking
+               throw new DBUnexpectedError( $this, "Database selection is disallowed to enable reuse." );
+       }
+
        public function getDBname() {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
index a091242..91cb881 100644 (file)
@@ -81,8 +81,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected $user;
        /** @var string Password used to establish the current connection */
        protected $password;
-       /** @var string Database that this instance is currently connected to */
-       protected $dbName;
        /** @var array[] Map of (table => (dbname, schema, prefix) map) */
        protected $tableAliases = [];
        /** @var string[] Map of (index alias => index) */
@@ -120,10 +118,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var bool Whether to suppress triggering of transaction end callbacks */
        protected $trxEndCallbacksSuppressed = false;
 
-       /** @var string */
-       protected $tablePrefix = '';
-       /** @var string */
-       protected $schema = '';
        /** @var int */
        protected $flags;
        /** @var array */
@@ -291,13 +285,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @param array $params Parameters passed from Database::factory()
         */
        protected function __construct( array $params ) {
-               foreach ( [ 'host', 'user', 'password', 'dbname' ] as $name ) {
+               foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
                        $this->connectionParams[$name] = $params[$name];
                }
 
-               $this->schema = $params['schema'];
-               $this->tablePrefix = $params['tablePrefix'];
-
                $this->cliMode = $params['cliMode'];
                // Agent name is added to SQL queries in a comment, so make sure it can't break out
                $this->agent = str_replace( '/', '-', $params['agent'] );
@@ -329,7 +320,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                // Set initial dummy domain until open() sets the final DB/prefix
-               $this->currentDomain = DatabaseDomain::newUnspecified();
+               $this->currentDomain = new DatabaseDomain(
+                       $params['dbname'] != '' ? $params['dbname'] : null,
+                       $params['schema'] != '' ? $params['schema'] : null,
+                       $params['tablePrefix']
+               );
        }
 
        /**
@@ -346,11 +341,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
                // Establish the connection
                $this->doInitConnection();
-               // Set the domain object after open() sets the relevant fields
-               if ( $this->dbName != '' ) {
-                       // Domains with server scope but a table prefix are not used by IDatabase classes
-                       $this->currentDomain = new DatabaseDomain( $this->dbName, null, $this->tablePrefix );
-               }
        }
 
        /**
@@ -366,7 +356,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->connectionParams['host'],
                                $this->connectionParams['user'],
                                $this->connectionParams['password'],
-                               $this->connectionParams['dbname']
+                               $this->connectionParams['dbname'],
+                               $this->connectionParams['schema'],
+                               $this->connectionParams['tablePrefix']
                        );
                } else {
                        throw new InvalidArgumentException( "No database user provided." );
@@ -380,10 +372,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @param string $user Database user name
         * @param string $password Database user password
         * @param string $dbName Database name
+        * @param string|null $schema Database schema name
+        * @param string $tablePrefix Table prefix
         * @return bool
         * @throws DBConnectionError
         */
-       abstract protected function open( $server, $user, $password, $dbName );
+       abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix );
 
        /**
         * Construct a Database subclass instance given a database type and parameters
@@ -441,7 +435,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $p['flags'] = $p['flags'] ?? 0;
                        $p['variables'] = $p['variables'] ?? [];
                        $p['tablePrefix'] = $p['tablePrefix'] ?? '';
-                       $p['schema'] = $p['schema'] ?? '';
+                       $p['schema'] = $p['schema'] ?? null;
                        $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
                        $p['agent'] = $p['agent'] ?? '';
                        if ( !isset( $p['connLogger'] ) ) {
@@ -599,24 +593,37 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function tablePrefix( $prefix = null ) {
-               $old = $this->tablePrefix;
+               $old = $this->currentDomain->getTablePrefix();
                if ( $prefix !== null ) {
-                       $this->tablePrefix = $prefix;
-                       $this->currentDomain = ( $this->dbName != '' )
-                               ? new DatabaseDomain( $this->dbName, null, $this->tablePrefix )
-                               : DatabaseDomain::newUnspecified();
+                       $this->currentDomain = new DatabaseDomain(
+                               $this->currentDomain->getDatabase(),
+                               $this->currentDomain->getSchema(),
+                               $prefix
+                       );
                }
 
                return $old;
        }
 
        public function dbSchema( $schema = null ) {
-               $old = $this->schema;
+               $old = $this->currentDomain->getSchema();
                if ( $schema !== null ) {
-                       $this->schema = $schema;
+                       $this->currentDomain = new DatabaseDomain(
+                               $this->currentDomain->getDatabase(),
+                               // DatabaseDomain uses null for unspecified schemas
+                               strlen( $schema ) ? $schema : null,
+                               $this->currentDomain->getTablePrefix()
+                       );
                }
 
-               return $old;
+               return (string)$old;
+       }
+
+       /**
+        * @return string Schema to use to qualify relations in queries
+        */
+       protected function relationSchemaQualifier() {
+               return $this->dbSchema();
        }
 
        public function getLBInfo( $name = null ) {
@@ -900,7 +907,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return array_merge(
                        [
                                'db_server' => $this->server,
-                               'db_name' => $this->dbName,
+                               'db_name' => $this->getDBname(),
                                'db_user' => $this->user,
                        ],
                        $extras
@@ -2287,17 +2294,26 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return false;
        }
 
-       public function selectDB( $db ) {
-               # Stub. Shouldn't cause serious problems if it's not overridden, but
-               # if your database engine supports a concept similar to MySQL's
-               # databases you may as well.
-               $this->dbName = $db;
+       final public function selectDB( $db ) {
+               $this->selectDomain( new DatabaseDomain(
+                       $db,
+                       $this->currentDomain->getSchema(),
+                       $this->currentDomain->getTablePrefix()
+               ) );
 
                return true;
        }
 
+       final public function selectDomain( $domain ) {
+               $this->doSelectDomain( DatabaseDomain::newFromId( $domain ) );
+       }
+
+       protected function doSelectDomain( DatabaseDomain $domain ) {
+               $this->currentDomain = $domain;
+       }
+
        public function getDBname() {
-               return $this->dbName;
+               return $this->currentDomain->getDatabase();
        }
 
        public function getServer() {
@@ -2382,14 +2398,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $database = $this->tableAliases[$table]['dbname'];
                                $schema = is_string( $this->tableAliases[$table]['schema'] )
                                        ? $this->tableAliases[$table]['schema']
-                                       : $this->schema;
+                                       : $this->relationSchemaQualifier();
                                $prefix = is_string( $this->tableAliases[$table]['prefix'] )
                                        ? $this->tableAliases[$table]['prefix']
-                                       : $this->tablePrefix;
+                                       : $this->tablePrefix();
                        } else {
                                $database = '';
-                               $schema = $this->schema; # Default schema
-                               $prefix = $this->tablePrefix; # Default prefix
+                               $schema = $this->relationSchemaQualifier(); # Default schema
+                               $prefix = $this->tablePrefix(); # Default prefix
                        }
                }
 
@@ -4109,7 +4125,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->opened = false;
                $this->conn = false;
                try {
-                       $this->open( $this->server, $this->user, $this->password, $this->dbName );
+                       $this->open(
+                               $this->server,
+                               $this->user,
+                               $this->password,
+                               $this->getDBname(),
+                               $this->dbSchema(),
+                               $this->tablePrefix()
+                       );
                        $this->lastPing = microtime( true );
                        $ok = true;
 
@@ -4643,7 +4666,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->conn = false;
                        $this->trxEndCallbacks = []; // don't copy
                        $this->handleSessionLoss(); // no trx or locks anymore
-                       $this->open( $this->server, $this->user, $this->password, $this->dbName );
+                       $this->open(
+                               $this->server,
+                               $this->user,
+                               $this->password,
+                               $this->getDBname(),
+                               $this->dbSchema(),
+                               $this->tablePrefix()
+                       );
                        $this->lastPing = microtime( true );
                }
        }
index 1246e44..61367f5 100644 (file)
@@ -77,7 +77,7 @@ class DatabaseMssql extends Database {
                parent::__construct( $params );
        }
 
-       protected function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                # Test for driver support, to avoid suppressed fatal error
                if ( !function_exists( 'sqlsrv_connect' ) ) {
                        throw new DBConnectionError(
@@ -96,11 +96,10 @@ class DatabaseMssql extends Database {
                $this->server = $server;
                $this->user = $user;
                $this->password = $password;
-               $this->dbName = $dbName;
 
                $connectionInfo = [];
 
-               if ( $dbName ) {
+               if ( $dbName != '' ) {
                        $connectionInfo['Database'] = $dbName;
                }
 
@@ -120,6 +119,11 @@ class DatabaseMssql extends Database {
                }
 
                $this->opened = true;
+               $this->currentDomain = new DatabaseDomain(
+                       ( $dbName != '' ) ? $dbName : null,
+                       null,
+                       $tablePrefix
+               );
 
                return (bool)$this->conn;
        }
@@ -1006,7 +1010,7 @@ class DatabaseMssql extends Database {
                }
 
                if ( $schema === false ) {
-                       $schema = $this->schema;
+                       $schema = $this->dbSchema();
                }
 
                $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES
@@ -1167,18 +1171,13 @@ class DatabaseMssql extends Database {
                        $s );
        }
 
-       /**
-        * @param string $db
-        * @return bool
-        */
-       public function selectDB( $db ) {
-               try {
-                       $this->dbName = $db;
-                       $this->query( "USE $db" );
-                       return true;
-               } catch ( Exception $e ) {
-                       return false;
-               }
+       protected function doSelectDomain( DatabaseDomain $domain ) {
+               $encDatabase = $this->addIdentifierQuotes( $domain->getDatabase() );
+               $this->query( "USE $encDatabase" );
+               // Update that domain fields on success (no exception thrown)
+               $this->currentDomain = $domain;
+
+               return true;
        }
 
        /**
@@ -1306,8 +1305,8 @@ class DatabaseMssql extends Database {
        private function populateColumnCaches() {
                $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
                        [
-                               'TABLE_CATALOG' => $this->dbName,
-                               'TABLE_SCHEMA' => $this->schema,
+                               'TABLE_CATALOG' => $this->getDBname(),
+                               'TABLE_SCHEMA' => $this->dbSchema(),
                                'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
                        ] );
 
index 4bd607c..cc9e98f 100644 (file)
@@ -120,18 +120,17 @@ abstract class DatabaseMysqlBase extends Database {
                return 'mysql';
        }
 
-       protected function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                # Close/unset connection handle
                $this->close();
 
                $this->server = $server;
                $this->user = $user;
                $this->password = $password;
-               $this->dbName = $dbName;
 
                $this->installErrorHandler();
                try {
-                       $this->conn = $this->mysqlConnect( $this->server );
+                       $this->conn = $this->mysqlConnect( $this->server, $dbName );
                } catch ( Exception $ex ) {
                        $this->restoreErrorHandler();
                        throw $ex;
@@ -156,20 +155,9 @@ abstract class DatabaseMysqlBase extends Database {
                }
 
                if ( strlen( $dbName ) ) {
-                       Wikimedia\suppressWarnings();
-                       $success = $this->selectDB( $dbName );
-                       Wikimedia\restoreWarnings();
-                       if ( !$success ) {
-                               $error = $this->lastError();
-                               $this->queryLogger->error(
-                                       "Error selecting database {db_name} on server {db_server}: {error}",
-                                       $this->getLogContext( [
-                                               'method' => __METHOD__,
-                                               'error' => $error,
-                                       ] )
-                               );
-                               throw new DBConnectionError( $this, "Error selecting database $dbName: $error" );
-                       }
+                       $this->selectDomain( new DatabaseDomain( $dbName, null, $tablePrefix ) );
+               } else {
+                       $this->currentDomain = new DatabaseDomain( null, null, $tablePrefix );
                }
 
                // Tell the server what we're communicating with
@@ -240,10 +228,11 @@ abstract class DatabaseMysqlBase extends Database {
         * Open a connection to a MySQL server
         *
         * @param string $realServer
+        * @param string|null $dbName
         * @return mixed Raw connection
         * @throws DBConnectionError
         */
-       abstract protected function mysqlConnect( $realServer );
+       abstract protected function mysqlConnect( $realServer, $dbName );
 
        /**
         * Set the character set of the MySQL link
@@ -1513,7 +1502,7 @@ abstract class DatabaseMysqlBase extends Database {
         */
        public function listViews( $prefix = null, $fname = __METHOD__ ) {
                // The name of the column containing the name of the VIEW
-               $propertyName = 'Tables_in_' . $this->dbName;
+               $propertyName = 'Tables_in_' . $this->getDBname();
 
                // Query for the VIEWS
                $res = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
index 6d9dabd..ad9b0a4 100644 (file)
@@ -53,10 +53,11 @@ class DatabaseMysqli extends DatabaseMysqlBase {
 
        /**
         * @param string $realServer
+        * @param string|null $dbName
         * @return bool|mysqli
         * @throws DBConnectionError
         */
-       protected function mysqlConnect( $realServer ) {
+       protected function mysqlConnect( $realServer, $dbName ) {
                # Avoid suppressed fatal error, which is very hard to track down
                if ( !function_exists( 'mysqli_init' ) ) {
                        throw new DBConnectionError( $this, "MySQLi functions missing,"
@@ -111,9 +112,15 @@ class DatabaseMysqli extends DatabaseMysqlBase {
                }
                $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, 3 );
 
-               if ( $mysqli->real_connect( $realServer, $this->user,
-                       $this->password, $this->dbName, $port, $socket, $connFlags )
-               ) {
+               if ( $mysqli->real_connect(
+                       $realServer,
+                       $this->user,
+                       $this->password,
+                       $dbName,
+                       $port,
+                       $socket,
+                       $connFlags
+               ) ) {
                        return $mysqli;
                }
 
@@ -177,16 +184,22 @@ class DatabaseMysqli extends DatabaseMysqlBase {
                return $conn->affected_rows;
        }
 
-       /**
-        * @param string $db
-        * @return bool
-        */
-       function selectDB( $db ) {
+       function doSelectDomain( DatabaseDomain $domain ) {
                $conn = $this->getBindingHandle();
 
-               $this->dbName = $db;
+               if ( $domain->getSchema() !== null ) {
+                       throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+               }
+
+               $database = $domain->getDatabase();
+               if ( !$conn->select_db( $database ) ) {
+                       throw new DBExpectedError( $this, "Could not select database '$database'." );
+               }
+
+               // Update that domain fields on success (no exception thrown)
+               $this->currentDomain = $domain;
 
-               return $conn->select_db( $db );
+               return true;
        }
 
        /**
index 691a4b7..e1cd764 100644 (file)
@@ -86,7 +86,7 @@ class DatabasePostgres extends Database {
                return false;
        }
 
-       protected function open( $server, $user, $password, $dbName ) {
+       protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                # Test for Postgres support, to avoid suppressed fatal error
                if ( !function_exists( 'pg_connect' ) ) {
                        throw new DBConnectionError(
@@ -100,7 +100,6 @@ class DatabasePostgres extends Database {
                $this->server = $server;
                $this->user = $user;
                $this->password = $password;
-               $this->dbName = $dbName;
 
                $connectVars = [
                        // pg_connect() user $user as the default database. Since a database is *required*,
@@ -157,30 +156,42 @@ class DatabasePostgres extends Database {
                $this->query( "SET standard_conforming_strings = on", __METHOD__ );
                $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127
 
-               $this->determineCoreSchema( $this->schema );
-               // The schema to be used is now in the search path; no need for explicit qualification
-               $this->schema = '';
+               $this->determineCoreSchema( $schema );
+               $this->currentDomain = new DatabaseDomain( $dbName, $schema, $tablePrefix );
 
-               return $this->conn;
+               return (bool)$this->conn;
+       }
+
+       protected function relationSchemaQualifier() {
+               if ( $this->coreSchema === $this->currentDomain->getSchema() ) {
+                       // The schema to be used is now in the search path; no need for explicit qualification
+                       return '';
+               }
+
+               return parent::relationSchemaQualifier();
        }
 
        public function databasesAreIndependent() {
                return true;
        }
 
-       /**
-        * Postgres doesn't support selectDB in the same way MySQL does. So if the
-        * DB name doesn't match the open connection, open a new one
-        * @param string $db
-        * @return bool
-        * @throws DBUnexpectedError
-        */
-       public function selectDB( $db ) {
-               if ( $this->dbName !== $db ) {
-                       return (bool)$this->open( $this->server, $this->user, $this->password, $db );
+       public function doSelectDomain( DatabaseDomain $domain ) {
+               if ( $this->getDBname() !== $domain->getDatabase() ) {
+                       // Postgres doesn't support selectDB in the same way MySQL does.
+                       // So if the DB name doesn't match the open connection, open a new one
+                       $this->open(
+                               $this->server,
+                               $this->user,
+                               $this->password,
+                               $domain->getDatabase(),
+                               $domain->getSchema(),
+                               $domain->getTablePrefix()
+                       );
                } else {
-                       return true;
+                       $this->currentDomain = $domain;
                }
+
+               return true;
        }
 
        /**
@@ -1320,10 +1331,6 @@ SQL;
                return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
        }
 
-       public function getDBname() {
-               return $this->dbName;
-       }
-
        public function getServer() {
                return $this->server;
        }
index 0e6240f..487e122 100644 (file)
@@ -71,6 +71,10 @@ class DatabaseSqlite extends Database {
                if ( isset( $p['dbFilePath'] ) ) {
                        $this->dbPath = $p['dbFilePath'];
                        $lockDomain = md5( $this->dbPath );
+                       // Use "X" for things like X.sqlite and ":memory:" for RAM-only DBs
+                       if ( !isset( $p['dbname'] ) || !strlen( $p['dbname'] ) ) {
+                               $p['dbname'] = preg_replace( '/\.sqlite\d?$/', '', basename( $this->dbPath ) );
+                       }
                } elseif ( isset( $p['dbDirectory'] ) ) {
                        $this->dbDir = $p['dbDirectory'];
                        $lockDomain = $p['dbname'];
@@ -109,7 +113,7 @@ class DatabaseSqlite extends Database {
         */
        public static function newStandaloneInstance( $filename, array $p = [] ) {
                $p['dbFilePath'] = $filename;
-               $p['schema'] = false;
+               $p['schema'] = null;
                $p['tablePrefix'] = '';
                /** @var DatabaseSqlite $db */
                $db = Database::factory( 'sqlite', $p );
@@ -120,7 +124,11 @@ class DatabaseSqlite extends Database {
        protected function doInitConnection() {
                if ( $this->dbPath !== null ) {
                        // Standalone .sqlite file mode.
-                       $this->openFile( $this->dbPath, $this->connectionParams['dbname'] );
+                       $this->openFile(
+                               $this->dbPath,
+                               $this->connectionParams['dbname'],
+                               $this->connectionParams['tablePrefix']
+                       );
                } elseif ( $this->dbDir !== null ) {
                        // Stock wiki mode using standard file names per DB
                        if ( strlen( $this->connectionParams['dbname'] ) ) {
@@ -128,7 +136,9 @@ class DatabaseSqlite extends Database {
                                        $this->connectionParams['host'],
                                        $this->connectionParams['user'],
                                        $this->connectionParams['password'],
-                                       $this->connectionParams['dbname']
+                                       $this->connectionParams['dbname'],
+                                       $this->connectionParams['schema'],
+                                       $this->connectionParams['tablePrefix']
                                );
                        } else {
                                // Caller will manually call open() later?
@@ -155,7 +165,7 @@ class DatabaseSqlite extends Database {
                return false;
        }
 
-       protected function open( $server, $user, $pass, $dbName ) {
+       protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
                $this->close();
                $fileName = self::generateFileName( $this->dbDir, $dbName );
                if ( !is_readable( $fileName ) ) {
@@ -163,7 +173,7 @@ class DatabaseSqlite extends Database {
                        throw new DBConnectionError( $this, "SQLite database not accessible" );
                }
                // Only $dbName is used, the other parameters are irrelevant for SQLite databases
-               $this->openFile( $fileName, $dbName );
+               $this->openFile( $fileName, $dbName, $tablePrefix );
 
                return (bool)$this->conn;
        }
@@ -173,10 +183,11 @@ class DatabaseSqlite extends Database {
         *
         * @param string $fileName
         * @param string $dbName
+        * @param string $tablePrefix
         * @throws DBConnectionError
         * @return PDO|bool SQL connection or false if failed
         */
-       protected function openFile( $fileName, $dbName ) {
+       protected function openFile( $fileName, $dbName, $tablePrefix ) {
                $err = false;
 
                $this->dbPath = $fileName;
@@ -198,7 +209,7 @@ class DatabaseSqlite extends Database {
 
                $this->opened = is_object( $this->conn );
                if ( $this->opened ) {
-                       $this->dbName = $dbName;
+                       $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
                        # Set error codes only, don't raise exceptions
                        $this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
                        # Enforce LIKE to be case sensitive, just like MySQL
index 0608253..1973322 100644 (file)
@@ -174,14 +174,15 @@ interface IDatabase {
        /**
         * Get/set the table prefix.
         * @param string|null $prefix The table prefix to set, or omitted to leave it unchanged.
-        * @return string The previous table prefix.
+        * @return string The previous table prefix
+        * @throws DBUnexpectedError
         */
        public function tablePrefix( $prefix = null );
 
        /**
         * Get/set the db schema.
         * @param string|null $schema The database schema to set, or omitted to leave it unchanged.
-        * @return string The previous db schema.
+        * @return string The previous db schema
         */
        public function dbSchema( $schema = null );
 
@@ -358,6 +359,10 @@ interface IDatabase {
        public function getFlag( $flag );
 
        /**
+        * Return the currently selected domain ID
+        *
+        * Null components (database/schema) might change once a connection is established
+        *
         * @return string
         */
        public function getDomainID();
@@ -1115,14 +1120,27 @@ interface IDatabase {
         * Change the current database
         *
         * @param string $db
-        * @return bool Success or failure
+        * @return bool True unless an exception was thrown
         * @throws DBConnectionError If databasesAreIndependent() is true and an error occurs
+        * @throws DBError
+        * @deprecated Since 1.32
         */
        public function selectDB( $db );
 
+       /**
+        * Set the current domain (database, schema, and table prefix)
+        *
+        * This will throw an error for some database types if the database unspecified
+        *
+        * @param string|DatabaseDomain $domain
+        * @since 1.32
+        * @throws DBConnectionError
+        */
+       public function selectDomain( $domain );
+
        /**
         * Get the current DB name
-        * @return string
+        * @return string|null
         */
        public function getDBname();
 
index b4c7c8f..d84ba65 100644 (file)
@@ -974,12 +974,11 @@ class LoadBalancer implements ILoadBalancer {
         * @param int $i Server index
         * @param string $domain Domain ID to open
         * @param int $flags Class CONN_* constant bitfield
-        * @return Database
+        * @return Database|bool Returns false on connection error
+        * @throws DBError When database selection fails
         */
        private function openForeignConnection( $i, $domain, $flags = 0 ) {
                $domainInstance = DatabaseDomain::newFromId( $domain );
-               $dbName = $domainInstance->getDatabase();
-               $prefix = $domainInstance->getTablePrefix();
                $autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
 
                if ( $autoCommit ) {
@@ -990,6 +989,7 @@ class LoadBalancer implements ILoadBalancer {
                        $connInUseKey = self::KEY_FOREIGN_INUSE;
                }
 
+               /** @var Database $conn */
                if ( isset( $this->conns[$connInUseKey][$i][$domain] ) ) {
                        // Reuse an in-use connection for the same domain
                        $conn = $this->conns[$connInUseKey][$i][$domain];
@@ -1004,19 +1004,18 @@ class LoadBalancer implements ILoadBalancer {
                        // Reuse a free connection from another domain
                        $conn = reset( $this->conns[$connFreeKey][$i] );
                        $oldDomain = key( $this->conns[$connFreeKey][$i] );
-                       if ( strlen( $dbName ) && !$conn->selectDB( $dbName ) ) {
-                               $this->lastError = "Error selecting database '$dbName' on server " .
-                                       $conn->getServer() . " from client host {$this->hostname}";
-                               $this->errorConnection = $conn;
-                               $conn = false;
+                       if ( $domainInstance->getDatabase() !== null ) {
+                               $conn->selectDomain( $domainInstance );
                        } else {
-                               $conn->tablePrefix( $prefix );
-                               unset( $this->conns[$connFreeKey][$i][$oldDomain] );
-                               // Note that if $domain is an empty string, getDomainID() might not match it
-                               $this->conns[$connInUseKey][$i][$conn->getDomainId()] = $conn;
-                               $this->connLogger->debug( __METHOD__ .
-                                       ": reusing free connection from $oldDomain for $domain" );
+                               // Stay on the current database, but update the schema/prefix
+                               $conn->dbSchema( $domainInstance->getSchema() );
+                               $conn->tablePrefix( $domainInstance->getTablePrefix() );
                        }
+                       unset( $this->conns[$connFreeKey][$i][$oldDomain] );
+                       // Note that if $domain is an empty string, getDomainID() might not match it
+                       $this->conns[$connInUseKey][$i][$conn->getDomainId()] = $conn;
+                       $this->connLogger->debug( __METHOD__ .
+                               ": reusing free connection from $oldDomain for $domain" );
                } else {
                        if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) {
                                throw new InvalidArgumentException( "No server with index '$i'." );
index 5c0af11..2161b66 100644 (file)
@@ -77,7 +77,7 @@ class LoadBalancerSingle extends LoadBalancer {
                ) );
        }
 
-       protected function reallyOpenConnection( array $server, DatabaseDomain $domainOverride ) {
+       protected function reallyOpenConnection( array $server, DatabaseDomain $domain ) {
                return $this->db;
        }
 }
index b53a486..5706f2d 100644 (file)
@@ -645,7 +645,7 @@ class ManualLogEntry extends LogEntryBase {
                $relations = $this->relations;
 
                // Ensure actor relations are set
-               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH &&
+               if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) &&
                        empty( $relations['target_author_actor'] )
                ) {
                        $actorIds = [];
@@ -664,7 +664,7 @@ class ManualLogEntry extends LogEntryBase {
                                $params['authorActors'] = $actorIds;
                        }
                }
-               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_NEW ) {
+               if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
                        unset( $relations['target_author_id'], $relations['target_author_ip'] );
                        unset( $params['authorIds'], $params['authorIPs'] );
                }
index 4a689d3..803bf0a 100644 (file)
@@ -20,9 +20,9 @@
  * @file
  */
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 
 /**
  * Class for viewing MediaWiki article and history.
@@ -2053,25 +2053,31 @@ class Article implements Page {
         * Perform a deletion and output success or failure messages
         * @param string $reason
         * @param bool $suppress
+        * @param bool $immediate false allows deleting over time via the job queue
+        * @throws FatalError
+        * @throws MWException
         */
-       public function doDelete( $reason, $suppress = false ) {
+       public function doDelete( $reason, $suppress = false, $immediate = false ) {
                $error = '';
                $context = $this->getContext();
                $outputPage = $context->getOutput();
                $user = $context->getUser();
-               $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error, $user );
+               $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error, $user,
+                       [], 'delete', $immediate );
 
-               if ( $status->isGood() ) {
+               if ( $status->isOK() ) {
                        $deleted = $this->getTitle()->getPrefixedText();
 
                        $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) );
                        $outputPage->setRobotPolicy( 'noindex,nofollow' );
 
-                       $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
-
-                       $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
-
-                       Hooks::run( 'ArticleDeleteAfterSuccess', [ $this->getTitle(), $outputPage ] );
+                       if ( $status->isGood() ) {
+                               $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
+                               $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
+                               Hooks::run( 'ArticleDeleteAfterSuccess', [ $this->getTitle(), $outputPage ] );
+                       } else {
+                               $outputPage->addWikiMsg( 'delete-scheduled', wfEscapeWikiText( $deleted ) );
+                       }
 
                        $outputPage->returnToMain( false );
                } else {
@@ -2297,10 +2303,10 @@ class Article implements Page {
         */
        public function doDeleteArticleReal(
                $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
-               $tags = []
+               $tags = [], $immediate = false
        ) {
                return $this->mPage->doDeleteArticleReal(
-                       $reason, $suppress, $u1, $u2, $error, $user, $tags
+                       $reason, $suppress, $u1, $u2, $error, $user, $tags, 'delete', $immediate
                );
        }
 
@@ -2826,12 +2832,16 @@ class Article implements Page {
         * @param int|null $u1 Unused
         * @param bool|null $u2 Unused
         * @param string &$error
+        * @param bool $immediate false allows deleting over time via the job queue
         * @return bool
+        * @throws FatalError
+        * @throws MWException
         */
        public function doDeleteArticle(
-               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = ''
+               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', $immediate = false
        ) {
-               return $this->mPage->doDeleteArticle( $reason, $suppress, $u1, $u2, $error );
+               return $this->mPage->doDeleteArticle( $reason, $suppress, $u1, $u2, $error,
+                       null, $immediate );
        }
 
        /**
index dfc7c02..e843cf3 100644 (file)
@@ -19,8 +19,8 @@
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
 use MediaWiki\Storage\SqlBlobStore;
 use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\IResultWrapper;
index 7c97465..081af19 100644 (file)
 use MediaWiki\Edit\PreparedEdit;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Revision\RevisionRenderer;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\DerivedPageDataUpdater;
 use MediaWiki\Storage\PageUpdater;
-use MediaWiki\Storage\RevisionRecord;
 use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
 use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\FakeResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
@@ -987,8 +987,16 @@ class WikiPage implements Page, IDBAccessObject {
 
                // rd_fragment and rd_interwiki were added later, populate them if empty
                if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
+                       // (T203942) We can't redirect to Media namespace because it's virtual.
+                       // We don't want to modify Title objects farther down the
+                       // line. So, let's fix this here by changing to File namespace.
+                       if ( $row->rd_namespace == NS_MEDIA ) {
+                               $namespace = NS_FILE;
+                       } else {
+                               $namespace = $row->rd_namespace;
+                       }
                        $this->mRedirectTarget = Title::makeTitle(
-                               $row->rd_namespace, $row->rd_title,
+                               $namespace, $row->rd_title,
                                $row->rd_fragment, $row->rd_interwiki
                        );
                        return $this->mRedirectTarget;
@@ -2512,6 +2520,28 @@ class WikiPage implements Page, IDBAccessObject {
                return implode( ':', $bits );
        }
 
+       /**
+        * Determines if deletion of this page would be batched (executed over time by the job queue)
+        * or not (completed in the same request as the delete call).
+        *
+        * It is unlikely but possible that an edit from another request could push the page over the
+        * batching threshold after this function is called, but before the caller acts upon the
+        * return value.  Callers must decide for themselves how to deal with this.  $safetyMargin
+        * is provided as an unreliable but situationally useful help for some common cases.
+        *
+        * @param int $safetyMargin Added to the revision count when checking for batching
+        * @return bool True if deletion would be batched, false otherwise
+        */
+       public function isBatchedDelete( $safetyMargin = 0 ) {
+               global $wgDeleteRevisionsBatchSize;
+
+               $dbr = wfGetDB( DB_REPLICA );
+               $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
+               $revCount += $safetyMargin;
+
+               return $revCount >= $wgDeleteRevisionsBatchSize;
+       }
+
        /**
         * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
         * backwards compatibility, if you care about error reporting you should use
@@ -2526,13 +2556,20 @@ class WikiPage implements Page, IDBAccessObject {
         * @param bool|null $u2 Unused
         * @param array|string &$error Array of errors to append to
         * @param User|null $user The deleting user
+        * @param bool $immediate false allows deleting over time via the job queue
         * @return bool True if successful
+        * @throws FatalError
+        * @throws MWException
         */
        public function doDeleteArticle(
-               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
+               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
+               $immediate = false
        ) {
-               $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
-               return $status->isGood();
+               $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
+                       [], 'delete', $immediate );
+
+               // Returns true if the page was actually deleted, or is scheduled for deletion
+               return $status->isOK();
        }
 
        /**
@@ -2550,27 +2587,23 @@ class WikiPage implements Page, IDBAccessObject {
         * @param User|null $deleter The deleting user
         * @param array $tags Tags to apply to the deletion action
         * @param string $logsubtype
+        * @param bool $immediate false allows deleting over time via the job queue
         * @return Status Status object; if successful, $status->value is the log_id of the
         *   deletion log entry. If the page couldn't be deleted because it wasn't
         *   found, $status is a non-fatal 'cannotdelete' error
+        * @throws FatalError
+        * @throws MWException
         */
        public function doDeleteArticleReal(
                $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
-               $tags = [], $logsubtype = 'delete'
+               $tags = [], $logsubtype = 'delete', $immediate = false
        ) {
-               global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage,
-                       $wgActorTableSchemaMigrationStage, $wgMultiContentRevisionSchemaMigrationStage;
+               global $wgUser;
 
                wfDebug( __METHOD__ . "\n" );
 
                $status = Status::newGood();
 
-               if ( $this->mTitle->getDBkey() === '' ) {
-                       $status->error( 'cannotdelete',
-                               wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
-                       return $status;
-               }
-
                // Avoid PHP 7.1 warning of passing $this by reference
                $wikiPage = $this;
 
@@ -2585,6 +2618,26 @@ class WikiPage implements Page, IDBAccessObject {
                        return $status;
                }
 
+               return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
+                       $logsubtype, $immediate );
+       }
+
+       /**
+        * Back-end article deletion
+        *
+        * Only invokes batching via the job queue if necessary per $wgDeleteRevisionsBatchSize.
+        * Deletions can often be completed inline without involving the job queue.
+        *
+        * Potentially called many times per deletion operation for pages with many revisions.
+        */
+       public function doDeleteArticleBatched(
+               $reason, $suppress, User $deleter, $tags,
+               $logsubtype, $immediate = false, $webRequestId = null
+       ) {
+               wfDebug( __METHOD__ . "\n" );
+
+               $status = Status::newGood();
+
                $dbw = wfGetDB( DB_MASTER );
                $dbw->startAtomic( __METHOD__ );
 
@@ -2603,11 +2656,7 @@ class WikiPage implements Page, IDBAccessObject {
                        return $status;
                }
 
-               // Given the lock above, we can be confident in the title and page ID values
-               $namespace = $this->getTitle()->getNamespace();
-               $dbKey = $this->getTitle()->getDBkey();
-
-               // At this point we are now comitted to returning an OK
+               // At this point we are now committed to returning an OK
                // status unless some DB query error or other exception comes up.
                // This way callers don't have to call rollback() if $status is bad
                // unless they actually try to catch exceptions (which is rare).
@@ -2623,6 +2672,133 @@ class WikiPage implements Page, IDBAccessObject {
                        $content = null;
                }
 
+               // Archive revisions.  In immediate mode, archive all revisions.  Otherwise, archive
+               // one batch of revisions and defer archival of any others to the job queue.
+               $explictTrxLogged = false;
+               while ( true ) {
+                       $done = $this->archiveRevisions( $dbw, $id, $suppress );
+                       if ( $done || !$immediate ) {
+                               break;
+                       }
+                       $dbw->endAtomic( __METHOD__ );
+                       if ( $dbw->explicitTrxActive() ) {
+                               // Explict transactions may never happen here in practice.  Log to be sure.
+                               if ( !$explictTrxLogged ) {
+                                       $explictTrxLogged = true;
+                                       LoggerFactory::getInstance( 'wfDebug' )->debug(
+                                               'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
+                                               'title' => $this->getTitle()->getText(),
+                                       ] );
+                               }
+                               continue;
+                       }
+                       if ( $dbw->trxLevel() ) {
+                               $dbw->commit();
+                       }
+                       $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+                       $lbFactory->waitForReplication();
+                       $dbw->startAtomic( __METHOD__ );
+               }
+
+               // If done archiving, also delete the article.
+               if ( !$done ) {
+                       $dbw->endAtomic( __METHOD__ );
+
+                       $jobParams = [
+                               'wikiPageId' => $id,
+                               'requestId' => $webRequestId ?? WebRequest::getRequestId(),
+                               'reason' => $reason,
+                               'suppress' => $suppress,
+                               'userId' => $deleter->getId(),
+                               'tags' => json_encode( $tags ),
+                               'logsubtype' => $logsubtype,
+                       ];
+
+                       $job = new DeletePageJob( $this->getTitle(), $jobParams );
+                       JobQueueGroup::singleton()->push( $job );
+
+                       $status->warning( 'delete-scheduled',
+                               wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+               } else {
+                       // Get archivedRevisionCount by db query, because there's no better alternative.
+                       // Jobs cannot pass a count of archived revisions to the next job, because additional
+                       // deletion operations can be started while the first is running.  Jobs from each
+                       // gracefully interleave, but would not know about each other's count.  Deduplication
+                       // in the job queue to avoid simultaneous deletion operations would add overhead.
+                       // Number of archived revisions cannot be known beforehand, because edits can be made
+                       // while deletion operations are being processed, changing the number of archivals.
+                       $archivedRevisionCount = $dbw->selectRowCount(
+                               'archive', '1', [ 'ar_page_id' => $id ], __METHOD__
+                       );
+
+                       // Clone the title and wikiPage, so we have the information we need when
+                       // we log and run the ArticleDeleteComplete hook.
+                       $logTitle = clone $this->mTitle;
+                       $wikiPageBeforeDelete = clone $this;
+
+                       // Now that it's safely backed up, delete it
+                       $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
+
+                       // Log the deletion, if the page was suppressed, put it in the suppression log instead
+                       $logtype = $suppress ? 'suppress' : 'delete';
+
+                       $logEntry = new ManualLogEntry( $logtype, $logsubtype );
+                       $logEntry->setPerformer( $deleter );
+                       $logEntry->setTarget( $logTitle );
+                       $logEntry->setComment( $reason );
+                       $logEntry->setTags( $tags );
+                       $logid = $logEntry->insert();
+
+                       $dbw->onTransactionPreCommitOrIdle(
+                               function () use ( $logEntry, $logid ) {
+                                       // T58776: avoid deadlocks (especially from FileDeleteForm)
+                                       $logEntry->publish( $logid );
+                               },
+                               __METHOD__
+                       );
+
+                       $dbw->endAtomic( __METHOD__ );
+
+                       $this->doDeleteUpdates( $id, $content, $revision, $deleter );
+
+                       Hooks::run( 'ArticleDeleteComplete', [
+                               &$wikiPageBeforeDelete,
+                               &$deleter,
+                               $reason,
+                               $id,
+                               $content,
+                               $logEntry,
+                               $archivedRevisionCount
+                       ] );
+                       $status->value = $logid;
+
+                       // Show log excerpt on 404 pages rather than just a link
+                       $cache = MediaWikiServices::getInstance()->getMainObjectStash();
+                       $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
+                       $cache->set( $key, 1, $cache::TTL_DAY );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Archives revisions as part of page deletion.
+        *
+        * @param IDatabase $dbw
+        * @param int $id
+        * @param bool $suppress Suppress all revisions and log the deletion in
+        *   the suppression log instead of the deletion log
+        * @return bool
+        */
+       protected function archiveRevisions( $dbw, $id, $suppress ) {
+               global $wgContentHandlerUseDB, $wgMultiContentRevisionSchemaMigrationStage,
+                       $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage,
+                       $wgDeleteRevisionsBatchSize;
+
+               // Given the lock above, we can be confident in the title and page ID values
+               $namespace = $this->getTitle()->getNamespace();
+               $dbKey = $this->getTitle()->getDBkey();
+
                $commentStore = CommentStore::getStore();
                $actorMigration = ActorMigration::newMigration();
 
@@ -2669,13 +2845,14 @@ class WikiPage implements Page, IDBAccessObject {
                        }
                }
 
-                       // Get all of the page revisions
+               // Get as many of the page revisions as we are allowed to.  The +1 lets us recognize the
+               // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
                $res = $dbw->select(
                        $revQuery['tables'],
                        $revQuery['fields'],
                        [ 'rev_page' => $id ],
                        __METHOD__,
-                       [],
+                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
                        $revQuery['joins']
                );
 
@@ -2686,16 +2863,22 @@ class WikiPage implements Page, IDBAccessObject {
                /** @var int[] Revision IDs of edits that were made by IPs */
                $ipRevIds = [];
 
+               $done = true;
                foreach ( $res as $row ) {
+                       if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
+                               $done = false;
+                               break;
+                       }
+
                        $comment = $commentStore->getComment( 'rev_comment', $row );
                        $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
                        $rowInsert = [
-                               'ar_namespace'  => $namespace,
-                               'ar_title'      => $dbKey,
-                               'ar_timestamp'  => $row->rev_timestamp,
-                               'ar_minor_edit' => $row->rev_minor_edit,
-                               'ar_rev_id'     => $row->rev_id,
-                               'ar_parent_id'  => $row->rev_parent_id,
+                                       'ar_namespace'  => $namespace,
+                                       'ar_title'      => $dbKey,
+                                       'ar_timestamp'  => $row->rev_timestamp,
+                                       'ar_minor_edit' => $row->rev_minor_edit,
+                                       'ar_rev_id'     => $row->rev_id,
+                                       'ar_parent_id'  => $row->rev_parent_id,
                                        /**
                                         * ar_text_id should probably not be written to when the multi content schema has
                                         * been migrated to (wgMultiContentRevisionSchemaMigrationStage) however there is no
@@ -2704,11 +2887,11 @@ class WikiPage implements Page, IDBAccessObject {
                                         * Task: https://phabricator.wikimedia.org/T190148
                                         * Copying the value from the revision table should not lead to any issues for now.
                                         */
-                               'ar_len'        => $row->rev_len,
-                               'ar_page_id'    => $id,
-                               'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
-                               'ar_sha1'       => $row->rev_sha1,
-                       ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
+                                       'ar_len'        => $row->rev_len,
+                                       'ar_page_id'    => $id,
+                                       'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
+                                       'ar_sha1'       => $row->rev_sha1,
+                               ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
                                + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
 
                        if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
@@ -2729,70 +2912,27 @@ class WikiPage implements Page, IDBAccessObject {
                                $ipRevIds[] = $row->rev_id;
                        }
                }
-               // Copy them into the archive table
-               $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
-               // Save this so we can pass it to the ArticleDeleteComplete hook.
-               $archivedRevisionCount = $dbw->affectedRows();
 
-               // Clone the title and wikiPage, so we have the information we need when
-               // we log and run the ArticleDeleteComplete hook.
-               $logTitle = clone $this->mTitle;
-               $wikiPageBeforeDelete = clone $this;
+               // This conditional is just a sanity check
+               if ( count( $revids ) > 0 ) {
+                       // Copy them into the archive table
+                       $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
 
-               // Now that it's safely backed up, delete it
-               $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
-               $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
-               if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
-                       $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
-               }
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
-                       $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
-               }
+                       $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
+                       if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
+                               $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
+                       }
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
+                               $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
+                       }
 
-               // Also delete records from ip_changes as applicable.
-               if ( count( $ipRevIds ) > 0 ) {
-                       $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
+                       // Also delete records from ip_changes as applicable.
+                       if ( count( $ipRevIds ) > 0 ) {
+                               $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
+                       }
                }
 
-               // Log the deletion, if the page was suppressed, put it in the suppression log instead
-               $logtype = $suppress ? 'suppress' : 'delete';
-
-               $logEntry = new ManualLogEntry( $logtype, $logsubtype );
-               $logEntry->setPerformer( $deleter );
-               $logEntry->setTarget( $logTitle );
-               $logEntry->setComment( $reason );
-               $logEntry->setTags( $tags );
-               $logid = $logEntry->insert();
-
-               $dbw->onTransactionPreCommitOrIdle(
-                       function () use ( $logEntry, $logid ) {
-                               // T58776: avoid deadlocks (especially from FileDeleteForm)
-                               $logEntry->publish( $logid );
-                       },
-                       __METHOD__
-               );
-
-               $dbw->endAtomic( __METHOD__ );
-
-               $this->doDeleteUpdates( $id, $content, $revision, $deleter );
-
-               Hooks::run( 'ArticleDeleteComplete', [
-                       &$wikiPageBeforeDelete,
-                       &$deleter,
-                       $reason,
-                       $id,
-                       $content,
-                       $logEntry,
-                       $archivedRevisionCount
-               ] );
-               $status->value = $logid;
-
-               // Show log excerpt on 404 pages rather than just a link
-               $cache = MediaWikiServices::getInstance()->getMainObjectStash();
-               $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
-               $cache->set( $key, 1, $cache::TTL_DAY );
-
-               return $status;
+               return $done;
        }
 
        /**
index 6e6a574..2c9fbc8 100644 (file)
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Revision\RevisionRenderer;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
 
 class PoolWorkArticleView extends PoolCounterWork {
        /** @var WikiPage */
index c5cffea..7ddf587 100644 (file)
@@ -218,14 +218,14 @@ abstract class RevDelList extends RevisionListBase {
                                $virtualOldBits |= $removedBits;
 
                                $status->successCount++;
-                               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                        if ( $item->getAuthorId() > 0 ) {
                                                $authorIds[] = $item->getAuthorId();
                                        } elseif ( IP::isIPAddress( $item->getAuthorName() ) ) {
                                                $authorIPs[] = $item->getAuthorName();
                                        }
                                }
-                               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                        $authorActors[] = $item->getAuthorActor();
                                }
 
@@ -271,11 +271,11 @@ abstract class RevDelList extends RevisionListBase {
 
                // Log it
                $authorFields = [];
-               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                        $authorFields['authorIds'] = $authorIds;
                        $authorFields['authorIPs'] = $authorIPs;
                }
-               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                        $authorFields['authorActors'] = $authorActors;
                }
                $this->updateLog(
index 6291e8d..a8bf814 100644 (file)
@@ -67,7 +67,7 @@ class RevisionDeleteUser {
                $userTitle = Title::makeTitleSafe( NS_USER, $name );
                $userDbKey = $userTitle->getDBkey();
 
-               if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                        # Hide name from live edits
                        $dbw->update(
                                'revision',
@@ -116,7 +116,7 @@ class RevisionDeleteUser {
                        );
                }
 
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                        $actorId = $dbw->selectField( 'actor', 'actor_id', [ 'actor_name' => $name ], __METHOD__ );
                        if ( $actorId ) {
                                # Hide name from live edits
index e39ec58..99a5a9a 100644 (file)
@@ -824,12 +824,12 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
 
                // Both Hooks::run are explicit here to make findHooks.php happy
                if ( $this->isSignup() ) {
-                       Hooks::run( 'UserCreateForm', [ &$template ] );
+                       Hooks::run( 'UserCreateForm', [ &$template ], '1.27' );
                        if ( $oldTemplate !== $template ) {
                                wfDeprecated( "reference in UserCreateForm hook", '1.27' );
                        }
                } else {
-                       Hooks::run( 'UserLoginForm', [ &$template ] );
+                       Hooks::run( 'UserLoginForm', [ &$template ], '1.27' );
                        if ( $oldTemplate !== $template ) {
                                wfDeprecated( "reference in UserLoginForm hook", '1.27' );
                        }
index 54afde1..9931614 100644 (file)
@@ -104,30 +104,12 @@ class SpecialLog extends SpecialPage {
                        $offenderName = $opts->getValue( 'offender' );
                        $offender = empty( $offenderName ) ? null : User::newFromName( $offenderName, false );
                        if ( $offender ) {
-                               if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                                        $qc = [ 'ls_field' => 'target_author_actor', 'ls_value' => $offender->getActorId() ];
+                               } elseif ( $offender->getId() > 0 ) {
+                                       $qc = [ 'ls_field' => 'target_author_id', 'ls_value' => $offender->getId() ];
                                } else {
-                                       if ( $offender->getId() > 0 ) {
-                                               $field = 'target_author_id';
-                                               $value = $offender->getId();
-                                       } else {
-                                               $field = 'target_author_ip';
-                                               $value = $offender->getName();
-                                       }
-                                       if ( !$offender->getActorId() ) {
-                                               $qc = [ 'ls_field' => $field, 'ls_value' => $value ];
-                                       } else {
-                                               $db = wfGetDB( DB_REPLICA );
-                                               $qc = [
-                                                       'ls_field' => [ 'target_author_actor', $field ], // So LogPager::getQueryInfo() works right
-                                                       $db->makeList( [
-                                                               $db->makeList(
-                                                                       [ 'ls_field' => 'target_author_actor', 'ls_value' => $offender->getActorId() ], LIST_AND
-                                                               ),
-                                                               $db->makeList( [ 'ls_field' => $field, 'ls_value' => $value ], LIST_AND ),
-                                                       ], LIST_OR ),
-                                               ];
-                                       }
+                                       $qc = [ 'ls_field' => 'target_author_ip', 'ls_value' => $offender->getName() ];
                                }
                        }
                } else {
index 464be4f..2f6dc03 100644 (file)
@@ -547,6 +547,15 @@ class MovePageForm extends UnlistedSpecialPage {
                                return;
                        }
 
+                       $page = WikiPage::factory( $nt );
+
+                       // Small safety margin to guard against concurrent edits
+                       if ( $page->isBatchedDelete( 5 ) ) {
+                               $this->showForm( [ [ 'movepage-delete-first' ] ] );
+
+                               return;
+                       }
+
                        $reason = $this->msg( 'delete_and_move_reason', $ot )->inContentLanguage()->text();
 
                        // Delete an associated image if there is
@@ -559,7 +568,6 @@ class MovePageForm extends UnlistedSpecialPage {
                        }
 
                        $error = ''; // passed by ref
-                       $page = WikiPage::factory( $nt );
                        $deleteStatus = $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user );
                        if ( !$deleteStatus->isGood() ) {
                                $this->showForm( $deleteStatus->getErrorsArray() );
index e88162e..06e1c77 100644 (file)
@@ -218,7 +218,7 @@ class SpecialNewFiles extends IncludableSpecialPage {
                $message = $this->msg( 'newimagestext' )->inContentLanguage();
                if ( !$message->isDisabled() ) {
                        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
-                       $this->getOutput()->addWikiTextTidy(
+                       $this->getOutput()->addWikiTextAsContent(
                                Html::rawElement( 'div',
                                        [
 
index a929820..e4e513e 100644 (file)
@@ -22,7 +22,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 use Wikimedia\Rdbms\IResultWrapper;
 
 /**
index 5b50f0a..9900340 100644 (file)
@@ -221,14 +221,12 @@ class ContribsPager extends RangeChronologicalPager {
                                $conds = ActorMigration::newMigration()->getWhere( $this->mDb, 'rev_user', $user );
                                $queryInfo['conds'][] = $conds['conds'];
                                // Force the appropriate index to avoid bad query plans (T189026)
-                               if ( count( $conds['orconds'] ) === 1 ) {
-                                       if ( isset( $conds['orconds']['actor'] ) ) {
-                                               // @todo: This will need changing when revision_comment_temp goes away
-                                               $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
-                                       } else {
-                                               $queryInfo['options']['USE INDEX']['revision'] =
-                                                       isset( $conds['orconds']['userid'] ) ? 'user_timestamp' : 'usertext_timestamp';
-                                       }
+                               if ( isset( $conds['orconds']['actor'] ) ) {
+                                       // @todo: This will need changing when revision_comment_temp goes away
+                                       $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
+                               } else {
+                                       $queryInfo['options']['USE INDEX']['revision'] =
+                                               isset( $conds['orconds']['userid'] ) ? 'user_timestamp' : 'usertext_timestamp';
                                }
                        }
                }
index c214f1f..6b7e4b8 100644 (file)
@@ -113,7 +113,7 @@ class NewFilesPager extends RangeChronologicalPager {
                        $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
                        $conds['rc_namespace'] = NS_FILE;
 
-                       if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                                $jcond = 'rc_actor = ' . $imgQuery['fields']['img_actor'];
                        } else {
                                $rcQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
index 5762120..0c4b425 100644 (file)
@@ -410,9 +410,7 @@ class BotPassword implements IDBAccessObject {
        /**
         * There are two ways to login with a bot password: "username@appId", "password" and
         * "username", "appId@password". Transform it so it is always in the first form.
-        * Returns [bot username, bot password, could be normal password?] where the last one is a flag
-        * meaning this could either be a bot password or a normal password, it cannot be decided for
-        * certain (although in such cases it almost always will be a bot password).
+        * Returns [bot username, bot password].
         * If this cannot be a bot password login just return false.
         * @param string $username
         * @param string $password
@@ -424,14 +422,14 @@ class BotPassword implements IDBAccessObject {
                if ( strlen( $password ) >= 32 && strpos( $username, $sep ) !== false ) {
                        // the separator is not valid in new usernames but might appear in legacy ones
                        if ( preg_match( '/^[0-9a-w]{32,}$/', $password ) ) {
-                               return [ $username, $password, true ];
+                               return [ $username, $password ];
                        }
                } elseif ( strlen( $password ) > 32 && strpos( $password, $sep ) !== false ) {
                        $segments = explode( $sep, $password );
                        $password = array_pop( $segments );
                        $appId = implode( $sep, $segments );
                        if ( preg_match( '/^[0-9a-w]{32,}$/', $password ) ) {
-                               return [ $username . $sep . $appId, $password, true ];
+                               return [ $username . $sep . $appId, $password ];
                        }
                }
                return false;
index 12623e8..ada5dc9 100644 (file)
@@ -627,9 +627,12 @@ class User implements IDBAccessObject, UserIdentity {
        public static function newFromActorId( $id ) {
                global $wgActorTableSchemaMigrationStage;
 
-               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_OLD ) {
+               // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW,
+               // but it does little harm and might be needed for write callers loading a User.
+               if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) ) {
                        throw new BadMethodCallException(
-                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage is MIGRATION_OLD'
+                               'Cannot use ' . __METHOD__
+                                       . ' when $wgActorTableSchemaMigrationStage lacks SCHEMA_COMPAT_NEW'
                        );
                }
 
@@ -679,7 +682,9 @@ class User implements IDBAccessObject, UserIdentity {
                $user = new User;
                $user->mFrom = 'defaults';
 
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD && $actorId !== null ) {
+               // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW,
+               // but it does little harm and might be needed for write callers loading a User.
+               if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) && $actorId !== null ) {
                        $user->mActorId = (int)$actorId;
                        if ( $user->mActorId !== 0 ) {
                                $user->mFrom = 'actor';
@@ -1488,7 +1493,9 @@ class User implements IDBAccessObject, UserIdentity {
 
                $this->mGroupMemberships = null; // deferred
 
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+               // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW,
+               // but it does little harm and might be needed for write callers loading a User.
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) {
                        if ( isset( $row->actor_id ) ) {
                                $this->mActorId = (int)$row->actor_id;
                                if ( $this->mActorId !== 0 ) {
@@ -2491,7 +2498,9 @@ class User implements IDBAccessObject, UserIdentity {
        public function getActorId( IDatabase $dbw = null ) {
                global $wgActorTableSchemaMigrationStage;
 
-               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_OLD ) {
+               // Technically we should always return 0 without SCHEMA_COMPAT_READ_NEW,
+               // but it does little harm and might be needed for write callers loading a User.
+               if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
                        return 0;
                }
 
@@ -2501,7 +2510,7 @@ class User implements IDBAccessObject, UserIdentity {
 
                // Currently $this->mActorId might be null if $this was loaded from a
                // cache entry that was written when $wgActorTableSchemaMigrationStage
-               // was MIGRATION_OLD. Once that is no longer a possibility (i.e. when
+               // was SCHEMA_COMPAT_OLD. Once that is no longer a possibility (i.e. when
                // User::VERSION is incremented after $wgActorTableSchemaMigrationStage
                // has been removed), that condition may be removed.
                if ( $this->mActorId === null || !$this->mActorId && $dbw ) {
@@ -4222,7 +4231,7 @@ class User implements IDBAccessObject, UserIdentity {
                                );
                        }
 
-                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                $dbw->update(
                                        'actor',
                                        [ 'actor_name' => $this->mName ],
@@ -4320,9 +4329,10 @@ class User implements IDBAccessObject, UserIdentity {
                        $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
                        if ( $dbw->affectedRows() ) {
                                $newUser = self::newFromId( $dbw->insertId() );
+                               $newUser->mName = $fields['user_name'];
+                               $newUser->updateActorId( $dbw );
                                // Load the user from master to avoid replica lag
                                $newUser->load( self::READ_LATEST );
-                               $newUser->updateActorId( $dbw );
                        } else {
                                $newUser = null;
                        }
@@ -4431,7 +4441,7 @@ class User implements IDBAccessObject, UserIdentity {
        private function updateActorId( IDatabase $dbw ) {
                global $wgActorTableSchemaMigrationStage;
 
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                        $dbw->insert(
                                'actor',
                                [ 'actor_user' => $this->mId, 'actor_name' => $this->mName ],
@@ -5656,14 +5666,18 @@ class User implements IDBAccessObject, UserIdentity {
                        ],
                        'joins' => [],
                ];
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+
+               // Technically we shouldn't allow this without SCHEMA_COMPAT_READ_NEW,
+               // but it does little harm and might be needed for write callers loading a User.
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_NEW ) {
                        $ret['tables']['user_actor'] = 'actor';
                        $ret['fields'][] = 'user_actor.actor_id';
                        $ret['joins']['user_actor'] = [
-                               $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+                               ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ? 'JOIN' : 'LEFT JOIN',
                                [ 'user_actor.actor_user = user_id' ]
                        ];
                }
+
                return $ret;
        }
 
index 52cc956..b19a336 100644 (file)
        "create-local": "Tamah deskripsi lokal",
        "delete": "Sampôh",
        "undelete_short": "Bateuë sampôh {{PLURAL:$1|one edit|$1 edits}}",
-       "viewdeleted_short": "Eu {{PLURAL:$1|saboh neuandam|$1 neuandam}} nyang geusampôh",
+       "viewdeleted_short": "Kalön {{PLURAL:$1|hasé peusaneut teusampôh}}",
        "protect": "Peulindông",
        "protect_change": "ubah",
        "unprotect": "Gantoë neulindông",
        "red-link-title": "$1 (laman hana)",
        "sort-descending": "Peuurôt tren",
        "sort-ascending": "Peuurôt ék",
-       "nstab-main": "Miëng",
+       "nstab-main": "Laman",
        "nstab-user": "Ureuëng ngui",
        "nstab-media": "Laman media",
-       "nstab-special": "Miëng kusuih",
+       "nstab-special": "Laman kusuih",
        "nstab-project": "Laman buët",
        "nstab-image": "Beureukaih",
        "nstab-mediawiki": "Peusan",
        "actionthrottled": "Buet geupeubataih",
        "actionthrottledtext": "Sibagoe saboh seunipat lawan-spam, droeneuh geupeubataih nibak neupeulaku buet nyoe le that gö lam watèë paneuk, ngön droeneuh ka leubèh nibak bataih.\nNeuci lom lam padum minèt.",
        "protectedpagetext": "Laman nyoe ka geupeulindông mangat bèk jeuet geuandam",
-       "viewsourcetext": "Droeneuh jeuet neueu ngön neusalén nè nibak mieng nyoe.",
+       "viewsourcetext": "Droeneuh jeuet neukalön ngön neusalén nè nibak laman nyoe.",
        "viewyourtext": "Droëneuh jeuet neukalön ngön neusalén nè nibak <strong>hasé peusaneut Droëneuh</strong> u laman nyoë.",
        "protectedinterface": "Halaman nyoe na tèks muka keu muka keu peukakaih leumiëk ngön geupeulindông mangat bek jeuet jipeureuloh.\nKeu neuk tamah atawa ubah teujeumah keu ban dum wiki, neungui [https://translatewiki.net/ translatewiki.net], proyek lokalisasi MediaWiki.",
        "mycustomcssprotected": "Droëneuh hana hak neuandam halaman CSS nyoe.",
        "mycustomjsprotected": "Droëneuh hana idin neuandam halaman JavaScript nyoe.",
        "mypreferencesprotected": "Droeneuh hana izin keu neuandam geunalak droeneuh.",
-       "ns-specialprotected": "Laman khusuih bèk neuandam",
+       "ns-specialprotected": "Laman kusuih h'an jeuet geupeusaneut",
        "titleprotected": "Nan nyoe ka geupeulindông nibak neuandam lé [[User:$1|$1]].\nDalèhjih nakeuh <em>$2</em>.",
        "invalidtitle-knownnamespace": "Nan nyang hana sah ngön ruweueng nan \"$2\" ngön \"$3\"",
        "exception-nologin": "Goh lom tamong log",
        "summary": "Éhtisa:",
        "subject": "Bhaih:",
        "minoredit": "Nyoë lôn peusaneut bacut",
-       "watchthis": "Kalön miëng nyoë",
+       "watchthis": "Kalön laman nyoë",
        "savearticle": "Keubah laman",
        "savechanges": "Keubah neuubah",
        "publishpage": "Peuteubiet mieng",
        "preview": "Eu dilèë",
        "showpreview": "Peuleumah hasé",
        "showdiff": "Peuleumah neuubah",
-       "anoneditwarning": "<strong>Peuneugah:</strong> Droëneuh hana lom neutamong. Alamat IP-neuh jeuët deuh bak ureuëng la'én meunyö neumeuandam. Meunyö Droëneuh <strong>[$1 neutamong]</strong> atawa <strong>[$2 neudapeuta]</strong>, neuandamneuh jeuët teutuléh ateuëh nan Droëneuh ngön na lom meunapha'at nyang la'én.",
+       "anoneditwarning": "<strong>Peuneugah:</strong> Droëneuh goh lom neutamong. Alamat IP-neuh jeuët leumah bak ureuëng la'én meunyö neupeusaneut sipeue-peue. Meunyö Droëneuh <strong>[$1 neutamong]</strong> atawa <strong>[$2 neudapeuta]</strong>, atra neupeusaneut jeuët teutuléh ateuëh nan Droëneuh ngön na lom meunapha'at nyang la'én.",
        "missingcommenttext": "Neupasoë beunalah di yup.",
        "summary-preview": "Eu neuringkaih neuandam:",
        "blockedtitle": "Ureueng ngui geutheun",
        "newarticle": "(Barô)",
        "newarticletext": "Droëneuh ka neuseutöt peunawôt u laman nyang goh na.\nKeu neupeugöt laman nyan, neukeutik lam plôk di yup (eu [$1 laman beunantu] keu haba leubèh le).\nMeunyö droëneuh trôk keunoë hana neusaja, neuteugön tèk '''back''' bak ''browser'''droëneuh.",
        "anontalkpagetext": "----''Nyoe nakeuh ôn marit ureueng ngui nyang hana tamöng atawa hana geungui.''\nSaweub nyan, kamoe payah meukubah alamat IP-geuh keu meuparéksa. \nAlamat IP mungkén jingui lé padum-padum droe ureueng.\nMeunyoe droeneuh ureueng nyang hana tamöng nyan, tulông [[Special:CreateAccount|peugöt nan ureueng ngui]] atawa [[Special:UserLogin|tamöng log]] mangat meuteugah nibak bhah nyang hana meuphôm bak uroe la'én.",
-       "noarticletext": "Hana naseukah jinoë lam miëng nyoë.\nDroëneuh jeuët [[Special:Search/{{PAGENAME}}|neuseutöt nan miëng nyoë]] bak miëng-miëng la’én, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} neuseutöt log nyang na hubôngan], atawa [{{fullurl:{{FULLPAGENAME}}|action=edit}} neupeugöt miëng nyoë]</span>.",
+       "noarticletext": "Hana naseukah jinoë lam laman nyoë.\nDroëneuh jeuët [[Special:Search/{{PAGENAME}}|neuseutöt nan laman nyoë]] bak laman-laman nyang la’én, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} neuseutöt log nyang na hubôngan], atawa [{{fullurl:{{FULLPAGENAME}}|action=edit}} neupeugöt laman nyoë]</span>.",
        "noarticletext-nopermission": "Hana asoë bak laman nyoë jinoë.\nDroëneuh jeuët [[Special:Search/{{PAGENAME}}|neumita keu nan ôn nyoë]] bak laman-laman la'én,\natawa <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} neumita log nyang na meuhubông]</span>, tapi Droëneuh hana idin keu neupeugöt laman nyoë",
        "userpage-userdoesnotexist-view": "Ureueng ngui \"$1\" hana teudapeuta.",
        "updated": "(Seubarô)",
        "permissionserrorstext": "Droëneuh hana geupeuidin keu neupubuët nyoë, muroë {{PLURAL:$1|dalèh}} nyoë:",
        "permissionserrorstext-withaction": "Droëneuh hana hak tamöng keu $2, muroë {{PLURAL:$1|choë|choë}} nyoë:",
        "recreate-moveddeleted-warn": "'''Ingat: Droëneuh neupeugöt ulang saboh laman nyang ka tom geusampôh. ''',\n\nNeutimang-timang dilèë peuë ék patôt neulanjut atra nyang teungöh neupeusaneut.\nNyoë pat nakeuh log seunampôh nibak laman nyoë:",
-       "moveddeleted-notice": "Mieng nyoë ka geusampôh.\nLog seunampôh, neulindông ngön peuninah keu mieng nyoe na di yup nyoe keu catatan.",
+       "moveddeleted-notice": "Laman nyoë ka geusampôh.\nLog seunampôh, neulindông ngön peuninah keu laman nyoe na di yup nyoe keu catatan.",
        "log-fulllog": "Eu ban dum ceunatat",
        "edit-hook-aborted": "Seunampôh geupeubateuë lé kaw'ét parser.\nHana jeuneulaih.",
        "edit-gone-missing": "Han jeut peubarô ôn.\nÔn nyoe mungkén ka geusampôh.",
        "edit-conflict": "Konflik peusaneut.",
        "postedit-confirmation-saved": "Neuandam droeneuh ka meukubah.",
-       "edit-already-exists": "Han jeut peugöt ôn barô.\nÔn nyoe ka lheuh na.",
+       "edit-already-exists": "Han jeuet peugöt laman barô.\nLaman nyoe ka lheueh na.",
        "defaultmessagetext": "Naseukah peusan pukok",
        "content-model-wikitext": "seunurat wiki",
        "post-expand-template-inclusion-warning": "'''Ingat:''' Seunipat seunaleuëk nyang neunguy rayek that.\nLadôm seunaleuëk hana geupeurôh",
        "revdelete-radio-unset": "Deuih",
        "revdelete-log": "Alasan:",
        "revdel-restore": "Gantoë neuleumah",
-       "pagehist": "Riwayat miëng",
+       "pagehist": "Riwayat laman",
        "deletedhist": "Riwayat nyang teusampôh",
        "mergehistory-from": "Nè mieng:",
-       "mergehistory-into": "Mieng nyang geutuju:",
+       "mergehistory-into": "Laman tujuan:",
        "mergehistory-invalid-source": "Asai ôn payah nan nyang beutôi.",
        "mergehistory-reason": "Alasan:",
        "mergelog": "Peugabông log",
        "viewprevnext": "Eu ($1 {{int:pipe-separator}} $2)($3)",
        "searchmenu-exists": "'''Na laman ngön nan \"[[:$1]]\" bak wiki nyoe.'''",
        "searchmenu-new": "<strong>Peugöt laman \"[[:$1]]\" bak wiki nyoë!</strong> {{PLURAL:$2|0=|Eu cit laman nyang geurumpok nibak meunita droëneuh.|Eu cit hasé mita nyang geurumpok.}}",
-       "searchprofile-articles": "Miëng asoë",
+       "searchprofile-articles": "Laman asoë",
        "searchprofile-images": "Multimedia",
        "searchprofile-everything": "Ban dum",
        "searchprofile-advanced": "Tingkat lanjut",
        "searchprofile-articles-tooltip": "Mita bak $1",
        "searchprofile-images-tooltip": "Mita beureukaih",
-       "searchprofile-everything-tooltip": "Mita ban dum miëng asoë (rôh ôn marit)",
+       "searchprofile-everything-tooltip": "Mita ban dum laman asoë (rôh ôn marit)",
        "searchprofile-advanced-tooltip": "Mita bak ruweuëng nan meupat-pat",
        "search-result-size": "$1 ({{PLURAL:$2|1 narit|$2 narit}})",
        "search-result-category-size": "{{PLURAL:$1|1 anggeeta|$1 anggeeta}} ({{PLURAL:$2|1 aneuk kawan|$2 aneuk kawan}}, {{PLURAL:$3|1 beureukaih|$3 beureukaih}})",
        "prefs-email": "Peuniléh surat-e",
        "prefs-rendering": "Neuleumah",
        "saveprefs": "Keubah",
-       "prefs-editing": "Neuandam",
+       "prefs-editing": "Peusaneut",
        "searchresultshead": "Mita",
        "stub-threshold-disabled": "Geupeumaté",
        "timezoneuseoffset": "La'én (peuteuntèe bidajih)",
        "recentchanges-summary": "Neukalön nyang ban meuubah bak wiki lam laman nyoe.",
        "recentchanges-noresult": "Hana neuubah lam lheuëng watèë nyoë nyang paih ngön syarat",
        "recentchanges-feed-description": "Seutöt neuubah barô lam wiki bak umpeuën nyoë.",
-       "recentchanges-label-newpage": "Neuandam nyoë jipeugöt miëng barô",
-       "recentchanges-label-minor": "Nyoe neuandam ubeut",
-       "recentchanges-label-bot": "Neuandam nyoe geupubuet le bot",
-       "recentchanges-label-unpatrolled": "Neuandam nyoe goh lom geukalon",
-       "recentchanges-label-plusminus": "Seunipat miëng geugantoë lé jeumeulah bita nyoë",
+       "recentchanges-label-newpage": "Hasé peusaneut nyoë jipeugöt laman barô",
+       "recentchanges-label-minor": "Nyoe geupeusaneut bacut",
+       "recentchanges-label-bot": "Geupeusaneut lé bot",
+       "recentchanges-label-unpatrolled": "Hasé peusaneut nyoe goh lom geukalon",
+       "recentchanges-label-plusminus": "Seunipat laman geugantoë lé jeumeulah bita nyoë",
        "recentchanges-legend-heading": "<strong>Hareutoë:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (eu cit [[Special:NewPages|dapeuta laman barô]])",
        "rcfilters-legend-heading": "<strong>Dapeuta seuningkat:</strong>",
        "rcfilters-hours-title": "Jeum seuneulheueh",
        "rcfilters-quickfilters": "Seunaréng teukeubah",
        "rcfilters-savedqueries-defaultlabel": "Seunaréng teukeubah",
+       "rcfilters-show-new-changes": "Peuleumah neuubah barô",
        "rcfilters-search-placeholder": "Saréng neuubah (ngui menu atawa plôk mita keu neusaréng nan)",
        "rcfilters-filter-editsbyself-label": "Atra lôn peusaneut",
        "rcfilters-filter-editsbyother-label": "Atra geupeusaneut lé gop",
        "recentchangeslinked-feed": "Neuubah meuhubông",
        "recentchangeslinked-toolbox": "Neuubah teukaw'èt",
        "recentchangeslinked-title": "Neuubah nyang meukaw'èt ngön $1",
-       "recentchangeslinked-summary": "Neupasoe saboh nan mieng keu neueu neuubah bak mieng nyang mupawôt u atawa nibak mieng nyan. (Keu neueu anggèeta nibak saboh kawan, neupasoe {{ns:category}}:Nan kawan). Neuubah bak mieng nyang na bak [[Special:Watchlist|dapeuta keunalön]] teutuléh <strong>teubai</strong>.",
-       "recentchangeslinked-page": "Nan miëng:",
+       "recentchangeslinked-summary": "Neupasoe saboh nan laman keu neukalon neuubah bak laman nyang mupawôt u atawa nibak laman nyan. (Keu neukalon anggèeta nibak saboh kawan, neupasoe {{ns:category}}:Nan kawan). Neuubah bak laman nyang na bak [[Special:Watchlist|dapeuta keunalön]] teutuléh <strong>teubai</strong>.",
+       "recentchangeslinked-page": "Nan laman:",
        "recentchangeslinked-to": "Peuleumah neuubah nibak laman-laman nyang mupawôt ngön laman nyang geubri",
        "upload": "Peutamöng beureukaih",
        "uploadbtn": "Peutamong beureukaih",
        "mimesearch": "Mita MIME",
        "listredirects": "Dapeuta peuninah",
        "unusedtemplates": "Templat nyang hana geungui",
-       "randompage": "Miëng baranggari",
+       "randompage": "Laman baranggari",
        "randomredirect": "Peuninah saban sakri",
        "statistics": "Keunira",
        "doubleredirects": "Peuninah ganda",
        "protectedpages-noredirect": "Peusom peuninah",
        "listusers": "Dapeuta ureuëng ngui",
        "usercreated": "{{GENDER:$3|Geupeugot}} bak $1 poh $2",
-       "newpages": "Miëng barô",
+       "newpages": "Laman barô",
        "newpages-username": "Ureuëng ngui:",
        "ancientpages": "Laman paléng awai",
        "move": "Pupinah",
        "watch": "Kalön",
        "watchthispage": "Kalön ôn nyoë",
        "unwatch": "Bateuë kalön",
-       "watchlist-details": "Na {{PLURAL:$1|$1 mieng}} lam dapeuta kalön Droeneuh, hana rôh mieng marit",
+       "watchlist-details": "Na {{PLURAL:$1|$1 laman}} lam dapeuta keunalön-neuh (rôh laman marit).",
        "wlshowlast": "Peuleumah $1 jeum $2 uroe seuneulheueh",
        "watchlist-hide": "Peusom",
        "watchlist-options": "Peuniléh dapeuta kalön",
        "deleteotherreason": "Nyang la’én/choë la’én:",
        "deletereasonotherlist": "Choë la’én",
        "rollbacklink": "pulang",
-       "rollbacklinkcount": "peuriwang $1 {{PLURAL:$1|neuandam}}",
+       "rollbacklinkcount": "peuriwang $1 {{PLURAL:$1|hase peusaneut}}",
        "changecontentmodel-submit": "Gantoe",
        "protectlogpage": "Log lindông",
        "protectedarticle": "peulindông \"[[$1]]\"",
        "undelete-search-submit": "Mita",
        "namespace": "Ruweuëng nan:",
        "invert": "Peubalék peuniléh",
-       "tooltip-invert": "Neuceuë kutak nyoë keu neupeusom neuubah miëng lam ruweuëng nan nyang neupiléh (ngön ruweuëng nan teukaw`èt meunyö neuceuë)",
+       "tooltip-invert": "Neuceuë kutak nyoë keu neupeusom neuubah laman lam ruweuëng nan nyang neupiléh (ngön ruweuëng nan teukaw`èt meunyö neuceuë)",
        "namespace_association": "Ruweuëng nan meuhubông",
        "tooltip-namespace_association": "Neuceuë kutak nyoë keu neupeurôh ruweuëng nan marit atawa bhaih nyang teukaw`èt ngön ruweuëng nan teupiléh",
        "blanknamespace": "(Keue)",
        "sp-contributions-submit": "Mita",
        "whatlinkshere": "Peunawôt balék",
        "whatlinkshere-title": "Laman nyang mupawôt u $1",
-       "whatlinkshere-page": "Miëng:",
+       "whatlinkshere-page": "Laman:",
        "linkshere": "Laman-laman nyoë meupawôt u '''$2''':",
        "nolinkshere": "Hana halaman nyang teukaw'et u '''$2'''.",
        "isredirect": "laman peuninah",
        "movelogpage": "Log pinah",
        "movereason": "Choë:",
        "revertmove": "peuriwang",
-       "export": "Peuteubiët miëng",
+       "export": "Èkspor laman",
        "allmessages": "Peusan sistem",
        "allmessagesname": "Nan",
        "allmessagesdefault": "Naseukah pukok",
        "thumbnail-more": "Peurayek",
        "thumbnail_error": "Salah bak peugöt gamba cut: $1",
        "importlogpage": "Log impor",
-       "tooltip-pt-userpage": "Mieng {{GENDER:|ureueng ngui Droeneuh}}",
-       "tooltip-pt-mytalk": "Mieng {{GENDER:|marit Droeneuh}}",
+       "tooltip-pt-userpage": "Laman {{GENDER:|ureueng ngui Droeneuh}}",
+       "tooltip-pt-mytalk": "Laman {{GENDER:|marit Droeneuh}}",
        "tooltip-pt-preferences": "Atô",
        "tooltip-pt-watchlist": "Dapeuta laman nyang lônkalön",
        "tooltip-pt-mycontris": "Dapeuta beuneuri {{GENDER:|Droeneuh}}",
        "tooltip-pt-login": "Droëneuh geupadan keu neutamong log, bah pih nyan hana geupeuwajéb.",
        "tooltip-pt-logout": "Teubiët",
        "tooltip-pt-createaccount": "Droëneuh geupadan keu neupeugöt saboh akun ngön neutamöng; bah pih nyan hana wajéb",
-       "tooltip-ca-talk": "Marit miëng asoë",
+       "tooltip-ca-talk": "Marit laman asoë",
        "tooltip-ca-edit": "Peusaneut laman nyoe",
        "tooltip-ca-addsection": "Puphôn beunagi barô",
        "tooltip-ca-viewsource": "Laman nyoë geulindông.\nDroëneuh jeuët neu’eu nèjih mantöng.",
-       "tooltip-ca-history": "Geunantoë awai nibak miëng nyoë",
+       "tooltip-ca-history": "Geunantoë awai nibak laman nyoë",
        "tooltip-ca-protect": "Peulindông laman nyoë",
        "tooltip-ca-delete": "Sampôh laman nyoë",
        "tooltip-ca-move": "Pupinah laman nyoë",
-       "tooltip-ca-watch": "Tamah miëng nyoë u dapeuta kalön droëneuh",
+       "tooltip-ca-watch": "Tamah laman nyoë u dapeuta keunalön",
        "tooltip-ca-unwatch": "Sampôh laman nyoë nibak dapeuta kalön droëneuh",
        "tooltip-search": "Mita {{SITENAME}}",
        "tooltip-search-go": "Mita saboh laman ngon nan nyang peureuséh lagèë nyoë meunyo na",
-       "tooltip-search-fulltext": "Mita miëng nyang na asoë lagèë nyoë",
+       "tooltip-search-fulltext": "Mita laman nyang na asoë lagèë nyoë",
        "tooltip-p-logo": "Saweuë ôn keuë",
        "tooltip-n-mainpage": "Saweuë ôn keuë",
        "tooltip-n-mainpage-description": "Saweuë ôn keuë",
        "tooltip-n-recentchanges": "Dapeuta neuubah barô lam wiki.",
        "tooltip-n-randompage": "Peuleumah laman baranggari",
        "tooltip-n-help": "Bak mita bantu.",
-       "tooltip-t-whatlinkshere": "Dapeuta ban dum miëng wiki nyang mupawôt keunoë",
+       "tooltip-t-whatlinkshere": "Dapeuta ban dum laman wiki nyang mupawôt keunoë",
        "tooltip-t-recentchangeslinked": "Neuubah barô lam laman nyang meupawôt nibak laman nyoë",
        "tooltip-feed-rss": "Umpeuën RSS keu laman nyoë",
-       "tooltip-feed-atom": "Umpeuën Atom keu miëng nyoë",
+       "tooltip-feed-atom": "Umpeuën Atom keu laman nyoë",
        "tooltip-t-contributions": "Dapeuta beuneuri {{GENDER:$1|ureuëng ngui nyoë}}",
        "tooltip-t-emailuser": "Peu-ét surat elektronik keu {{GENDER:$1|ureueng ngui nyoe}}",
        "tooltip-t-upload": "Peutamong beureukaih",
        "tooltip-t-specialpages": "Dapeuta ban dum laman kusuih",
-       "tooltip-t-print": "Seunalén rakam miëng nyoë",
-       "tooltip-t-permalink": "Peunawôt teutap keu geunantoë miëng nyoë",
-       "tooltip-ca-nstab-main": "Eu miëng asoë",
-       "tooltip-ca-nstab-user": "Eu miëng ureuëng ngui",
-       "tooltip-ca-nstab-special": "Nyoë nakeuh miëng kusuih, ngön h’an jeuët geuandam.",
+       "tooltip-t-print": "Seunalén rakam laman nyoë",
+       "tooltip-t-permalink": "Peunawôt teutap keu geunantoë laman nyoë",
+       "tooltip-ca-nstab-main": "Kalön laman asoë",
+       "tooltip-ca-nstab-user": "Kalön laman ureuëng ngui",
+       "tooltip-ca-nstab-special": "Nyoë laman kusuih, h’an jeuët geupeusaneut.",
        "tooltip-ca-nstab-project": "Eu laman buët",
-       "tooltip-ca-nstab-image": "Eu miëng beureukaih",
+       "tooltip-ca-nstab-image": "Kalön laman beureukaih",
        "tooltip-ca-nstab-mediawiki": "Eu peusan sistem",
        "tooltip-ca-nstab-template": "Eu seunaleuëk",
        "tooltip-ca-nstab-help": "Eu laman beunantu",
-       "tooltip-ca-nstab-category": "Eu miëng kawan",
+       "tooltip-ca-nstab-category": "Kalön laman kawan",
        "tooltip-minoredit": "Peutanda nyoë sibagoë peusaneut bacut",
        "tooltip-save": "Keubah neuubah Droëneuh",
        "tooltip-preview": "Peuleumah neuubah Droëneuh, neungui nyoë sigohlom neukeubah!",
        "tooltip-diff": "Peuleumah neuubah nyang ka Droëneuh peugöt",
        "tooltip-compareselectedversions": "Ngiëng bida nibak duwa geunantoë laman nyang teupiléh",
        "tooltip-watch": "Tamah laman nyoë u dapeuta kalön droëneuh",
-       "tooltip-rollback": "\"Rollback\" jipeugisa keulayi neuandam ureueng tuléh seuneulheueh u laman nyoe ngön sigo teugön",
+       "tooltip-rollback": "\"Peuriwang\" jipeugisa atra geuandam lé ureueng tuléh seuneulheueh u laman nyoe ngön sigo teugön",
        "tooltip-undo": "Peuriwang geunantoë nyoë ngön peuhah kutak peusaneut ngön cara eu hasé dilèë. Alasan jeuët geutamah bak kuta èhtisa.",
        "tooltip-summary": "Pasoë éhtisa paneuk",
        "interlanguage-link-title": "$1 – $2",
        "pageinfo-title": "Keutrangan keu \"$1\"",
        "pageinfo-header-basic": "Keutrangan peuneuphôn",
        "pageinfo-header-edits": "Riwayat peusaneut",
-       "pageinfo-header-restrictions": "Lindông mieng",
+       "pageinfo-header-restrictions": "Lindông laman",
        "pageinfo-display-title": "Judul tampilan",
        "pageinfo-default-sort": "Gunci urôt baku",
-       "pageinfo-length": "Panyang mieng (lam bita)",
-       "pageinfo-article-id": "ID Mieng",
-       "pageinfo-language": "Bahsa asoe mieng",
-       "pageinfo-content-model": "Modèl asoe mieng",
+       "pageinfo-length": "Panyang laman (lam bit)",
+       "pageinfo-article-id": "ID Laman",
+       "pageinfo-language": "Bahsa asoe laman",
+       "pageinfo-content-model": "Modèl asoe laman",
        "pageinfo-robot-policy": "Geuindèks lé robot",
        "pageinfo-robot-index": "Geupeuidin",
        "pageinfo-robot-noindex": "Hana geupeuidin",
-       "pageinfo-watchers": "Jumeulah ureueng kalön mieng",
-       "pageinfo-redirects-name": "Jumeulah peuninah u mieng nyoe",
-       "pageinfo-firstuser": "Ureueng peugot mieng",
-       "pageinfo-firsttime": "Uroe buleuen pumeugot mieng",
+       "pageinfo-watchers": "Jumeulah ureueng kalön laman",
+       "pageinfo-redirects-name": "Jumeulah peuninah u laman nyoe",
+       "pageinfo-firstuser": "Ureueng peugöt laman",
+       "pageinfo-firsttime": "Uroe buleuen pumeugot laman",
        "pageinfo-lastuser": "Ureueng peusaneut seuneulheueh",
        "pageinfo-lasttime": "Uroe peusaneut seuneulheueh",
-       "pageinfo-edits": "Jumeulah neuandam ban dum",
+       "pageinfo-edits": "Jumeulah hasé peusaneut ban dum",
        "pageinfo-authors": "Jumeulah ban dum ureueng teumuléh nyang mubida",
-       "pageinfo-recent-edits": "Jumeulah neuandam ban-ban nyoe (lam $1 nyoe)",
-       "pageinfo-toolboxlink": "Keutrangan miëng",
+       "pageinfo-recent-edits": "Jumeulah peusaneut ban-ban nyoe (lam $1 nyoe)",
+       "pageinfo-toolboxlink": "Keutrangan laman",
        "pageinfo-contentpage-yes": "Nyo",
        "patrol-log-page": "Log patroli",
        "previousdiff": "← Bida awai",
        "nextdiff": "Geunantoë lheuëh nyan →",
        "file-info-size": "$1 × $2 piksel, rayek beureukaih: $3, MIME jeunèh: $4",
-       "file-info-size-pages": "$1 × $2 piksel, seunipat beureukaih: $3, jeunèh MIME: $4, $5 {{PLURAL:$5|mieng}}",
+       "file-info-size-pages": "$1 × $2 piksel, seunipat beureukaih: $3, jeunèh MIME: $4, $5 {{PLURAL:$5|laman}}",
        "file-nohires": "Hana resolusi nyang leubèh manyang.",
        "svg-long-desc": "Beureukah SVG, nominal $1 x $2 piksel, rayek beureukah: $3",
        "show-big-image": "Beureukaih aseuli",
        "confirm-unwatch-button": "Ka göt",
        "confirm-unwatch-top": "Sampôh laman nyoë nibak dapeuta keunalön droëneuh?",
        "imgmultipageprev": "← laman sigohlomjih",
-       "imgmultipagenext": "mieng lheueh nyoe →",
+       "imgmultipagenext": "laman u keue →",
        "imgmultigo": "Jak!",
-       "imgmultigoto": "Jak u mieng $1",
+       "imgmultigoto": "Jak u laman $1",
        "autosumm-new": "Geupeugöt laman ngön asoë '$1'",
        "watchlisttools-clear": "Sampôh dapeuta keunalön",
        "watchlisttools-view": "Peuleumah neuubah meukaw'èt",
        "redirect-lookup": "Mita",
        "redirect-value": "Nilai:",
        "redirect-user": "ID ureueng ngui",
-       "redirect-page": "ID Mieng",
-       "redirect-revision": "Pubeutoi mieng",
+       "redirect-page": "ID Laman",
+       "redirect-revision": "Pubeutoi laman",
        "redirect-file": "Nan beureukaih",
        "fileduplicatesearch-submit": "Mita",
        "specialpages": "Laman kusuih",
        "tags-active-yes": "Nyo",
        "tags-active-no": "H`an",
        "tags-hitcount": "$1 {{PLURAL:$1|neuubah}}",
-       "logentry-delete-delete": "$1 {{GENDER:$2|geusampôh}} miëng $3",
-       "logentry-move-move": "$1 {{GENDER:$2|geupinah}} mieng $3 u $4",
-       "logentry-move-move-noredirect": "$1 {{GENDER:$2|geupinah}} mieng $3 u $4 hana geubôh peuninah",
-       "logentry-move-move_redir": "$1 {{GENDER:$2|geupinah}} mieng $3 u $4 ateueh mieng peuninah",
-       "logentry-patrol-patrol-auto": "$1 otomatis {{GENDER:$2|geutanda}} revisi $4 nibak mieng $3 nyang geukawai",
+       "logentry-delete-delete": "$1 {{GENDER:$2|geusampôh}} laman $3",
+       "logentry-move-move": "$1 {{GENDER:$2|geupinah}} laman $3 u $4",
+       "logentry-move-move-noredirect": "$1 {{GENDER:$2|geupinah}} laman $3 u $4 hana geubôh peuninah",
+       "logentry-move-move_redir": "$1 {{GENDER:$2|geupinah}} laman $3 u $4 ateueh laman peuninah",
+       "logentry-patrol-patrol-auto": "$1 otomatis {{GENDER:$2|geutanda}} revisi $4 nibak laman $3 nyang geukawai",
        "logentry-newusers-create": "$1 {{GENDER:$2|geupeugöt}} akun ureuëng ngui",
        "logentry-upload-upload": "$1 {{GENDER:$2|geupasoe}} $3",
        "searchsuggest-search": "Mita {{SITENAME}}",
index 7c28e5e..ab0aa58 100644 (file)
@@ -26,7 +26,8 @@
                        "Matma Rex",
                        "Biggs ZA",
                        "Slashme",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Vlad5250"
                ]
        },
        "tog-underline": "Onderstreep skakels:",
        "right-editsemiprotected": "Wysig bladsye wat as \"{{int:protect-level-autoconfirmed}}\" beskerm is",
        "right-editinterface": "Wysig die gebruikerskoppelvlak",
        "right-editusercss": "Wysig ander gebruikers se CSS-lêers",
+       "right-edituserjson": "Wysig ander gebruikers se JSON-lêers",
        "right-edituserjs": "Wysig ander gebruikers se JS-lêers",
        "right-editmyusercss": "Wysig u persoonlike CSS-bladsy",
+       "right-editmyuserjson": "Wysig u persoonlike JSON-bladsy",
        "right-editmyuserjs": "Wysig u persoonlike JavaScript-bladsy",
        "right-viewmywatchlist": "Wys u persoonlike dophoulys",
        "right-editmywatchlist": "Wysig u persoonlike dophoulys. Sommige aksies sal steeds bladsye byvoeg, selfs sonder die bevoegdheid.",
index b7c069d..9191738 100644 (file)
        "badarticleerror": "لا يمكن إجراء هذا الفعل على هذه الصفحة.",
        "cannotdelete": "تعذّر حذف الصفحة أو الملف \"$1\".\nربما حذفها شخص آخر بالفعل.",
        "cannotdelete-title": "تعذّر حذف الصفحة \"$1\"",
+       "delete-scheduled": "تمت جدولة الصفحة \"$1\" للحذف،\nيُرجَى التحلي بالصبر.",
        "delete-hook-aborted": "أجهض خطّاف الحذف.\nلم يقدم أي توضيح.",
        "no-null-revision": "تعذر إنشاء مراجعة جديدة فارغة لصفحة \"$1\"",
        "badtitle": "عنوان سيء",
        "movepage-moved": "'''نُقِلت \"$1\" إلى \"$2\"'''",
        "movepage-moved-redirect": "أنشئت تحويلة.",
        "movepage-moved-noredirect": "إنشاء التحويلة تم التغاضي عنه.",
+       "movepage-delete-first": "تحتوي الصفحة الهدف على عدد كبير جدا من المراجعات لحذفها كجزء من نقل الصفحة; يُرجَى أولا حذف الصفحة يدويا، ثم إعادة المحاولة.",
        "articleexists": "توجد صفحة بهذا الاسم، أو أن الاسم الذي تم اختياره غير صالح.\nمن فضلك اختر اسم آخر.",
        "cantmove-titleprotected": "لا يمكنك نقل صفحة إلى هذا الموقع، لأن العنوان الجديد تمت حمايته ضد الإنشاء",
        "movetalk": "انقل صفحة النقاش المرفقة",
index 364a18d..f72a8fe 100644 (file)
        "badarticleerror": "Гэтае дзеяньне немагчыма выканаць на гэтай старонцы.",
        "cannotdelete": "Немагчыма выдаліць старонку альбо файл «$1». Магчыма, яна ўжо выдаленая кімсьці іншым.",
        "cannotdelete-title": "Немагчыма выдаліць старонку «$1»",
+       "delete-scheduled": "Старонка «$1» заплянаваная на выдаленьне.\nКалі ласка, будзьце цярплівымі.",
        "delete-hook-aborted": "Выдаленьне скасаванае працэдурай-перахопнікам.\nТлумачэньняў не было.",
        "no-null-revision": "Немагчыма стварыць нулявую вэрсію для старонкі «$1»",
        "badtitle": "Няслушная назва",
index 8fd3489..5b286d7 100644 (file)
        "badarticleerror": "Denne funktion kan ikke udføres på denne side.",
        "cannotdelete": "Kunne ikke slette siden eller filen \"$1\".\nDen kan være blevet slettet af en anden.",
        "cannotdelete-title": "Kan ikke slette siden \"$1\"",
+       "delete-scheduled": "Siden \"$1\" er sat til at blive slettet.\nHav tålmodighed.",
        "delete-hook-aborted": "Sletningen blev afbrudt af en programfunktion.\nDer blev ikke givet nogen forklaring.",
        "no-null-revision": "Kunne ikke oprette nye tom revision for side \"$1\"",
        "badtitle": "Ugyldig titel",
index 6e44fe3..8a2588a 100644 (file)
        "badarticleerror": "Diese Aktion kann auf diese Seite nicht angewendet werden.",
        "cannotdelete": "Die Seite oder Datei „$1“ konnte nicht gelöscht werden.\nMöglicherweise wurde sie bereits von jemand anderem gelöscht.",
        "cannotdelete-title": "Seite „$1“ kann nicht gelöscht werden",
+       "delete-scheduled": "Die Löschung der Seite „$1“ wurde eingeplant.\nBitte habe Geduld.",
        "delete-hook-aborted": "Die Löschung wurde von einer Programmerweiterung zu MediaWiki verhindert.\nEs ist hierzu keine Erklärung verfügbar.",
        "no-null-revision": "Die neue Nullversion für die Seite „$1“ konnte nicht erstellt werden",
        "badtitle": "Ungültiger Titel",
        "movepage-moved": "'''Die Seite „$1“ wurde nach „$2“ verschoben.'''",
        "movepage-moved-redirect": "Eine Weiterleitung wurde erstellt.",
        "movepage-moved-noredirect": "Die Erstellung einer Weiterleitung wurde unterdrückt.",
+       "movepage-delete-first": "Die Zielseite hat zu viele Versionen, um sie als Teil einer Seitenverschiebung zu löschen. Bitte lösche zuerst die Seite manuell und versuche es dann erneut.",
        "articleexists": "Unter diesem Namen existiert bereits eine Seite. Bitte wähle einen anderen Namen.",
        "cantmove-titleprotected": "Die Verschiebung kann nicht durchgeführt werden, da der Zieltitel zur Erstellung gesperrt ist.",
        "movetalk": "Sofern möglich, die Diskussionsseite mitverschieben",
index fde4b65..f3a81cc 100644 (file)
@@ -7,7 +7,8 @@
                        "아라",
                        "Shirayuki",
                        "Gloria sah",
-                       "Macofe"
+                       "Macofe",
+                       "Vlad5250"
                ]
        },
        "tog-underline": "Tîra 'na rîga sòta i colegamèint.",
        "right-editcontentmodel": "Mudéfica al mudèl ed còl ché dèinter int 'na pàgina.",
        "right-editinterface": "Mudéfica al colegamèint tra sistēma e utèint",
        "right-editusercss": "Mudéfica i file CSS 'd êter utèint",
+       "right-edituserjson": "Mudéfica i file JSON 'd êter utèint",
        "right-edituserjs": "Mudéfica i file JS 'd êter utèint",
        "right-editmyusercss": "Mudéfica i file CSS dal só utèint",
+       "right-editmyuserjson": "Mudéfica i file JSON dal só utèint",
        "right-editmyuserjs": "Mudéfica i file JavaScript dal só utèint",
        "right-viewmywatchlist": "Guêrda la lésta di tō tgnû 'd ôc specêl",
        "right-editmywatchlist": "Mudéfica i tō tgnu 'd ôc. Da nutêr che soquânti asiòun a prân incòra zuntêr dal pàgini ânca sèinsa avèiren al dirét.",
index 1d7f3f5..ea63054 100644 (file)
        "badarticleerror": "This action cannot be performed on this page.",
        "cannotdelete": "The page or file \"$1\" could not be deleted.\nIt may have already been deleted by someone else.",
        "cannotdelete-title": "Cannot delete page \"$1\"",
+       "delete-scheduled": "The page \"$1\" is scheduled for deletion.\nPlease be patient.",
        "delete-hook-aborted": "Deletion aborted by hook.\nIt gave no explanation.",
        "no-null-revision": "Could not create new null revision for page \"$1\"",
        "badtitle": "Bad title",
        "movepage-moved": "<strong>\"$1\" has been moved to \"$2\"</strong>",
        "movepage-moved-redirect": "A redirect has been created.",
        "movepage-moved-noredirect": "The creation of a redirect has been suppressed.",
+       "movepage-delete-first": "The target page has too many revisions to delete as part of a page move.  Please first delete the page manually, then try again.",
        "articleexists": "A page of that name already exists, or the name you have chosen is not valid.\nPlease choose another name.",
        "cantmove-titleprotected": "You cannot move a page to this location because the new title has been protected from creation.",
        "movetalk": "Move associated talk page",
index 0a8fd7c..f10a2cb 100644 (file)
        "badarticleerror": "Cette action ne peut pas être effectuée sur cette page.",
        "cannotdelete": "Impossible de supprimer la page ou le fichier « $1 ».\nLa suppression a peut-être déjà été effectuée par quelqu’un d’autre.",
        "cannotdelete-title": "Impossible de supprimer la page « $1 »",
+       "delete-scheduled": "La page « $1 » st programmée pour être supprimée.\nVeuillez patienter.",
        "delete-hook-aborted": "Suppression annulée par une extension.\nAucune explication n’a été fournie.",
        "no-null-revision": "Impossible de créer une nouvelle révision vide pour la page « $1 »",
        "badtitle": "Mauvais titre",
        "movepage-moved": "<strong>« $1 » a été renommée en « $2 »</strong>",
        "movepage-moved-redirect": "Une redirection depuis l’ancien nom a été créée.",
        "movepage-moved-noredirect": "La création d’une redirection depuis l’ancien nom a été annulée.",
+       "movepage-delete-first": "La page cible a trop de révisions à supprimer en tant que faisant partie du déplacement d’une page.\nVeuillez d’abord supprimer la page manuellement, puis réessayer.",
        "articleexists": "Il existe déjà une page portant ce titre, ou le titre que vous avez choisi n'est pas correct.\nVeuillez en choisir un autre.",
        "cantmove-titleprotected": "Vous ne pouvez pas déplacer une page vers cet emplacement car la création de page avec ce nouveau titre a été protégée.",
        "movetalk": "Renommer aussi la page de discussion associée",
index f59d497..6216762 100644 (file)
        "badarticleerror": "לא ניתן לבצע את הפעולה הזאת בדף זה.",
        "cannotdelete": "לא ניתן היה למחוק את הדף או את הקובץ \"$1\".\nייתכן שהוא כבר נמחק על־ידי מישהו אחר.",
        "cannotdelete-title": "לא ניתן למחוק את הדף \"$1\"",
+       "delete-scheduled": "הדף \"$1\" ממתין למחיקה.\nנא להמתין.",
        "delete-hook-aborted": "המחיקה הופסקה על־ידי מבנה Hook.\nלא ניתן הסבר.",
        "no-null-revision": "לא ניתן היה ליצור גרסת־דמה בדף \"$1\"",
        "badtitle": "כותרת שגויה",
        "movepage-moved": "<strong>הדף \"$1\" הועבר לשם \"$2\"</strong>",
        "movepage-moved-redirect": "נוצרה הפניה.",
        "movepage-moved-noredirect": "יצירת ההפניה בוטלה.",
+       "movepage-delete-first": "לדף היעד יש גרסאות רבות מדי לצורך מחיקה כחלק מהעברת דף.\nראשית יש למחוק את הדף ידנית, ואז לנסות שוב.",
        "articleexists": "קיים כבר דף באותו שם, או שהשם שבחרת אינו תקין.\nנא לבחור שם אחר.",
        "cantmove-titleprotected": "אין לך הרשאה להעביר את הדף לכאן, כי השם החדש מוגן מפני יצירה.",
        "movetalk": "העברה גם של דף השיחה",
index 82458eb..a7caa16 100644 (file)
        "badarticleerror": "Dës Aktioun kann net op dëser Säit duerchgefouert ginn.",
        "cannotdelete": "D'Bild oder d'Säit \"$1\" konnt net geläscht ginn.\nEt ka sinn datt et scho vun engem Anere geläscht gouf.",
        "cannotdelete-title": "D'Säit \"$1\" kann net geläscht ginn",
+       "delete-scheduled": "D'Läsche vun der Säit \"$1\" ass programméiert.\nHutt w.e.g. e bësse Gedold.",
        "delete-hook-aborted": "D'Läsche gouf vun enger Schnëttstell (hook) ofgebrach.\nEng Erklärung gouf net ginn.",
        "no-null-revision": "Déi nei Nullversioun fir d'Säit \"$1\" konnt net ugeluecht ginn",
        "badtitle": "Schlechten Titel",
index f146712..d133436 100644 (file)
@@ -39,7 +39,8 @@
                        "Nersip",
                        "Manvydasz",
                        "Fitoschido",
-                       "Matěj Suchánek"
+                       "Matěj Suchánek",
+                       "Vlad5250"
                ]
        },
        "tog-underline": "Nuorodos pabraukimas:",
        "right-editcontentmodel": "Redaguoti puslapio turinio modelį",
        "right-editinterface": "Keisti naudotojo aplinką",
        "right-editusercss": "Redaguoti kitų naudotojų CSS failus",
+       "right-edituserjson": "Redaguoti kitų naudotojų JSON failus",
        "right-edituserjs": "Redaguoti kitų naudotojų JS failus",
        "right-editmyusercss": "Redaguoti savo vartotojo CSS failus",
+       "right-editmyuserjson": "Redaguoti savo vartotojo JSON failus",
        "right-editmyuserjs": "Redaguokite savo naudotojo vartotojo JavaScript failus",
        "right-viewmywatchlist": "Peržiūrėti savo stebimųjų sąrašą",
        "right-editmywatchlist": "Keiskite savo stebimųjų sąrašą. Atminkite, kad kai kurie veiksmai vis vien pridės puslapius netgi be tokios teisės.",
index 86bca30..9080dbe 100644 (file)
        "badarticleerror": "Ова дејство не може да се спроведе на оваа страница.",
        "cannotdelete": "Страницата или податотеката „$1“ не можеше да се избрише.\nМожеби некој друг веќе ја избришал.",
        "cannotdelete-title": "Не можам да ја избришам страницата „$1“",
+       "delete-scheduled": "Страницата „$1“ е предвидена за бришење.\nВе молиме за трпеливост.",
        "delete-hook-aborted": "Бришењето е прекинато со пресретник.\nНе е дадено никакво образложение.",
        "no-null-revision": "Не можев да направам нова ништовна преработка на страницата „$1“",
        "badtitle": "Неисправен наслов",
        "movepage-moved": "'''„$1“ е преместена под името „$2“'''",
        "movepage-moved-redirect": "Направено е пренасочување.",
        "movepage-moved-noredirect": "Создавањето на пренасочување е оневозможено.",
+       "movepage-delete-first": "Целната страница има премногу преработки за да можат да се избришат како дел од преместување на страница. Најпрвин избришете ја страницата рачно, па потоа обидете се пак.",
        "articleexists": "Веќе постои страница со тоа име, или името што го одбравте е неважечко.\nИзберете друго име.",
        "cantmove-titleprotected": "Не може да ја преместите страницата на тоа место бидејќи саканиот наслов е заштитен од создавање.",
        "movetalk": "Премести ја и разговорната страница, ако е возможно.",
index 47f7790..f104bc5 100644 (file)
        "movepage-moved": "'''\"$1\" is hernoemd naar \"$2\"'''",
        "movepage-moved-redirect": "Er is een doorverwijzing aangemaakt.",
        "movepage-moved-noredirect": "Er is geen doorverwijzing aangemaakt.",
+       "movepage-delete-first": "De doelpagina heeft te veel versies om deze als deel van een paginahernoeming te verwijderen. Verwijder de pagina handmatig en probeer het daarna opnieuw.",
        "articleexists": "De pagina bestaat al of de paginanaam is ongeldig.\nKies een andere paginanaam.",
        "cantmove-titleprotected": "U kunt geen pagina naar deze naam hernoemen, omdat deze naam beveiligd is tegen het aanmaken ervan.",
        "movetalk": "Bijbehorende overlegpagina hernoemen",
index c4585f3..2ca765f 100644 (file)
        "badarticleerror": "Used as error message in moving page.\n\nSee also:\n* {{msg-mw|Articleexists}}\n* {{msg-mw|Bad-target-model}}",
        "cannotdelete": "Error message in deleting. Parameters:\n* $1 - page name or file name",
        "cannotdelete-title": "Title of error page when the user cannot delete a page. Parameters:\n* $1 - the page name",
+       "delete-scheduled": "Warning message shown when page deletion is deferred to the job queue, and therefore is not immediate.",
        "delete-hook-aborted": "Error message shown when an extension hook prevents a page deletion, but does not provide an error message.",
        "no-null-revision": "Error message shown when no null revision could be created to reflect a protection level change.\n\nAbout \"null revision\":\n* Create a new null-revision for insertion into a page's history. This will not re-save the text, but simply refer to the text from the previous version.\n* Such revisions can for instance identify page rename operations and other such meta-modifications.\n\nParameters:\n* $1 - page title",
        "badtitle": "The page title when a user requested a page with invalid page name. The content will be {{msg-mw|badtitletext}}.",
        "movepage-moved": "Message displayed after successfully moving a page from source to target name.\n\nParameters:\n* $1 - the source page as a link with display name\n* $2 - the target page as a link with display name\n* $3 - (optional) the source page name without a link\n* $4 - (optional) the target page name without a link\nSee also:\n* {{msg-mw|Movepage-moved-redirect}}\n* {{msg-mw|Movepage-moved-noredirect}}",
        "movepage-moved-redirect": "See also:\n* {{msg-mw|Movepage-moved}}\n* {{msg-mw|Movepage-moved-noredirect}}",
        "movepage-moved-noredirect": "The message is shown after pagemove if checkbox \"{{int:move-leave-redirect}}\" was unselected before moving.\n\nSee also:\n* {{msg-mw|Movepage-moved}}\n* {{msg-mw|Movepage-moved-redirect}}",
+       "movepage-delete-first": "Error message shown when trying to move a page and delete the existing page by that name, but the existing page has too many revisions.",
        "articleexists": "Used as error message when moving a page.\n\nSee also:\n* {{msg-mw|Badarticleerror}}\n* {{msg-mw|Bad-target-model}}",
        "cantmove-titleprotected": "Used as error message when moving a page.",
        "movetalk": "The text of the checkbox to watch the associated talk page to the page you are moving. This only appears when the talk page is not empty. Used in [[Special:MovePage]].\n\nSee also:\n* {{msg-mw|Move-page-legend|legend for the form}}\n* {{msg-mw|newtitle|label for new title}}\n* {{msg-mw|Movereason|label for textarea}}\n* {{msg-mw|Move-leave-redirect|label for checkbox}}\n* {{msg-mw|Fix-double-redirects|label for checkbox}}\n* {{msg-mw|Move-subpages|label for checkbox}}\n* {{msg-mw|Move-talk-subpages|label for checkbox}}\n* {{msg-mw|Move-watch|label for checkbox}}",
index b0344aa..8b7585a 100644 (file)
        "badarticleerror": "Quest'azione non ge pò essere fatte sus 'a sta pàgene.",
        "cannotdelete": "'A pàgene o 'u file \"$1\" non ge pò essere scangellate.\nPò essere ca ggià ha state scangellete da quacche otre.",
        "cannotdelete-title": "Non ge puè scangellà 'a pàgene \"$1\"",
+       "delete-scheduled": "'A pàgene \"$1\" jè schedulate pa scangellazione.\nPe piacere tìne 'nu picche de pascenze.",
        "delete-hook-aborted": "Cangiamende annullete da  'nu ''hook''.\nNon g'à date nisciune mutive.",
        "no-null-revision": "Non ge se pò ccrejà 'na revisiona nove vacande pa pàgene \"$1\"",
        "badtitle": "Titele sbagliete",
        "rcfilters-exclude-button-on": "Scettanne le scacchiate",
        "rcfilters-view-tags": "Cangiaminde taggate",
        "rcfilters-liveupdates-button": "Aggiornaminde in tiembe reale",
+       "rcfilters-liveupdates-button-title-on": "Stute le aggiornaminde automatece",
        "rcnotefrom": "Sotte {{PLURAL:$5|ste 'u cangiamende|stonne le cangiaminde}} da <strong>$3, $4</strong> ('nzigne a <strong>$1</strong> fatte vedè).",
        "rclistfrom": "Fà vedè le urteme cangiaminde partenne da $3 $2",
        "rcshowhideminor": "$1 cangiaminde stuèdeche",
        "recentchangeslinked-to": "Fa vedè le cangiaminde de le pàggene colleghete a 'na certa pàgene",
        "recentchanges-page-added-to-category": "[[:$1]] aggiunde a categorije",
        "recentchanges-page-removed-from-category-bundled": "[[:$1]] luate da 'a categorije, [[Special:WhatLinksHere/$1|sta vôsce ste sckaffate jndr'à otre pàggene]]",
+       "autochange-username": "Cangiamende automateche de MediaUicchi",
        "upload": "Careche 'u file",
        "uploadbtn": "Careche 'nu fail",
        "reuploaddesc": "Scangille 'u carecamende e tuerne a 'a schermete de le carecaminde",
        "uploadstash-bad-path-invalid": "Percorse invalide.",
        "uploadstash-bad-path-unknown-type": "Tipe scanusciute \"$1\".",
        "uploadstash-bad-path-unrecognized-thumb-name": "Nome d'a miniature non acchiate.",
+       "uploadstash-bad-path-bad-format": "'A chiave \"$1\" non ge ste jndr'à 'nu formate appropriate.",
        "invalid-chunk-offset": "distanze d'u chunk invalide",
        "img-auth-accessdenied": "Accesse negate",
        "img-auth-nopathinfo": "No se iacchie 'u percorse d'a 'mbormnazione.\n'U server tune adda essere 'mbostate pe passà sta 'le variabbile REQUEST_URI e/o PATH_INFO.\nCe jè accussì, pruève a abbilità $wgUsePathInfo.\n'Ndruche https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "apisandbox-dynamic-parameters-add-placeholder": "Nome d'u parametre",
        "apisandbox-dynamic-error-exists": "'Nu parametre chiamate \"$1\" ggià esiste.",
        "apisandbox-deprecated-parameters": "Parametre ca non già penzà",
+       "apisandbox-add-multi": "Aggiunge",
+       "apisandbox-submit-invalid-fields-title": "Quacche cambe non g'è valide",
        "apisandbox-results": "Resultate",
        "apisandbox-request-format-url-label": "Stringhe d'a 'nderrogazione de l'URL",
        "apisandbox-request-url-label": "URL richieste:",
        "tooltip-ca-nstab-category": "Vide a pàgene de le categorije",
        "tooltip-minoredit": "Signe cumme a 'nu cangiaminde stuèdeche",
        "tooltip-save": "Reggistre le cangiaminde ca è fatte",
+       "tooltip-publish": "Pubbleche le cangiaminde tune",
        "tooltip-preview": "Fà l'andeprime de le cangiaminde ca ste face. Pe piacere falle prima cu reggistre 'a vôsce!",
        "tooltip-diff": "Fà vedè ce cangiaminde e fatte a 'u teste.",
        "tooltip-compareselectedversions": "Vide le differenze 'mbrà le doje versiune selezionete de sta pàgene.",
        "group-bot.css": "/* 'U CSS ca se iacchie aqquà ave effettue seulamende sus a le bot */",
        "group-sysop.css": "/* 'U CSS ca se iacchie aqquà ave effettue seulamende sus a le utinde amministrature */",
        "group-bureaucrat.css": "/* 'U CSS ca se iacchie aqquà ave effettue seulamende sus a le utinde burocrate */",
+       "common.json": "/* Ogne JSON aqquà avène carecate pe tutte le utinde sus a ogne pàgene carecate. */",
        "common.js": "/* Ogne JavaScript aqquà avène carecate pe tutte le utinde sus a ogne pàgene carecate. */",
        "group-autoconfirmed.js": "/* Ogne JavaScript aqquà avène carecate pe le utinde autoconfirmatarie */",
        "group-bot.js": "/* Ogne JavaScript aqquà avène carecate pe le bot */",
        "confirm-unwatch-button": "OK",
        "confirm-unwatch-top": "Vuè ccu live sta pàgene da chidde condrollate?",
        "confirm-rollback-button": "OK",
+       "confirm-mcrundo-title": "Annulle 'u cangiamende",
+       "mcrundofailed": "Annullamende fallite",
        "semicolon-separator": ";&#32;",
        "comma-separator": ",&#32;",
        "colon-separator": ":&#32;",
        "compare-title-not-exists": "'U titele ca è specificate non g'esiste.",
        "compare-revision-not-exists": "'A revisione ca è specificate non g'esiste.",
        "diff-form": "Differenze",
+       "permanentlink-revid": "ID d'a revisione",
        "dberr-problems": "Sime spiacende! Stu site stè 'ngondre de le difficoltà tecniche.",
        "dberr-again": "Aspitte quacche minute e pò recareche.",
        "dberr-info": "(Non ge riuscime a trasè sus a'u server d'u database: $1)",
index ebeed7d..a3b042a 100644 (file)
        "badarticleerror": "Это действие не может быть выполнено на данной странице.",
        "cannotdelete": "Невозможно удалить или переименовать страницу или файл «$1».\nВозможно, уже было произведено удаление.",
        "cannotdelete-title": "Нельзя удалить страницу «$1»",
+       "delete-scheduled": "Страница «$1» запланирована для удаления.\nБудьте терпеливы.",
        "delete-hook-aborted": "Правка отменена процедурой-перехватчиком.\nДополнительных пояснений не приведено.",
        "no-null-revision": "Не удалось создать новую нулевую правку для страницы «$1»",
        "badtitle": "Недопустимое название",
        "movepage-moved": "'''Страница «$1» переименована в «$2»'''",
        "movepage-moved-redirect": "Было создано перенаправление.",
        "movepage-moved-noredirect": "Создание перенаправления было подавлено.",
+       "movepage-delete-first": "У целевой страницы слишком много версий для удаления как части переименования страницы. Сначала удалите эту страницу вручную, а затем повторите попытку.",
        "articleexists": "Страница с таким именем уже существует или указанное вами название недопустимо.\nПожалуйста, выберите другое название.",
        "cantmove-titleprotected": "Невозможно переименовать страницу, так как новое название входит в список запрещённых.",
        "movetalk": "Переименовать соответствующую страницу обсуждения",
        "filedelete-archive-read-only": "Архивная директория «$1» не доступна для записи веб-серверу.",
        "previousdiff": "← Предыдущая правка",
        "nextdiff": "Следующая правка →",
-       "mediawarning": "<strong>Внимание</strong>. Этот тип файла может содержать вредоносный программный код.\nПри его запуске ваша система может быть заражена.",
+       "mediawarning": "<strong>Внимание:</strong> Этот тип файла может содержать вредоносный программный код.\nПри его запуске ваша система может быть заражена.",
        "imagemaxsize": "Ограничение на размер изображения для страницы описания файла",
        "thumbsize": "Размер уменьшенной версии изображения:",
        "widthheight": "$1 × $2",
index ebec299..a35c06a 100644 (file)
        "listgrouprights-namespaceprotection-header": "නාමඅවකාශය සීමා",
        "listgrouprights-namespaceprotection-namespace": "නාමඅවකාශය",
        "listgrouprights-namespaceprotection-restrictedto": "පරිශීලක සංස්කරණය කිරීමට ඉඩ (ව) රයිට්",
-       "trackingcategories": "රà¶\82à¶\9c à·\81à·\92ලà·\8aපà·\93නà·\8a à¶´à·\8aරවර්ග",
+       "trackingcategories": "නà·\92රà·\93à¶\9aà·\8aà·\82ණ à¶´à·\8aâ\80\8dරවර්ග",
        "trackingcategories-summary": "ස්වයංක්රීයව මාධ්යවිකි මෘදුකාංග විසින් ජනාකීර්ණ වන කාණ්ඩ සොයා ගැනීමට මෙම පිටුවෙහි ලැයිස්තුගත. නාමඅවකාශය: ඔවුන්ගේ නම් {{ns:8}} අදාළ පද්ධති පණිවුඩ වෙනස් වෙනස් කළ හැක.",
        "trackingcategories-msg": "ට්රැකින් ප්රවර්ගය",
        "trackingcategories-name": "පණිවිඩය නම",
index be41b60..b4049af 100644 (file)
        "badarticleerror": "Na tej strani dejanja ne morem izvesti. Morda je bila stran med predložitvijo vaše zahteve že izbrisana.",
        "cannotdelete": "Strani ali datoteke »$1« ni mogoče izbrisati.\nMorda jo je izbrisal že kdo drug.",
        "cannotdelete-title": "Ne morem izbrisati strani »$1«",
+       "delete-scheduled": "Stran »$1« je načrtovana za brisanje.\nProsimo, bodite strpni.",
        "delete-hook-aborted": "Zanka je prekinila brisanje.\nVrnila ni nobene razlage.",
        "no-null-revision": "Ne morem ustvariti nove ničelne redakcije strani »$1«",
        "badtitle": "Nepravilen naslov",
        "right-editsitecss": "Urejanje CSS spletišča",
        "right-editsitejson": "Urejanje JSON spletišča",
        "right-editsitejs": "Urejanje JavaScripta spletišča",
-       "right-editmyusercss": "Uredite svoje uporabniške datoteke CSS",
+       "right-editmyusercss": "Urejanje svojih uporabniških datotek CSS",
        "right-editmyuserjson": "Urejanje svojih uporabniških datotek JSON",
-       "right-editmyuserjs": "Uredite svoje uporabniške datoteke JavaScript",
+       "right-editmyuserjs": "Urejanje svojih uporabniških datotek JavaScript",
        "right-viewmywatchlist": "Ogledovanje svojega spiska nadzorov",
        "right-editmywatchlist": "Urejanje vašega spiska nadzorov. Vedite, da bodo nekatera dejanja dodala strani nanj tudi brez te pravice.",
        "right-viewmyprivateinfo": "Ogled svojih zasebnih podatkov (npr. e-poštnega naslova, pravega imena)",
        "movepage-moved": "Stran '''»$1«''' je prestavljena na naslov '''»$2«'''.",
        "movepage-moved-redirect": "Preusmeritev je bila ustvarjena.",
        "movepage-moved-noredirect": "Izdelava preusmeritve je bila zatrta.",
+       "movepage-delete-first": "Ciljna stran ima preveč redakcij, da bi jo lahko izbrisali kot del premikanja strani. Prosimo, da najprej ročno izbrišete stran in nato poskusite znova.",
        "articleexists": "Izbrano ime je že zasedeno ali pa ni veljavno.\nProsimo, izberite drugo ime.",
        "cantmove-titleprotected": "Strani ne morete premakniti na slednjo lokacijo, saj je nov naslov zaščiten pred ustvarjanjem",
        "movetalk": "Prestavi tudi pogovorno stran",
index 21cccf3..8784722 100644 (file)
        "badarticleerror": "Ова радња се не може извршити на овој страници.",
        "cannotdelete": "Није могуће избрисати страницу или датотеку „$1”.\nМогуће је да ју је неко већ избрисао.",
        "cannotdelete-title": "Није могуће избрисати страницу „$1”",
+       "delete-scheduled": "Страница „$1” је заказана за брисање.\nБудите стрпљиви.",
        "delete-hook-aborted": "Брисање је прекинула кука.\nНије дато никакво образложење.",
        "no-null-revision": "Није могуће направити нову ништавну измену странице „$1”",
        "badtitle": "Лош наслов",
        "movepage-moved": "<strong>Страница „$1“ је премештена на наслов „$2“</strong>",
        "movepage-moved-redirect": "Преусмерење је направљено.",
        "movepage-moved-noredirect": "Стварање преусмерења је онемогућено.",
+       "movepage-delete-first": "Циљна страница има превише измена за брисање као део премештања странице.  Прво ручно избришите страницу, па покушајте поново.",
        "articleexists": "Страница са тим именом већ постоји или име које сте одабрали није важеће.\nОдаберите друго.",
        "cantmove-titleprotected": "Не можете да преместите страницу на ову локацију јер је прављење новог наслова заштићено.",
        "movetalk": "Премести и страницу за разговор",
index 6c34646..2e84b26 100644 (file)
                        "Wxyveronica",
                        "夢蝶葬花",
                        "Dcljr",
-                       "Phenolla"
+                       "Phenolla",
+                       "Hello903hello"
                ]
        },
        "tog-underline": "链接下划线:",
index b70cdf7..2f74004 100644 (file)
        "badarticleerror": "無法在此頁進行該操作。",
        "cannotdelete": "無法刪除頁面或檔案 \"$1\"。\n它可能已經被其他人刪除。",
        "cannotdelete-title": "無法刪除頁面 \"$1\"",
+       "delete-scheduled": "頁面「$1」已被安排刪除。\n請耐心等待。",
        "delete-hook-aborted": "刪除已被 Hook 中止。\n且未回應無任何說明。",
        "no-null-revision": "無法建立頁面 \"$1\" 的新空白修訂",
        "badtitle": "無效的標題",
        "movepage-moved": "<strong>已移動 \"$1\" 至 \"$2\"</strong>",
        "movepage-moved-redirect": "已建立重新導向頁面。",
        "movepage-moved-noredirect": "已取消建立重新導向頁面。",
+       "movepage-delete-first": "目標頁面有太多修訂,而無法刪除作為頁面移動的部份。請先手動刪除頁面後再重試。",
        "articleexists": "該頁面名稱已存在,或您選擇的名稱無效。\n請改選擇其他名稱。",
        "cantmove-titleprotected": "您選擇的新標題已被禁止使用,您不可移動頁面到該位置。",
        "movetalk": "移動相關的對話頁面",
index 0f3c506..9e35687 100644 (file)
@@ -108,7 +108,7 @@ class DeleteBatch extends Maintenance {
                        }
                        $page = WikiPage::factory( $title );
                        $error = '';
-                       $success = $page->doDeleteArticle( $reason, false, 0, true, $error, $user );
+                       $success = $page->doDeleteArticle( $reason, false, null, null, $error, $user, true );
                        if ( $success ) {
                                $this->output( " Deleted!\n" );
                        } else {
index eb03b38..d4a7bef 100644 (file)
@@ -21,7 +21,7 @@
  * @ingroup Maintenance
  */
 
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\SlotRecord;
 
 require_once __DIR__ . '/Maintenance.php';
 
index 1b9dac0..938503c 100644 (file)
@@ -40,8 +40,6 @@ in the load balancer, usually indicating a replication environment.' );
        }
 
        public function execute() {
-               global $wgActorTableSchemaMigrationStage;
-
                $dbw = $this->getDB( DB_MASTER );
 
                // Autodetect mode...
@@ -56,15 +54,6 @@ in the load balancer, usually indicating a replication environment.' );
 
                $actorQuery = ActorMigration::newMigration()->getJoin( 'rev_user' );
 
-               $needSpecialQuery = ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD &&
-                       $wgActorTableSchemaMigrationStage !== MIGRATION_NEW );
-               if ( $needSpecialQuery ) {
-                       foreach ( $actorQuery['joins'] as &$j ) {
-                               $j[0] = 'JOIN'; // replace LEFT JOIN
-                       }
-                       unset( $j );
-               }
-
                if ( $backgroundMode ) {
                        $this->output( "Using replication-friendly background mode...\n" );
 
@@ -77,54 +66,15 @@ in the load balancer, usually indicating a replication environment.' );
                        for ( $min = 0; $min <= $lastUser; $min += $chunkSize ) {
                                $max = $min + $chunkSize;
 
-                               if ( $needSpecialQuery ) {
-                                       // Use separate subqueries to collect counts with the old
-                                       // and new schemas, to avoid having to do whole-table scans.
-                                       $result = $dbr->select(
-                                               [
-                                                       'user',
-                                                       'rev1' => '('
-                                                               . $dbr->selectSQLText(
-                                                                       [ 'revision', 'revision_actor_temp' ],
-                                                                       [ 'rev_user', 'ct' => 'COUNT(*)' ],
-                                                                       [
-                                                                               "rev_user > $min AND rev_user <= $max",
-                                                                               'revactor_rev' => null,
-                                                                       ],
-                                                                       __METHOD__,
-                                                                       [ 'GROUP BY' => 'rev_user' ],
-                                                                       [ 'revision_actor_temp' => [ 'LEFT JOIN', 'revactor_rev = rev_id' ] ]
-                                                               ) . ')',
-                                                       'rev2' => '('
-                                                               . $dbr->selectSQLText(
-                                                                       [ 'revision' ] + $actorQuery['tables'],
-                                                                       [ 'actor_user', 'ct' => 'COUNT(*)' ],
-                                                                       "actor_user > $min AND actor_user <= $max",
-                                                                       __METHOD__,
-                                                                       [ 'GROUP BY' => 'actor_user' ],
-                                                                       $actorQuery['joins']
-                                                               ) . ')',
-                                               ],
-                                               [ 'user_id', 'user_editcount' => 'COALESCE(rev1.ct,0) + COALESCE(rev2.ct,0)' ],
-                                               "user_id > $min AND user_id <= $max",
-                                               __METHOD__,
-                                               [],
-                                               [
-                                                       'rev1' => [ 'LEFT JOIN', 'user_id = rev_user' ],
-                                                       'rev2' => [ 'LEFT JOIN', 'user_id = actor_user' ],
-                                               ]
-                                       );
-                               } else {
-                                       $revUser = $actorQuery['fields']['rev_user'];
-                                       $result = $dbr->select(
-                                               [ 'user', 'rev' => [ 'revision' ] + $actorQuery['tables'] ],
-                                               [ 'user_id', 'user_editcount' => "COUNT($revUser)" ],
-                                               "user_id > $min AND user_id <= $max",
-                                               __METHOD__,
-                                               [ 'GROUP BY' => 'user_id' ],
-                                               [ 'rev' => [ 'LEFT JOIN', "user_id = $revUser" ] ] + $actorQuery['joins']
-                                       );
-                               }
+                               $revUser = $actorQuery['fields']['rev_user'];
+                               $result = $dbr->select(
+                                       [ 'user', 'rev' => [ 'revision' ] + $actorQuery['tables'] ],
+                                       [ 'user_id', 'user_editcount' => "COUNT($revUser)" ],
+                                       "user_id > $min AND user_id <= $max",
+                                       __METHOD__,
+                                       [ 'GROUP BY' => 'user_id' ],
+                                       [ 'rev' => [ 'LEFT JOIN', "user_id = $revUser" ] ] + $actorQuery['joins']
+                               );
 
                                foreach ( $result as $row ) {
                                        $dbw->update( 'user',
@@ -149,41 +99,15 @@ in the load balancer, usually indicating a replication environment.' );
                        $this->output( "Using single-query mode...\n" );
 
                        $user = $dbw->tableName( 'user' );
-                       if ( $needSpecialQuery ) {
-                               $subquery1 = $dbw->selectSQLText(
-                                       [ 'revision', 'revision_actor_temp' ],
-                                       [ 'COUNT(*)' ],
-                                       [
-                                               'user_id = rev_user',
-                                               'revactor_rev' => null,
-                                       ],
-                                       __METHOD__,
-                                       [],
-                                       [ 'revision_actor_temp' => [ 'LEFT JOIN', 'revactor_rev = rev_id' ] ]
-                               );
-                               $subquery2 = $dbw->selectSQLText(
-                                       [ 'revision' ] + $actorQuery['tables'],
-                                       [ 'COUNT(*)' ],
-                                       'user_id = actor_user',
-                                       __METHOD__,
-                                       [],
-                                       $actorQuery['joins']
-                               );
-                               $dbw->query(
-                                       "UPDATE $user SET user_editcount=($subquery1) + ($subquery2)",
-                                       __METHOD__
-                               );
-                       } else {
-                               $subquery = $dbw->selectSQLText(
-                                       [ 'revision' ] + $actorQuery['tables'],
-                                       [ 'COUNT(*)' ],
-                                       [ 'user_id = ' . $actorQuery['fields']['rev_user'] ],
-                                       __METHOD__,
-                                       [],
-                                       $actorQuery['joins']
-                               );
-                               $dbw->query( "UPDATE $user SET user_editcount=($subquery)", __METHOD__ );
-                       }
+                       $subquery = $dbw->selectSQLText(
+                               [ 'revision' ] + $actorQuery['tables'],
+                               [ 'COUNT(*)' ],
+                               [ 'user_id = ' . $actorQuery['fields']['rev_user'] ],
+                               __METHOD__,
+                               [],
+                               $actorQuery['joins']
+                       );
+                       $dbw->query( "UPDATE $user SET user_editcount=($subquery)", __METHOD__ );
                }
 
                $this->output( "Done!\n" );
index edd5dda..5e27ac8 100644 (file)
@@ -45,9 +45,9 @@ class MigrateActors extends LoggedUpdateMaintenance {
        protected function doDBUpdates() {
                global $wgActorTableSchemaMigrationStage;
 
-               if ( $wgActorTableSchemaMigrationStage < MIGRATION_WRITE_NEW ) {
+               if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
                        $this->output(
-                               "...cannot update while \$wgActorTableSchemaMigrationStage < MIGRATION_WRITE_NEW\n"
+                               "...cannot update while \$wgActorTableSchemaMigrationStage lacks SCHEMA_COMPAT_WRITE_NEW\n"
                        );
                        return false;
                }
@@ -266,7 +266,6 @@ class MigrateActors extends LoggedUpdateMaintenance {
                                        $table,
                                        [
                                                $actorField => $row->actor_id,
-                                               $nameField => '',
                                        ],
                                        array_intersect_key( (array)$row, $pkFilter ) + [
                                                $actorField => 0
@@ -377,7 +376,6 @@ class MigrateActors extends LoggedUpdateMaintenance {
                                }
                                $this->beginTransaction( $dbw, __METHOD__ );
                                $dbw->insert( $newTable, $inserts, __METHOD__ );
-                               $dbw->update( $table, [ $nameField => '' ], [ $primaryKey => $updates ], __METHOD__ );
                                $countUpdated += $dbw->affectedRows();
                                $this->commitTransaction( $dbw, __METHOD__ );
                        }
index 93d5baf..644ff87 100644 (file)
@@ -20,8 +20,8 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\NameTableStore;
-use MediaWiki\Storage\SlotRecord;
 use MediaWiki\Storage\SqlBlobStore;
 use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\IDatabase;
index 589be48..e80b6f6 100644 (file)
@@ -117,15 +117,17 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                        $tables = [ self::$tableMap[$prefix] ];
                                        $fields = [];
                                        $joins = [];
-                                       if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
+                                               // Read the old fields if we're still writing them regardless of read mode, to handle upgrades
                                                $fields['userid'] = $prefix . '_user';
                                                $fields['username'] = $prefix . '_user_text';
                                        }
-                                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
+                                               // Read the new fields if we're writing them regardless of read mode, to handle upgrades
                                                if ( $prefix === 'rev' ) {
                                                        $tables[] = 'revision_actor_temp';
                                                        $joins['revision_actor_temp'] = [
-                                                               $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+                                                               ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ? 'LEFT JOIN' : 'JOIN',
                                                                'rev_id = revactor_rev',
                                                        ];
                                                        $fields['actorid'] = 'revactor_actor';
@@ -147,11 +149,13 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                        $log->addRelations( 'log_id', $items, $row->log_id );
                                        // Query item author relations...
                                        $fields = [];
-                                       if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
+                                               // Read the old fields if we're still writing them regardless of read mode, to handle upgrades
                                                $fields['userid'] = 'log_user';
                                                $fields['username'] = 'log_user_text';
                                        }
-                                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
+                                               // Read the new fields if we're writing them regardless of read mode, to handle upgrades
                                                $fields['actorid'] = 'log_actor';
                                        }
 
@@ -163,14 +167,14 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                // Add item author relations...
                                $userIds = $userIPs = $userActors = [];
                                foreach ( $sres as $srow ) {
-                                       if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                                if ( $srow->userid > 0 ) {
                                                        $userIds[] = intval( $srow->userid );
                                                } elseif ( $srow->username != '' ) {
                                                        $userIPs[] = $srow->username;
                                                }
                                        }
-                                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                                if ( $srow->actorid ) {
                                                        $userActors[] = intval( $srow->actorid );
                                                } elseif ( $srow->userid > 0 ) {
@@ -181,11 +185,11 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                        }
                                }
                                // Add item author relations...
-                               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                        $log->addRelations( 'target_author_id', $userIds, $row->log_id );
                                        $log->addRelations( 'target_author_ip', $userIPs, $row->log_id );
                                }
-                               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                        $log->addRelations( 'target_author_actor', $userActors, $row->log_id );
                                }
                        }
index d90a4a7..98025d1 100644 (file)
@@ -136,20 +136,19 @@ class ReassignEdits extends Maintenance {
                        if ( $total ) {
                                # Reassign edits
                                $this->output( "\nReassigning current edits..." );
-                               if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                        $dbw->update(
                                                'revision',
                                                [
                                                        'rev_user' => $to->getId(),
-                                                       'rev_user_text' =>
-                                                               $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ? $to->getName() : ''
+                                                       'rev_user_text' => $to->getName(),
                                                ],
                                                $from->isLoggedIn()
                                                        ? [ 'rev_user' => $from->getId() ] : [ 'rev_user_text' => $from->getName() ],
                                                __METHOD__
                                        );
                                }
-                               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                        $dbw->update(
                                                'revision_actor_temp',
                                                [ 'revactor_actor' => $to->getActorId( $dbw ) ],
@@ -179,7 +178,7 @@ class ReassignEdits extends Maintenance {
        }
 
        /**
-        * Return user specifications
+        * Return user specifications for an UPDATE
         * i.e. user => id, user_text => text
         *
         * @param IDatabase $dbw Database handle
@@ -193,13 +192,13 @@ class ReassignEdits extends Maintenance {
                global $wgActorTableSchemaMigrationStage;
 
                $ret = [];
-               if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                        $ret += [
                                $idfield => $user->getId(),
-                               $utfield => $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ? $user->getName() : '',
+                               $utfield => $user->getName(),
                        ];
                }
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                        $ret += [ $acfield => $user->getActorId( $dbw ) ];
                }
                return $ret;
index 3fa30cb..6b2f488 100644 (file)
@@ -48,7 +48,7 @@ class RemoveUnusedAccounts extends Maintenance {
                $delUser = [];
                $delActor = [];
                $dbr = $this->getDB( DB_REPLICA );
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                        $res = $dbr->select(
                                [ 'user', 'actor' ],
                                [ 'user_id', 'user_name', 'user_touched', 'actor_id' ],
@@ -94,7 +94,7 @@ class RemoveUnusedAccounts extends Maintenance {
                        $this->output( "\nDeleting unused accounts..." );
                        $dbw = $this->getDB( DB_MASTER );
                        $dbw->delete( 'user', [ 'user_id' => $delUser ], __METHOD__ );
-                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                # Keep actor rows referenced from ipblocks
                                $keep = $dbw->selectFieldValues(
                                        'ipblocks', 'ipb_by_actor', [ 'ipb_by_actor' => $delActor ], __METHOD__
@@ -110,11 +110,11 @@ class RemoveUnusedAccounts extends Maintenance {
                        $dbw->delete( 'user_groups', [ 'ug_user' => $delUser ], __METHOD__ );
                        $dbw->delete( 'user_former_groups', [ 'ufg_user' => $delUser ], __METHOD__ );
                        $dbw->delete( 'user_properties', [ 'up_user' => $delUser ], __METHOD__ );
-                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                $dbw->delete( 'logging', [ 'log_actor' => $delActor ], __METHOD__ );
                                $dbw->delete( 'recentchanges', [ 'rc_actor' => $delActor ], __METHOD__ );
                        }
-                       if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                $dbw->delete( 'logging', [ 'log_user' => $delUser ], __METHOD__ );
                                $dbw->delete( 'recentchanges', [ 'rc_user' => $delUser ], __METHOD__ );
                        }
index 0f1eeac..51de082 100644 (file)
@@ -36,8 +36,8 @@
 
 CLDRPluralRuleParser:
   type: file
-  src: https://raw.githubusercontent.com/santhoshtr/CLDRPluralRuleParser/v1.1.3/src/CLDRPluralRuleParser.js
-  integrity: sha384-Y0qxTEDVQgh+N5In+vLbZLL2H7PEROnicj8vxof0mxR8kXcGysGE6OcF+cS+Ao0u
+  src: https://raw.githubusercontent.com/santhoshtr/CLDRPluralRuleParser/0dda851/src/CLDRPluralRuleParser.js
+  integrity: sha384-M4taeYYG2+9Ob1/La16iO+zlRRmBV5lBR3xUKkQT6kfkJ0aLbCi6yc0RYI1BDzdh
 
 easy-deflate:
   type: multi-file
index 878eb9b..0ea5db5 100644 (file)
@@ -99,18 +99,16 @@ class RollbackEdits extends Maintenance {
                $titles = [];
                $actorQuery = ActorMigration::newMigration()
                        ->getWhere( $dbr, 'rev_user', User::newFromName( $user, false ) );
-               foreach ( $actorQuery['orconds'] as $cond ) {
-                       $results = $dbr->select(
-                               [ 'page', 'revision' ] + $actorQuery['tables'],
-                               [ 'page_namespace', 'page_title' ],
-                               [ $cond ],
-                               __METHOD__,
-                               [],
-                               [ 'revision' => [ 'JOIN', 'page_latest = rev_id' ] ] + $actorQuery['joins']
-                       );
-                       foreach ( $results as $row ) {
-                               $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
-                       }
+               $results = $dbr->select(
+                       [ 'page', 'revision' ] + $actorQuery['tables'],
+                       [ 'page_namespace', 'page_title' ],
+                       $actorQuery['conds'],
+                       __METHOD__,
+                       [],
+                       [ 'revision' => [ 'JOIN', 'page_latest = rev_id' ] ] + $actorQuery['joins']
+               );
+               foreach ( $results as $row ) {
+                       $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
                }
 
                return $titles;
index 0f0073c..b00db20 100644 (file)
@@ -22,7 +22,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\SlotRecord;
 
 require_once __DIR__ . '/../Maintenance.php';
 
index 1491e3d..73268d4 100644 (file)
@@ -6,7 +6,6 @@
  * Released under the MIT license
  * http://opensource.org/licenses/MIT
  *
- * @version 0.1.0
  * @source https://github.com/santhoshtr/CLDRPluralRuleParser
  * @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
  * @author Timo Tijhof
@@ -26,6 +25,7 @@
                // AMD. Register as an anonymous module.
                define(factory);
        } else if (typeof exports === 'object') {
+               /* global module */
                // Node. Does not work with strict CommonJS, but
                // only CommonJS-like environments that support module.exports,
                // like Node.
@@ -322,9 +322,9 @@ function pluralRuleParser(rule, number) {
                        return null;
                }
 
-               debug(' -- passed ' + parseInt(result[0], 10) + ' ' + result[2] + ' ' + parseInt(result[4], 10));
+               debug(' -- passed ', parseInt(result[0], 10), result[2], parseInt(result[4], 10));
 
-               return parseInt(result[0], 10) % parseInt(result[4], 10);
+               return parseFloat(result[0]) % parseInt(result[4], 10);
        }
 
        function not() {
@@ -344,7 +344,7 @@ function pluralRuleParser(rule, number) {
                var result = sequence([expression, whitespace, choice([_is_]), whitespace, value]);
 
                if (result !== null) {
-                       debug(' -- passed is : ' + result[0] + ' == ' + parseInt(result[4], 10));
+                       debug(' -- passed is :', result[0], ' == ', parseInt(result[4], 10));
 
                        return result[0] === parseInt(result[4], 10);
                }
@@ -361,7 +361,7 @@ function pluralRuleParser(rule, number) {
                );
 
                if (result !== null) {
-                       debug(' -- passed isnot: ' + result[0] + ' != ' + parseInt(result[4], 10));
+                       debug(' -- passed isnot: ', result[0], ' != ', parseInt(result[4], 10));
 
                        return result[0] !== parseInt(result[4], 10);
                }
@@ -376,7 +376,7 @@ function pluralRuleParser(rule, number) {
                        result = sequence([expression, whitespace, _isnot_sign_, whitespace, rangeList]);
 
                if (result !== null) {
-                       debug(' -- passed not_in: ' + result[0] + ' != ' + result[4]);
+                       debug(' -- passed not_in: ', result[0], ' != ', result[4]);
                        range_list = result[4];
 
                        for (i = 0; i < range_list.length; i++) {
@@ -459,12 +459,12 @@ function pluralRuleParser(rule, number) {
                );
 
                if (result !== null) {
-                       debug(' -- passed _in:' + result);
+                       debug(' -- passed _in:', result);
 
                        range_list = result[5];
 
                        for (i = 0; i < range_list.length; i++) {
-                               if (parseInt(range_list[i], 10) === parseInt(result[0], 10)) {
+                               if (parseInt(range_list[i], 10) === parseFloat(result[0])) {
                                        return (result[1][0] !== 'not');
                                }
                        }
@@ -541,7 +541,7 @@ function pluralRuleParser(rule, number) {
                var result = sequence([whitespace, _and_, whitespace, relation]);
 
                if (result !== null) {
-                       debug(' -- passed andTail' + result);
+                       debug(' -- passed andTail', result);
 
                        return result[3];
                }
@@ -556,7 +556,7 @@ function pluralRuleParser(rule, number) {
                var result = sequence([whitespace, _or_, whitespace, and]);
 
                if (result !== null) {
-                       debug(' -- passed orTail: ' + result[3]);
+                       debug(' -- passed orTail: ', result[3]);
 
                        return result[3];
                }
@@ -597,7 +597,7 @@ function pluralRuleParser(rule, number) {
        }
 
        if (pos !== rule.length) {
-               debug('Warning: Rule not parsed completely. Parser stopped at ' + rule.substr(0, pos) + ' for rule: ' + rule);
+               debug('Warning: Rule not parsed completely. Parser stopped at ', rule.substr(0, pos), ' for rule: ', rule);
        }
 
        return result;
index daa7a91..242d280 100644 (file)
@@ -8,19 +8,23 @@
        animation-delay: @arguments; // Chrome 43+, Firefox 16+, IE 10+, Edge 12+, Safari 9+, Opera 12.10 & 30+, iOS 9+, Android 47+
 }
 
-// This is a general mixin for a color circle
-.mw-rcfilters-mixin-circle( @color: @color-base--inverted, @diameter: 2em, @padding: 0.5em, @border: false, @borderColor: @colorGray5, @emptyBackground: false ) {
+// Circle mixin
+.mw-rcfilters-circle( @min-size-diameter: @min-size-circle, @size-diameter: @size-circle, @margin: 0.5em ) {
        .box-sizing( border-box );
-       min-width: @diameter;
-       width: @diameter;
-       min-height: @diameter;
-       height: @diameter;
-       margin: @padding;
+       min-width: @min-size-diameter;
+       width: @size-diameter;
+       min-height: @min-size-diameter;
+       height: @size-diameter;
+       margin: @margin;
        border-radius: 50%;
+}
 
+// Circle color mixin
+.mw-rcfilters-circle-color( @color: @color-base--inverted, @border: false, @borderColor: @colorGray5, @emptyBackground: false ) {
        & when ( @emptyBackground = false ) {
                background-color: @color;
        }
+
        & when ( @emptyBackground = true ) {
                background-color: @highlight-none;
        }
@@ -35,8 +39,8 @@
 // a color class on its parent element
 .result-circle( @colorName: 'none' ) {
        &-@{colorName} {
-               .mw-rcfilters-mixin-circle( ~'@{highlight-@{colorName}}', @result-circle-diameter, 0 );
                display: none;
+               .mw-rcfilters-circle-color( ~'@{highlight-@{colorName}}' );
 
                .mw-rcfilters-highlight-color-@{colorName} & {
                        display: inline-block;
@@ -46,7 +50,7 @@
 
 // A mixin for changes list containers. Applies enough margin-left to fit the 5 highlight circles.
 .result-circle-margin() {
-       margin-left: ~'calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 + @{result-circle-general-margin} )';
+       margin-left: ~'calc( ( @{size-circle-result} + @{margin-circle-result} ) * 5 + @{margin-circle} )';
 }
 
 // A mixin just for changesListWrapperWidget page, to output the scope of the widget
index 920fec3..e90ce96 100644 (file)
@@ -9,65 +9,68 @@
                        list-style: none;
                }
        }
+
+       & > div {
+               margin-right: @margin-circle-result;
+       }
 }
 
 // Make more specific for the overrides
 div.mw-rcfilters-ui-highlights {
        body.mw-rcfilters-ui-initialized & {
                display: inline-block;
+               .mw-rcfilters-circle( @size-circle-result, @size-circle-result, 0 );
        }
 
        &-color {
                &-none {
                        display: inline-block;
+
                        .mw-changeslist-watchedseen & {
                                .mw-rcfilters-ui-changesListWrapperWidget.mw-rcfilters-ui-changesListWrapperWidget-highlighted & {
-                                       .mw-rcfilters-mixin-circle( @highlight-none, @result-circle-diameter, 0, true, @highlight-grey, true );
+                                       .mw-rcfilters-circle-color( @highlight-none, true, @highlight-grey, true );
                                }
 
                                .mw-rcfilters-ui-changesListWrapperWidget:not( .mw-rcfilters-ui-changesListWrapperWidget-highlighted ) & {
-                                       .mw-rcfilters-mixin-circle( @highlight-none, @result-circle-diameter, 0, true, @highlight-bluedot, true );
+                                       .mw-rcfilters-circle-color( @highlight-none, true, @highlight-bluedot, true );
                                }
                        }
 
                        .mw-changeslist-watchedunseen & {
                                .mw-rcfilters-ui-changesListWrapperWidget.mw-rcfilters-ui-changesListWrapperWidget-highlighted & {
-                                       .mw-rcfilters-mixin-circle( @highlight-grey, @result-circle-diameter, 0, true, @highlight-grey );
+                                       .mw-rcfilters-circle-color( @highlight-grey, true, @highlight-grey );
                                }
 
                                .mw-rcfilters-ui-changesListWrapperWidget:not( .mw-rcfilters-ui-changesListWrapperWidget-highlighted ) & {
-                                       .mw-rcfilters-mixin-circle( @highlight-bluedot, @result-circle-diameter, 0, true, @highlight-bluedot );
+                                       .mw-rcfilters-circle-color( @highlight-bluedot, true, @highlight-bluedot );
                                }
                        }
 
                }
 
-               // Watchlist unseen highlighted fixes
-               // Seen (empty circle)
-               // There's no need to correct 'unseen' because that would be
-               // a filled colorful circle, which is the regular rendering
-               .mw-changeslist-watchedseen &-c1 {
-                       .mw-rcfilters-mixin-circle( @highlight-c1, @result-circle-diameter, 0, true, @highlight-c1, true );
-               }
+       }
 
-               .mw-changeslist-watchedseen &-c2 {
-                       .mw-rcfilters-mixin-circle( @highlight-c2, @result-circle-diameter, 0, true, @highlight-c2, true );
-               }
+       // Watchlist unseen highlighted fixes
+       // Seen (empty circle)
+       // There's no need to correct 'unseen' because that would be
+       // a filled colorful circle, which is the regular rendering
+       .mw-changeslist-watchedseen &-c1 {
+               .mw-rcfilters-circle-color( @highlight-c1, true, @highlight-c1, true );
+       }
 
-               .mw-changeslist-watchedseen &-c3 {
-                       .mw-rcfilters-mixin-circle( @highlight-c3, @result-circle-diameter, 0, true, @highlight-c3, true );
-               }
+       .mw-changeslist-watchedseen &-c2 {
+               .mw-rcfilters-circle-color( @highlight-c2, true, @highlight-c2, true );
+       }
 
-               .mw-changeslist-watchedseen &-c4 {
-                       .mw-rcfilters-mixin-circle( @highlight-c4, @result-circle-diameter, 0, true, @highlight-c4, true );
-               }
+       .mw-changeslist-watchedseen &-c3 {
+               .mw-rcfilters-circle-color( @highlight-c3, true, @highlight-c3, true );
+       }
 
-               .mw-changeslist-watchedseen &-c5 {
-                       .mw-rcfilters-mixin-circle( @highlight-c5, @result-circle-diameter, 0, true, @highlight-c5, true );
-               }
+       .mw-changeslist-watchedseen &-c4 {
+               .mw-rcfilters-circle-color( @highlight-c4, true, @highlight-c4, true );
        }
 
-       .mw-rcfilters-ui-changesListWrapperWidget & > div {
-               margin-right: @result-circle-margin;
+       .mw-changeslist-watchedseen &-c5 {
+               .mw-rcfilters-circle-color( @highlight-c5, true, @highlight-c5, true );
        }
 }
index e0c3c8b..324c900 100644 (file)
                        width: 100%;
                }
        }
-}
-
-.mw-rcfilters-ui-highlights {
-       display: none;
-       padding: 0 @result-circle-general-margin 0 0;
-       // The width is 5 circles times their diameter + individual margin
-       // and then plus the general margin
-       width: ~'calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 )';
-       // And we want to shift the entire block to the left of the li
-       position: relative;
-       // Negative left margin of width + padding
-       margin-left: ~'calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * -5 - @{result-circle-general-margin} )';
 
-       .mw-rcfilters-ui-changesListWrapperWidget-highlighted & {
-               display: inline-block;
-       }
-
-       > div {
-               .box-sizing( border-box );
-               margin-right: @result-circle-margin;
-               vertical-align: middle;
-               // This is to make the dots appear at the center of the
-               // text itself; it's a horrendous hack and blame JamesF for it.
-               margin-top: -2px;
-               float: right;
-       }
-
-       &-color {
-               &-none {
-                       .mw-rcfilters-mixin-circle( @highlight-none, @result-circle-diameter, 0, true );
+       &-highlights {
+               display: none;
+               padding: 0 @margin-circle 0 0;
+               text-align: right;
+               // The width is 5 circles times their diameter + individual margin
+               // and then plus the general margin
+               width: ~'calc( ( @{size-circle-result} + @{margin-circle-result} ) * 5 )';
+               // And we want to shift the entire block to the left of the li
+               position: relative;
+               // Negative left margin of width + padding
+               margin-left: ~'calc( ( @{size-circle-result} + @{margin-circle-result} ) * -5 - @{margin-circle} )';
+
+               .mw-rcfilters-ui-changesListWrapperWidget-highlighted & {
                        display: inline-block;
+               }
+
+               // This needs to be very specific, since these are
+               // position rules that should apply to all overrides
+               .mw-rcfilters-ui-changesListWrapperWidget .mw-rcfilters-ui-changesListWrapperWidget-highlights > div&-circle {
+                       vertical-align: middle;
+                       .mw-rcfilters-circle( @size-circle-result, @size-circle-result, 0 );
+                       // This is to make the dots appear at the center of the
+                       // text itself; it's a horrendous hack and blame JamesF for it.
+                       margin-top: -2px;
+                       margin-right: @margin-circle-result;
+               }
 
-                       .mw-rcfilters-highlight-color-c1 &,
-                       .mw-rcfilters-highlight-color-c2 &,
-                       .mw-rcfilters-highlight-color-c3 &,
-                       .mw-rcfilters-highlight-color-c4 &,
-                       .mw-rcfilters-highlight-color-c5 & {
-                               display: none;
+               &-color {
+                       &-none {
+                               .mw-rcfilters-circle-color( @highlight-none, true );
+                               display: inline-block;
+
+                               .mw-rcfilters-highlight-color-c1 &,
+                               .mw-rcfilters-highlight-color-c2 &,
+                               .mw-rcfilters-highlight-color-c3 &,
+                               .mw-rcfilters-highlight-color-c4 &,
+                               .mw-rcfilters-highlight-color-c5 & {
+                                       display: none;
+                               }
                        }
+                       .result-circle( c1 );
+                       .result-circle( c2 );
+                       .result-circle( c3 );
+                       .result-circle( c4 );
+                       .result-circle( c5 );
                }
-               .result-circle( c1 );
-               .result-circle( c2 );
-               .result-circle( c3 );
-               .result-circle( c4 );
-               .result-circle( c5 );
        }
 }
 
 .highlight-color-mix( c1, c3, c4, c5 );
 .highlight-color-mix( c2, c3, c4, c5 );
 
-// Five colors:
+// Five colors
 .mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c2.mw-rcfilters-highlight-color-c3.mw-rcfilters-highlight-color-c4.mw-rcfilters-highlight-color-c5 {
        .highlight-results( tint( mix( @highlight-c1, mix( @highlight-c2, mix( @highlight-c3, average( @highlight-c4, @highlight-c5 ), 20% ), 20% ), 20% ), 15% ) );
 }
index dd77eea..93fae1e 100644 (file)
@@ -1,31 +1,30 @@
 @import 'mw.rcfilters.mixins';
+@import 'mw.rcfilters.variables';
 
 .mw-rcfilters-ui-filterItemHighlightButton {
        .oo-ui-buttonWidget.oo-ui-popupButtonWidget .oo-ui-buttonElement-button > &-circle {
                background-image: none;
                display: inline-block;
                vertical-align: middle;
-               margin-right: 0.2em;
                // Override OOUI rule on frameless icons
                opacity: 1;
+               .mw-rcfilters-circle( @min-size-circle, @size-circle, @margin-circle 0 );
 
                &-color {
                        &-c1 {
-                               // These values duplicate the sizing of the icon
-                               // width/height 1.875em
-                               .mw-rcfilters-mixin-circle( @highlight-c1, 1.875em, 0.2em 0 );
+                               .mw-rcfilters-circle-color( @highlight-c1 );
                        }
                        &-c2 {
-                               .mw-rcfilters-mixin-circle( @highlight-c2, 1.875em, 0.2em 0 );
+                               .mw-rcfilters-circle-color( @highlight-c2 );
                        }
                        &-c3 {
-                               .mw-rcfilters-mixin-circle( @highlight-c3, 1.875em, 0.2em 0 );
+                               .mw-rcfilters-circle-color( @highlight-c3 );
                        }
                        &-c4 {
-                               .mw-rcfilters-mixin-circle( @highlight-c4, 1.875em, 0.2em 0 );
+                               .mw-rcfilters-circle-color( @highlight-c4 );
                        }
                        &-c5 {
-                               .mw-rcfilters-mixin-circle( @highlight-c5, 1.875em, 0.2em 0 );
+                               .mw-rcfilters-circle-color( @highlight-c5 );
                        }
                }
        }
index 41dc8cf..deecd67 100644 (file)
@@ -1,48 +1,49 @@
 @import 'mediawiki.ui/variables';
 @import 'mw.rcfilters.mixins';
+@import 'mw.rcfilters.variables';
 
 .mw-rcfilters-ui-highlightColorPickerWidget {
        &-label {
                display: block;
                font-weight: bold;
-               font-size: 1.2em;
+               font-size: 1.1425em;
        }
 
        &-buttonSelect {
                &-color {
-                       // Override OOUI definition from padded popup; the definition
-                       // forces the first-child to be margin-top:0; which overrides
-                       // our definitions below where margin is 0.5em.
-                       // We set up the margin-top as 0.5em for all circles so we get
+                       .mw-rcfilters-circle( @min-size-circle-colorpicker, @size-circle-colorpicker, @margin-circle );
+
+                       // Override OOUI rule from padded popup;
+                       // We set margin-top as ≈0.357em≈5px for all circles so we get
                        // a consistent result
-                       &.oo-ui-widget-enabled.oo-ui-optionWidget.oo-ui-buttonElement.oo-ui-buttonElement-frameless.oo-ui-buttonOptionWidget {
-                               margin-top: 0.5em;
+                       &.oo-ui-buttonElement {
+                               margin-top: @margin-circle;
+
+                               // Override OOUI rule on frameless :first-child buttons
+                               &:first-child {
+                                       margin-left: 0;
+                               }
                        }
 
                        // Make the rule much more specific to override OOUI
-                       .oo-ui-iconElement-icon.oo-ui-icon-check {
-                               // Override OOUI icon dimensions
-                               // The parent is 2em with 0.5em margin
-                               // (see mw-rcfilters-mixin-circle below)
-                               // so here we want 2em - 0.5em = 1.5em
-                               width: 1.5em;
-                               height: 1.5em;
-                               // By eye, this is centered horizontally for the color circle
-                               margin-left: -0.1em;
+                       &.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon.oo-ui-icon-check {
+                               // Align centered horizontally within the color circle
+                               top: -2px;
+                               left: 4 / @font-size-system-ui / @font-size-vector;
                                // Override OOUI rule on frameless icons
                                opacity: 1;
                        }
 
                        &-none {
-                               .oo-ui-iconElement-icon.oo-ui-icon-check {
-                                       // By eye, this is centered horizontally for the white circle
-                                       margin-left: -0.2em;
-                               }
-
-                               .mw-rcfilters-mixin-circle( @highlight-none, 2em, 0.5em, true );
+                               .mw-rcfilters-circle-color( @highlight-none, true );
                                // Override `border-style` to `dashed`
                                border-style: dashed;
 
+                               &.oo-ui-buttonElement-frameless > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon.oo-ui-icon-check {
+                                       // Align centered horizontally in the dashed white circle with 1px border-width
+                                       left: 3 / @font-size-system-ui / @font-size-vector;
+                               }
+
                                &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
                                &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
                                &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
@@ -50,7 +51,8 @@
                                }
                        }
                        &-c1 {
-                               .mw-rcfilters-mixin-circle( @highlight-c1 );
+                               .mw-rcfilters-circle-color( @highlight-c1, false );
+                               border-color: @highlight-c1;
 
                                &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
                                &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
@@ -59,7 +61,8 @@
                                }
                        }
                        &-c2 {
-                               .mw-rcfilters-mixin-circle( @highlight-c2 );
+                               .mw-rcfilters-circle-color( @highlight-c2, true );
+                               border-color: @highlight-c2;
 
                                &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
                                &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
@@ -68,7 +71,8 @@
                                }
                        }
                        &-c3 {
-                               .mw-rcfilters-mixin-circle( @highlight-c3 );
+                               .mw-rcfilters-circle-color( @highlight-c3, true );
+                               border-color: @highlight-c3;
 
                                &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
                                &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
@@ -77,7 +81,8 @@
                                }
                        }
                        &-c4 {
-                               .mw-rcfilters-mixin-circle( @highlight-c4 );
+                               .mw-rcfilters-circle-color( @highlight-c4, true );
+                               border-color: @highlight-c4;
 
                                &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
                                &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
@@ -86,7 +91,8 @@
                                }
                        }
                        &-c5 {
-                               .mw-rcfilters-mixin-circle( @highlight-c5 );
+                               .mw-rcfilters-circle-color( @highlight-c5, true );
+                               border-color: @highlight-c5;
 
                                &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
                                &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
index e9c982a..824485f 100644 (file)
@@ -2,6 +2,9 @@
 @import 'mw.rcfilters.mixins';
 @import 'mw.rcfilters.variables';
 
+@size-circle: 20 / @font-size-system-ui / @font-size-vector;
+@margin-circle: 5 / @font-size-system-ui / @font-size-vector;
+
 .mw-rcfilters-ui-tagItemWidget {
        // Background and color of the capsule widget need a bit
        // more specificity to override OOUI internals
@@ -44,8 +47,8 @@
 
        &-highlight {
                display: none;
-               margin-right: 0.5em;
                width: 10px;
+               margin-right: @margin-circle;
 
                &-highlighted {
                        display: inline-block;
 
                &:before {
                        content: '';
-                       position: absolute;
                        display: block;
+                       position: absolute;
                        top: 50%;
+                       .mw-rcfilters-circle( 10px, 10px, ~'-5px 0.5em 0 0' );
                }
 
                &[ data-color='c1' ]:before {
-                       .mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'-5px 0.5em 0 0' );
+                       .mw-rcfilters-circle-color( @highlight-c1 );
                }
 
                &[ data-color='c2' ]:before {
-                       .mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'-5px 0.5em 0 0' );
+                       .mw-rcfilters-circle-color( @highlight-c2 );
                }
 
                &[ data-color='c3' ]:before {
-                       .mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'-5px 0.5em 0 0' );
+                       .mw-rcfilters-circle-color( @highlight-c3 );
                }
 
                &[ data-color='c4' ]:before {
-                       .mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'-5px 0.5em 0 0' );
+                       .mw-rcfilters-circle-color( @highlight-c4 );
                }
 
                &[ data-color='c5' ]:before {
-                       .mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'-5px 0.5em 0 0' );
+                       .mw-rcfilters-circle-color( @highlight-c5 );
                }
        }
 }
index 987f525..f59dc55 100644 (file)
 // Muted state
 @muted-opacity: 0.5;
 
+// Circles
+@min-size-circle: 20px;
+@size-circle: 20 / @font-size-system-ui / @font-size-vector;
+@margin-circle: 5 / @font-size-system-ui / @font-size-vector;
+
 // Result list circle indicators
 // Defined and used in mw.rcfilters.ui.ChangesListWrapperWidget.less
-@result-circle-margin: 3px;
-@result-circle-general-margin: 0.5em;
+@margin-circle-result: 3px;
 // In these small sizes, 'em' appears
 // squished and inconsistent.
 // Pixels are better for this use case:
-@result-circle-diameter: 6px;
+@size-circle-result: 6px;
+
+// Color picker circles
+@min-size-circle-colorpicker: 30px;
+@size-circle-colorpicker: 30 / @font-size-system-ui / @font-size-vector;
index 8385d7a..41f3192 100644 (file)
@@ -157,15 +157,15 @@ $wgAutoloadClasses += [
        'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php",
        'SpecialPageExecutor' => "$testDir/phpunit/includes/specials/SpecialPageExecutor.php",
 
-       # tests/phpunit/includes/Storage
-       'MediaWiki\Tests\Storage\McrSchemaDetection' => "$testDir/phpunit/includes/Storage/McrSchemaDetection.php",
-       'MediaWiki\Tests\Storage\McrSchemaOverride' => "$testDir/phpunit/includes/Storage/McrSchemaOverride.php",
-       'MediaWiki\Tests\Storage\McrWriteBothSchemaOverride' => "$testDir/phpunit/includes/Storage/McrWriteBothSchemaOverride.php",
-       'MediaWiki\Tests\Storage\McrReadNewSchemaOverride' => "$testDir/phpunit/includes/Storage/McrReadNewSchemaOverride.php",
-       'MediaWiki\Tests\Storage\RevisionSlotsTest' => "$testDir/phpunit/includes/Storage/RevisionSlotsTest.php",
-       'MediaWiki\Tests\Storage\RevisionRecordTests' => "$testDir/phpunit/includes/Storage/RevisionRecordTests.php",
-       'MediaWiki\Tests\Storage\RevisionStoreDbTestBase' => "$testDir/phpunit/includes/Storage/RevisionStoreDbTestBase.php",
-       'MediaWiki\Tests\Storage\PreMcrSchemaOverride' => "$testDir/phpunit/includes/Storage/PreMcrSchemaOverride.php",
+       # tests/phpunit/includes/Revision
+       'MediaWiki\Tests\Revision\McrSchemaDetection' => "$testDir/phpunit/includes/Revision/McrSchemaDetection.php",
+       'MediaWiki\Tests\Revision\McrSchemaOverride' => "$testDir/phpunit/includes/Revision/McrSchemaOverride.php",
+       'MediaWiki\Tests\Revision\McrWriteBothSchemaOverride' => "$testDir/phpunit/includes/Revision/McrWriteBothSchemaOverride.php",
+       'MediaWiki\Tests\Revision\McrReadNewSchemaOverride' => "$testDir/phpunit/includes/Revision/McrReadNewSchemaOverride.php",
+       'MediaWiki\Tests\Revision\RevisionSlotsTest' => "$testDir/phpunit/includes/Revision/RevisionSlotsTest.php",
+       'MediaWiki\Tests\Revision\RevisionRecordTests' => "$testDir/phpunit/includes/Revision/RevisionRecordTests.php",
+       'MediaWiki\Tests\Revision\RevisionStoreDbTestBase' => "$testDir/phpunit/includes/Revision/RevisionStoreDbTestBase.php",
+       'MediaWiki\Tests\Revision\PreMcrSchemaOverride' => "$testDir/phpunit/includes/Revision/PreMcrSchemaOverride.php",
 
        # tests/phpunit/languages
        'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php",
index cc3ef1c..1f3183f 100644 (file)
@@ -1233,7 +1233,7 @@ class ParserTestRunner {
                        $tables[] = 'image_comment_temp';
                }
 
-               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                        // The new tables for actors are in use
                        $tables[] = 'actor';
                        $tables[] = 'revision_actor_temp';
index c938750..b761d29 100644 (file)
@@ -27,6 +27,53 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                return new ActorMigration( $stage );
        }
 
+       /**
+        * @dataProvider provideConstructor
+        * @param int $stage
+        * @param string|null $exceptionMsg
+        */
+       public function testConstructor( $stage, $exceptionMsg ) {
+               try {
+                       $m = new ActorMigration( $stage );
+                       if ( $exceptionMsg !== null ) {
+                               $this->fail( 'Expected exception not thrown' );
+                       }
+                       $this->assertInstanceOf( ActorMigration::class, $m );
+               } catch ( InvalidArgumentException $ex ) {
+                       $this->assertSame( $exceptionMsg, $ex->getMessage() );
+               }
+       }
+
+       public static function provideConstructor() {
+               return [
+                       [ 0, '$stage must include a write mode' ],
+                       [ SCHEMA_COMPAT_READ_OLD, '$stage must include a write mode' ],
+                       [ SCHEMA_COMPAT_READ_NEW, '$stage must include a write mode' ],
+                       [ SCHEMA_COMPAT_READ_BOTH, '$stage must include a write mode' ],
+
+                       [ SCHEMA_COMPAT_WRITE_OLD, '$stage must include a read mode' ],
+                       [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_OLD, null ],
+                       [
+                               SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_NEW,
+                               'Cannot read the new schema without also writing it'
+                       ],
+                       [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, 'Cannot read both schemas' ],
+
+                       [ SCHEMA_COMPAT_WRITE_NEW, '$stage must include a read mode' ],
+                       [
+                               SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_OLD,
+                               'Cannot read the old schema without also writing it'
+                       ],
+                       [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_NEW, null ],
+                       [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH, 'Cannot read both schemas' ],
+
+                       [ SCHEMA_COMPAT_WRITE_BOTH, '$stage must include a read mode' ],
+                       [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, null ],
+                       [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, null ],
+                       [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH, 'Cannot read both schemas' ],
+               ];
+       }
+
        /**
         * @dataProvider provideGetJoin
         * @param int $stage
@@ -42,7 +89,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
        public static function provideGetJoin() {
                return [
                        'Simple table, old' => [
-                               MIGRATION_OLD, 'rc_user', [
+                               SCHEMA_COMPAT_OLD, 'rc_user', [
                                        'tables' => [],
                                        'fields' => [
                                                'rc_user' => 'rc_user',
@@ -52,34 +99,32 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                                        'joins' => [],
                                ],
                        ],
-                       'Simple table, write-both' => [
-                               MIGRATION_WRITE_BOTH, 'rc_user', [
-                                       'tables' => [ 'actor_rc_user' => 'actor' ],
+                       'Simple table, read-old' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'rc_user', [
+                                       'tables' => [],
                                        'fields' => [
-                                               'rc_user' => 'COALESCE( actor_rc_user.actor_user, rc_user )',
-                                               'rc_user_text' => 'COALESCE( actor_rc_user.actor_name, rc_user_text )',
-                                               'rc_actor' => 'rc_actor',
-                                       ],
-                                       'joins' => [
-                                               'actor_rc_user' => [ 'LEFT JOIN', 'actor_rc_user.actor_id = rc_actor' ],
+                                               'rc_user' => 'rc_user',
+                                               'rc_user_text' => 'rc_user_text',
+                                               'rc_actor' => 'NULL',
                                        ],
+                                       'joins' => [],
                                ],
                        ],
-                       'Simple table, write-new' => [
-                               MIGRATION_WRITE_NEW, 'rc_user', [
+                       'Simple table, read-new' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'rc_user', [
                                        'tables' => [ 'actor_rc_user' => 'actor' ],
                                        'fields' => [
-                                               'rc_user' => 'COALESCE( actor_rc_user.actor_user, rc_user )',
-                                               'rc_user_text' => 'COALESCE( actor_rc_user.actor_name, rc_user_text )',
+                                               'rc_user' => 'actor_rc_user.actor_user',
+                                               'rc_user_text' => 'actor_rc_user.actor_name',
                                                'rc_actor' => 'rc_actor',
                                        ],
                                        'joins' => [
-                                               'actor_rc_user' => [ 'LEFT JOIN', 'actor_rc_user.actor_id = rc_actor' ],
+                                               'actor_rc_user' => [ 'JOIN', 'actor_rc_user.actor_id = rc_actor' ],
                                        ],
                                ],
                        ],
                        'Simple table, new' => [
-                               MIGRATION_NEW, 'rc_user', [
+                               SCHEMA_COMPAT_NEW, 'rc_user', [
                                        'tables' => [ 'actor_rc_user' => 'actor' ],
                                        'fields' => [
                                                'rc_user' => 'actor_rc_user.actor_user',
@@ -93,7 +138,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        ],
 
                        'ipblocks, old' => [
-                               MIGRATION_OLD, 'ipb_by', [
+                               SCHEMA_COMPAT_OLD, 'ipb_by', [
                                        'tables' => [],
                                        'fields' => [
                                                'ipb_by' => 'ipb_by',
@@ -103,34 +148,32 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                                        'joins' => [],
                                ],
                        ],
-                       'ipblocks, write-both' => [
-                               MIGRATION_WRITE_BOTH, 'ipb_by', [
-                                       'tables' => [ 'actor_ipb_by' => 'actor' ],
+                       'ipblocks, read-old' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'ipb_by', [
+                                       'tables' => [],
                                        'fields' => [
-                                               'ipb_by' => 'COALESCE( actor_ipb_by.actor_user, ipb_by )',
-                                               'ipb_by_text' => 'COALESCE( actor_ipb_by.actor_name, ipb_by_text )',
-                                               'ipb_by_actor' => 'ipb_by_actor',
-                                       ],
-                                       'joins' => [
-                                               'actor_ipb_by' => [ 'LEFT JOIN', 'actor_ipb_by.actor_id = ipb_by_actor' ],
+                                               'ipb_by' => 'ipb_by',
+                                               'ipb_by_text' => 'ipb_by_text',
+                                               'ipb_by_actor' => 'NULL',
                                        ],
+                                       'joins' => [],
                                ],
                        ],
-                       'ipblocks, write-new' => [
-                               MIGRATION_WRITE_NEW, 'ipb_by', [
+                       'ipblocks, read-new' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'ipb_by', [
                                        'tables' => [ 'actor_ipb_by' => 'actor' ],
                                        'fields' => [
-                                               'ipb_by' => 'COALESCE( actor_ipb_by.actor_user, ipb_by )',
-                                               'ipb_by_text' => 'COALESCE( actor_ipb_by.actor_name, ipb_by_text )',
+                                               'ipb_by' => 'actor_ipb_by.actor_user',
+                                               'ipb_by_text' => 'actor_ipb_by.actor_name',
                                                'ipb_by_actor' => 'ipb_by_actor',
                                        ],
                                        'joins' => [
-                                               'actor_ipb_by' => [ 'LEFT JOIN', 'actor_ipb_by.actor_id = ipb_by_actor' ],
+                                               'actor_ipb_by' => [ 'JOIN', 'actor_ipb_by.actor_id = ipb_by_actor' ],
                                        ],
                                ],
                        ],
                        'ipblocks, new' => [
-                               MIGRATION_NEW, 'ipb_by', [
+                               SCHEMA_COMPAT_NEW, 'ipb_by', [
                                        'tables' => [ 'actor_ipb_by' => 'actor' ],
                                        'fields' => [
                                                'ipb_by' => 'actor_ipb_by.actor_user',
@@ -144,7 +187,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        ],
 
                        'Revision, old' => [
-                               MIGRATION_OLD, 'rev_user', [
+                               SCHEMA_COMPAT_OLD, 'rev_user', [
                                        'tables' => [],
                                        'fields' => [
                                                'rev_user' => 'rev_user',
@@ -154,42 +197,36 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                                        'joins' => [],
                                ],
                        ],
-                       'Revision, write-both' => [
-                               MIGRATION_WRITE_BOTH, 'rev_user', [
-                                       'tables' => [
-                                               'temp_rev_user' => 'revision_actor_temp',
-                                               'actor_rev_user' => 'actor',
-                                       ],
+                       'Revision, read-old' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'rev_user', [
+                                       'tables' => [],
                                        'fields' => [
-                                               'rev_user' => 'COALESCE( actor_rev_user.actor_user, rev_user )',
-                                               'rev_user_text' => 'COALESCE( actor_rev_user.actor_name, rev_user_text )',
-                                               'rev_actor' => 'temp_rev_user.revactor_actor',
-                                       ],
-                                       'joins' => [
-                                               'temp_rev_user' => [ 'LEFT JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
-                                               'actor_rev_user' => [ 'LEFT JOIN', 'actor_rev_user.actor_id = temp_rev_user.revactor_actor' ],
+                                               'rev_user' => 'rev_user',
+                                               'rev_user_text' => 'rev_user_text',
+                                               'rev_actor' => 'NULL',
                                        ],
+                                       'joins' => [],
                                ],
                        ],
-                       'Revision, write-new' => [
-                               MIGRATION_WRITE_NEW, 'rev_user', [
+                       'Revision, read-new' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'rev_user', [
                                        'tables' => [
                                                'temp_rev_user' => 'revision_actor_temp',
                                                'actor_rev_user' => 'actor',
                                        ],
                                        'fields' => [
-                                               'rev_user' => 'COALESCE( actor_rev_user.actor_user, rev_user )',
-                                               'rev_user_text' => 'COALESCE( actor_rev_user.actor_name, rev_user_text )',
+                                               'rev_user' => 'actor_rev_user.actor_user',
+                                               'rev_user_text' => 'actor_rev_user.actor_name',
                                                'rev_actor' => 'temp_rev_user.revactor_actor',
                                        ],
                                        'joins' => [
-                                               'temp_rev_user' => [ 'LEFT JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
-                                               'actor_rev_user' => [ 'LEFT JOIN', 'actor_rev_user.actor_id = temp_rev_user.revactor_actor' ],
+                                               'temp_rev_user' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
+                                               'actor_rev_user' => [ 'JOIN', 'actor_rev_user.actor_id = temp_rev_user.revactor_actor' ],
                                        ],
                                ],
                        ],
                        'Revision, new' => [
-                               MIGRATION_NEW, 'rev_user', [
+                               SCHEMA_COMPAT_NEW, 'rev_user', [
                                        'tables' => [
                                                'temp_rev_user' => 'revision_actor_temp',
                                                'actor_rev_user' => 'actor',
@@ -248,34 +285,28 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
 
                return [
                        'Simple table, old' => [
-                               MIGRATION_OLD, 'rc_user', $genericUser, true, [
+                               SCHEMA_COMPAT_OLD, 'rc_user', $genericUser, true, [
                                        'tables' => [],
                                        'orconds' => [ 'userid' => "rc_user = '1'" ],
                                        'joins' => [],
                                ],
                        ],
-                       'Simple table, write-both' => [
-                               MIGRATION_WRITE_BOTH, 'rc_user', $genericUser, true, [
+                       'Simple table, read-old' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'rc_user', $genericUser, true, [
                                        'tables' => [],
-                                       'orconds' => [
-                                               'actor' => "rc_actor = '11'",
-                                               'userid' => "rc_actor = '0' AND rc_user = '1'"
-                                       ],
+                                       'orconds' => [ 'userid' => "rc_user = '1'" ],
                                        'joins' => [],
                                ],
                        ],
-                       'Simple table, write-new' => [
-                               MIGRATION_WRITE_NEW, 'rc_user', $genericUser, true, [
+                       'Simple table, read-new' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'rc_user', $genericUser, true, [
                                        'tables' => [],
-                                       'orconds' => [
-                                               'actor' => "rc_actor = '11'",
-                                               'userid' => "rc_actor = '0' AND rc_user = '1'"
-                                       ],
+                                       'orconds' => [ 'actor' => "rc_actor = '11'" ],
                                        'joins' => [],
                                ],
                        ],
                        'Simple table, new' => [
-                               MIGRATION_NEW, 'rc_user', $genericUser, true, [
+                               SCHEMA_COMPAT_NEW, 'rc_user', $genericUser, true, [
                                        'tables' => [],
                                        'orconds' => [ 'actor' => "rc_actor = '11'" ],
                                        'joins' => [],
@@ -283,34 +314,28 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        ],
 
                        'ipblocks, old' => [
-                               MIGRATION_OLD, 'ipb_by', $genericUser, true, [
+                               SCHEMA_COMPAT_OLD, 'ipb_by', $genericUser, true, [
                                        'tables' => [],
                                        'orconds' => [ 'userid' => "ipb_by = '1'" ],
                                        'joins' => [],
                                ],
                        ],
-                       'ipblocks, write-both' => [
-                               MIGRATION_WRITE_BOTH, 'ipb_by', $genericUser, true, [
+                       'ipblocks, read-old' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'ipb_by', $genericUser, true, [
                                        'tables' => [],
-                                       'orconds' => [
-                                               'actor' => "ipb_by_actor = '11'",
-                                               'userid' => "ipb_by_actor = '0' AND ipb_by = '1'"
-                                       ],
+                                       'orconds' => [ 'userid' => "ipb_by = '1'" ],
                                        'joins' => [],
                                ],
                        ],
-                       'ipblocks, write-new' => [
-                               MIGRATION_WRITE_NEW, 'ipb_by', $genericUser, true, [
+                       'ipblocks, read-new' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'ipb_by', $genericUser, true, [
                                        'tables' => [],
-                                       'orconds' => [
-                                               'actor' => "ipb_by_actor = '11'",
-                                               'userid' => "ipb_by_actor = '0' AND ipb_by = '1'"
-                                       ],
+                                       'orconds' => [ 'actor' => "ipb_by_actor = '11'" ],
                                        'joins' => [],
                                ],
                        ],
                        'ipblocks, new' => [
-                               MIGRATION_NEW, 'ipb_by', $genericUser, true, [
+                               SCHEMA_COMPAT_NEW, 'ipb_by', $genericUser, true, [
                                        'tables' => [],
                                        'orconds' => [ 'actor' => "ipb_by_actor = '11'" ],
                                        'joins' => [],
@@ -318,44 +343,32 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        ],
 
                        'Revision, old' => [
-                               MIGRATION_OLD, 'rev_user', $genericUser, true, [
+                               SCHEMA_COMPAT_OLD, 'rev_user', $genericUser, true, [
                                        'tables' => [],
                                        'orconds' => [ 'userid' => "rev_user = '1'" ],
                                        'joins' => [],
                                ],
                        ],
-                       'Revision, write-both' => [
-                               MIGRATION_WRITE_BOTH, 'rev_user', $genericUser, true, [
-                                       'tables' => [
-                                               'temp_rev_user' => 'revision_actor_temp',
-                                       ],
-                                       'orconds' => [
-                                               'actor' =>
-                                                       "(temp_rev_user.revactor_actor IS NOT NULL) AND temp_rev_user.revactor_actor = '11'",
-                                               'userid' => "temp_rev_user.revactor_actor IS NULL AND rev_user = '1'"
-                                       ],
-                                       'joins' => [
-                                               'temp_rev_user' => [ 'LEFT JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
-                                       ],
+                       'Revision, read-old' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'rev_user', $genericUser, true, [
+                                       'tables' => [],
+                                       'orconds' => [ 'userid' => "rev_user = '1'" ],
+                                       'joins' => [],
                                ],
                        ],
-                       'Revision, write-new' => [
-                               MIGRATION_WRITE_NEW, 'rev_user', $genericUser, true, [
+                       'Revision, read-new' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'rev_user', $genericUser, true, [
                                        'tables' => [
                                                'temp_rev_user' => 'revision_actor_temp',
                                        ],
-                                       'orconds' => [
-                                               'actor' =>
-                                                       "(temp_rev_user.revactor_actor IS NOT NULL) AND temp_rev_user.revactor_actor = '11'",
-                                               'userid' => "temp_rev_user.revactor_actor IS NULL AND rev_user = '1'"
-                                       ],
+                                       'orconds' => [ 'actor' => "temp_rev_user.revactor_actor = '11'" ],
                                        'joins' => [
-                                               'temp_rev_user' => [ 'LEFT JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
+                                               'temp_rev_user' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
                                        ],
                                ],
                        ],
                        'Revision, new' => [
-                               MIGRATION_NEW, 'rev_user', $genericUser, true, [
+                               SCHEMA_COMPAT_NEW, 'rev_user', $genericUser, true, [
                                        'tables' => [
                                                'temp_rev_user' => 'revision_actor_temp',
                                        ],
@@ -367,7 +380,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        ],
 
                        'Multiple users, old' => [
-                               MIGRATION_OLD, 'rc_user', $complicatedUsers, true, [
+                               SCHEMA_COMPAT_OLD, 'rc_user', $complicatedUsers, true, [
                                        'tables' => [],
                                        'orconds' => [
                                                'userid' => "rc_user IN ('1','2','3') ",
@@ -376,30 +389,25 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                                        'joins' => [],
                                ],
                        ],
-                       'Multiple users, write-both' => [
-                               MIGRATION_WRITE_BOTH, 'rc_user', $complicatedUsers, true, [
+                       'Multiple users, read-old' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'rc_user', $complicatedUsers, true, [
                                        'tables' => [],
                                        'orconds' => [
-                                               'actor' => "rc_actor IN ('11','12','34') ",
-                                               'userid' => "rc_actor = '0' AND rc_user IN ('1','2','3') ",
-                                               'username' => "rc_actor = '0' AND rc_user_text IN ('192.168.12.34','192.168.12.35') "
+                                               'userid' => "rc_user IN ('1','2','3') ",
+                                               'username' => "rc_user_text IN ('192.168.12.34','192.168.12.35') "
                                        ],
                                        'joins' => [],
                                ],
                        ],
-                       'Multiple users, write-new' => [
-                               MIGRATION_WRITE_NEW, 'rc_user', $complicatedUsers, true, [
+                       'Multiple users, read-new' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'rc_user', $complicatedUsers, true, [
                                        'tables' => [],
-                                       'orconds' => [
-                                               'actor' => "rc_actor IN ('11','12','34') ",
-                                               'userid' => "rc_actor = '0' AND rc_user IN ('1','2','3') ",
-                                               'username' => "rc_actor = '0' AND rc_user_text IN ('192.168.12.34','192.168.12.35') "
-                                       ],
+                                       'orconds' => [ 'actor' => "rc_actor IN ('11','12','34') " ],
                                        'joins' => [],
                                ],
                        ],
                        'Multiple users, new' => [
-                               MIGRATION_NEW, 'rc_user', $complicatedUsers, true, [
+                               SCHEMA_COMPAT_NEW, 'rc_user', $complicatedUsers, true, [
                                        'tables' => [],
                                        'orconds' => [ 'actor' => "rc_actor IN ('11','12','34') " ],
                                        'joins' => [],
@@ -407,7 +415,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        ],
 
                        'Multiple users, no use ID, old' => [
-                               MIGRATION_OLD, 'rc_user', $complicatedUsers, false, [
+                               SCHEMA_COMPAT_OLD, 'rc_user', $complicatedUsers, false, [
                                        'tables' => [],
                                        'orconds' => [
                                                'username' => "rc_user_text IN ('User1','User2','User3','192.168.12.34','192.168.12.35') "
@@ -415,30 +423,24 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                                        'joins' => [],
                                ],
                        ],
-                       'Multiple users, write-both' => [
-                               MIGRATION_WRITE_BOTH, 'rc_user', $complicatedUsers, false, [
+                       'Multiple users, read-old' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'rc_user', $complicatedUsers, false, [
                                        'tables' => [],
                                        'orconds' => [
-                                               'actor' => "rc_actor IN ('11','12','34') ",
-                                               'username' => "rc_actor = '0' AND "
-                                               . "rc_user_text IN ('User1','User2','User3','192.168.12.34','192.168.12.35') "
+                                               'username' => "rc_user_text IN ('User1','User2','User3','192.168.12.34','192.168.12.35') "
                                        ],
                                        'joins' => [],
                                ],
                        ],
-                       'Multiple users, write-new' => [
-                               MIGRATION_WRITE_NEW, 'rc_user', $complicatedUsers, false, [
+                       'Multiple users, read-new' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'rc_user', $complicatedUsers, false, [
                                        'tables' => [],
-                                       'orconds' => [
-                                               'actor' => "rc_actor IN ('11','12','34') ",
-                                               'username' => "rc_actor = '0' AND "
-                                               . "rc_user_text IN ('User1','User2','User3','192.168.12.34','192.168.12.35') "
-                                       ],
+                                       'orconds' => [ 'actor' => "rc_actor IN ('11','12','34') " ],
                                        'joins' => [],
                                ],
                        ],
                        'Multiple users, new' => [
-                               MIGRATION_NEW, 'rc_user', $complicatedUsers, false, [
+                               SCHEMA_COMPAT_NEW, 'rc_user', $complicatedUsers, false, [
                                        'tables' => [],
                                        'orconds' => [ 'actor' => "rc_actor IN ('11','12','34') " ],
                                        'joins' => [],
@@ -470,12 +472,34 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                        $user->method( 'getActorId' )->willReturn( $this->db->insertId() );
                }
 
+               $stageNames = [
+                       SCHEMA_COMPAT_OLD => 'old',
+                       SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD => 'write-both-read-old',
+                       SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW => 'write-both-read-new',
+                       SCHEMA_COMPAT_NEW => 'new',
+               ];
+
                $stages = [
-                       MIGRATION_OLD => [ MIGRATION_OLD, MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW ],
-                       MIGRATION_WRITE_BOTH => [ MIGRATION_OLD, MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW,
-                               MIGRATION_NEW ],
-                       MIGRATION_WRITE_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW, MIGRATION_NEW ],
-                       MIGRATION_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW, MIGRATION_NEW ],
+                       SCHEMA_COMPAT_OLD => [
+                               SCHEMA_COMPAT_OLD,
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                       ],
+                       SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD => [
+                               SCHEMA_COMPAT_OLD,
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                               SCHEMA_COMPAT_NEW
+                       ],
+                       SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW => [
+                               SCHEMA_COMPAT_OLD,
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                               SCHEMA_COMPAT_NEW
+                       ],
+                       SCHEMA_COMPAT_NEW => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                               SCHEMA_COMPAT_NEW
+                       ],
                ];
 
                $nameKey = $key . '_text';
@@ -483,7 +507,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
 
                foreach ( $stages as $writeStage => $possibleReadStages ) {
                        if ( $key === 'ipb_by' ) {
-                               $extraFields['ipb_address'] = __CLASS__ . "#$writeStage";
+                               $extraFields['ipb_address'] = __CLASS__ . "#{$stageNames[$writeStage]}";
                        }
 
                        $w = $this->makeMigration( $writeStage );
@@ -495,17 +519,21 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                                $fields = $w->getInsertValues( $this->db, $key, $user );
                        }
 
-                       if ( $writeStage <= MIGRATION_WRITE_BOTH ) {
-                               $this->assertSame( $user->getId(), $fields[$key], "old field, stage=$writeStage" );
-                               $this->assertSame( $user->getName(), $fields[$nameKey], "old field, stage=$writeStage" );
+                       if ( $writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
+                               $this->assertSame( $user->getId(), $fields[$key],
+                                       "old field, stage={$stageNames[$writeStage]}" );
+                               $this->assertSame( $user->getName(), $fields[$nameKey],
+                                       "old field, stage={$stageNames[$writeStage]}" );
                        } else {
-                               $this->assertArrayNotHasKey( $key, $fields, "old field, stage=$writeStage" );
-                               $this->assertArrayNotHasKey( $nameKey, $fields, "old field, stage=$writeStage" );
+                               $this->assertArrayNotHasKey( $key, $fields, "old field, stage={$stageNames[$writeStage]}" );
+                               $this->assertArrayNotHasKey( $nameKey, $fields, "old field, stage={$stageNames[$writeStage]}" );
                        }
-                       if ( $writeStage >= MIGRATION_WRITE_BOTH && !$usesTemp ) {
-                               $this->assertSame( $user->getActorId(), $fields[$actorKey], "new field, stage=$writeStage" );
+                       if ( ( $writeStage & SCHEMA_COMPAT_WRITE_NEW ) && !$usesTemp ) {
+                               $this->assertSame( $user->getActorId(), $fields[$actorKey],
+                                       "new field, stage={$stageNames[$writeStage]}" );
                        } else {
-                               $this->assertArrayNotHasKey( $actorKey, $fields, "new field, stage=$writeStage" );
+                               $this->assertArrayNotHasKey( $actorKey, $fields,
+                                       "new field, stage={$stageNames[$writeStage]}" );
                        }
 
                        $this->db->insert( $table, $extraFields + $fields, __METHOD__ );
@@ -527,12 +555,14 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                                        $queryInfo['joins']
                                );
 
-                               $this->assertSame( $user->getId(), (int)$row->$key, "w=$writeStage, r=$readStage, id" );
-                               $this->assertSame( $user->getName(), $row->$nameKey, "w=$writeStage, r=$readStage, name" );
+                               $this->assertSame( $user->getId(), (int)$row->$key,
+                                       "w={$stageNames[$writeStage]}, r={$stageNames[$readStage]}, id" );
+                               $this->assertSame( $user->getName(), $row->$nameKey,
+                                       "w={$stageNames[$writeStage]}, r={$stageNames[$readStage]}, name" );
                                $this->assertSame(
-                                       $readStage === MIGRATION_OLD || $writeStage === MIGRATION_OLD ? 0 : $user->getActorId(),
+                                       ( $readStage & SCHEMA_COMPAT_READ_OLD ) ? 0 : $user->getActorId(),
                                        (int)$row->$actorKey,
-                                       "w=$writeStage, r=$readStage, actor"
+                                       "w={$stageNames[$writeStage]}, r={$stageNames[$readStage]}, actor"
                                );
                        }
                }
@@ -572,10 +602,10 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
 
        public static function provideStages() {
                return [
-                       'MIGRATION_OLD' => [ MIGRATION_OLD ],
-                       'MIGRATION_WRITE_BOTH' => [ MIGRATION_WRITE_BOTH ],
-                       'MIGRATION_WRITE_NEW' => [ MIGRATION_WRITE_NEW ],
-                       'MIGRATION_NEW' => [ MIGRATION_NEW ],
+                       'old' => [ SCHEMA_COMPAT_OLD ],
+                       'read-old' => [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD ],
+                       'read-new' => [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW ],
+                       'new' => [ SCHEMA_COMPAT_NEW ],
                ];
        }
 
@@ -630,6 +660,12 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
        }
 
        public function testInsertUserIdentity() {
+               $this->setMwGlobals( [
+                       // for User::getActorId()
+                       'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+               ] );
+               $this->overrideMwServices();
+
                $user = $this->getTestUser()->getUser();
                $userIdentity = $this->getMock( UserIdentity::class );
                $userIdentity->method( 'getId' )->willReturn( $user->getId() );
@@ -638,7 +674,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
 
                list( $cFields, $cCallback ) = MediaWikiServices::getInstance()->getCommentStore()
                        ->insertWithTempTable( $this->db, 'rev_comment', '' );
-               $m = $this->makeMigration( MIGRATION_WRITE_BOTH );
+               $m = $this->makeMigration( SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW );
                list( $fields, $callback ) =
                        $m->getInsertValuesWithTempTable( $this->db, 'rev_user', $userIdentity );
                $extraFields = [
@@ -652,22 +688,22 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                $callback( $id, $extraFields );
                $cCallback( $id );
 
-               $qi = Revision::getQueryInfo();
+               $qi = $m->getJoin( 'rev_user' );
                $row = $this->db->selectRow(
-                       $qi['tables'], $qi['fields'], [ 'rev_id' => $id ], __METHOD__, [], $qi['joins']
+                       [ 'revision' ] + $qi['tables'], $qi['fields'], [ 'rev_id' => $id ], __METHOD__, [], $qi['joins']
                );
                $this->assertSame( $user->getId(), (int)$row->rev_user );
                $this->assertSame( $user->getName(), $row->rev_user_text );
                $this->assertSame( $user->getActorId(), (int)$row->rev_actor );
 
-               $m = $this->makeMigration( MIGRATION_WRITE_BOTH );
+               $m = $this->makeMigration( SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW );
                $fields = $m->getInsertValues( $this->db, 'dummy_user', $userIdentity );
                $this->assertSame( $user->getId(), $fields['dummy_user'] );
                $this->assertSame( $user->getName(), $fields['dummy_user_text'] );
                $this->assertSame( $user->getActorId(), $fields['dummy_actor'] );
        }
 
-       public function testConstructor() {
+       public function testNewMigration() {
                $m = ActorMigration::newMigration();
                $this->assertInstanceOf( ActorMigration::class, $m );
                $this->assertSame( $m, ActorMigration::newMigration() );
@@ -687,10 +723,12 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
 
        public static function provideIsAnon() {
                return [
-                       'MIGRATION_OLD' => [ MIGRATION_OLD, 'foo = 0', 'foo != 0' ],
-                       'MIGRATION_WRITE_BOTH' => [ MIGRATION_WRITE_BOTH, 'foo = 0', 'foo != 0' ],
-                       'MIGRATION_WRITE_NEW' => [ MIGRATION_WRITE_NEW, 'foo = 0', 'foo != 0' ],
-                       'MIGRATION_NEW' => [ MIGRATION_NEW, 'foo IS NULL', 'foo IS NOT NULL' ],
+                       'old' => [ SCHEMA_COMPAT_OLD, 'foo = 0', 'foo != 0' ],
+                       'read-old' => [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'foo = 0', 'foo != 0' ],
+                       'read-new' => [
+                               SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'foo IS NULL', 'foo IS NOT NULL'
+                       ],
+                       'new' => [ SCHEMA_COMPAT_NEW, 'foo IS NULL', 'foo IS NOT NULL' ],
                ];
        }
 
index 62094b6..5cc59b3 100644 (file)
@@ -41,10 +41,13 @@ class HtmlTest extends MediaWikiTestCase {
        }
 
        protected function tearDown() {
+               Language::factory( 'en' )->resetNamespaces();
+
                if ( $this->restoreWarnings ) {
                        $this->restoreWarnings = false;
                        Wikimedia\restoreWarnings();
                }
+
                parent::tearDown();
        }
 
index 054636e..74c1736 100644 (file)
@@ -1392,6 +1392,8 @@ class OutputPageTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideAddWikiText
         * @covers OutputPage::addWikiText
+        * @covers OutputPage::addWikiTextAsInterface
+        * @covers OutputPage::addWikiTextAsContent
         * @covers OutputPage::addWikiTextWithTitle
         * @covers OutputPage::addWikiTextTitle
         * @covers OutputPage::addWikiTextTidy
@@ -1402,6 +1404,10 @@ class OutputPageTest extends MediaWikiTestCase {
                $op = $this->newInstance();
                $this->assertSame( '', $op->getHTML() );
 
+               $this->hideDeprecated( 'OutputPage::addWikiTextTitle' );
+               $this->hideDeprecated( 'OutputPage::addWikiTextWithTitle' );
+               $this->hideDeprecated( 'OutputPage::addWikiTextTidy' );
+               $this->hideDeprecated( 'OutputPage::addWikiTextTitleTidy' );
                if ( in_array(
                        $method,
                        [ 'addWikiTextWithTitle', 'addWikiTextTitleTidy', 'addWikiTextTitle' ]
@@ -1409,6 +1415,13 @@ class OutputPageTest extends MediaWikiTestCase {
                        // Special placeholder because we can't get the actual title in the provider
                        $args[1] = $op->getTitle();
                }
+               if ( in_array(
+                       $method,
+                       [ 'addWikiTextAsInterface', 'addWikiTextAsContent' ]
+               ) && count( $args ) >= 3 && $args[2] === null ) {
+                       // Special placeholder because we can't get the actual title in the provider
+                       $args[2] = $op->getTitle();
+               }
 
                $op->$method( ...$args );
                $this->assertSame( $expected, $op->getHTML() );
@@ -1417,6 +1430,7 @@ class OutputPageTest extends MediaWikiTestCase {
        public function provideAddWikiText() {
                $tests = [
                        'addWikiText' => [
+                               // Not tidied; this API is deprecated.
                                'Simple wikitext' => [
                                        [ "'''Bold'''" ],
                                        "<p><b>Bold</b>\n</p>",
@@ -1435,6 +1449,7 @@ class OutputPageTest extends MediaWikiTestCase {
                                ],
                        ],
                        'addWikiTextWithTitle' => [
+                               // Untidied; this API is deprecated
                                'With title at start' => [
                                        [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ) ],
                                        "<ul><li>Some page</li></ul>\n",
@@ -1443,7 +1458,36 @@ class OutputPageTest extends MediaWikiTestCase {
                                        "* Some page",
                                ],
                        ],
-                       'addWikiTextTidy' => [
+                       'addWikiTextAsInterface' => [
+                               // Preferred interface: output is tidied
+                               'Simple wikitext' => [
+                                       [ "'''Bold'''" ],
+                                       "<p><b>Bold</b>\n</p>",
+                               ], 'Untidy wikitext' => [
+                                       [ "<b>Bold" ],
+                                       "<p><b>Bold\n</b></p>",
+                               ], 'List at start' => [
+                                       [ '* List' ],
+                                       "<ul><li>List</li></ul>\n",
+                               ], 'List not at start' => [
+                                       [ '* Not a list', false ],
+                                       '<p>* Not a list</p>',
+                               ], 'No section edit links' => [
+                                       [ '== Title ==' ],
+                                       "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>\n",
+                               ], 'With title at start' => [
+                                       [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
+                                       "<ul><li>Some page</li></ul>\n",
+                               ], 'With title at start' => [
+                                       [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
+                                       "<p>* Some page</p>",
+                               ], 'Untidy input' => [
+                                       [ '<b>{{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
+                                       "<p><b>Some page\n</b></p>",
+                               ],
+                       ],
+                       'addWikiTextAsContent' => [
+                               // Preferred interface: output is tidied
                                'SpecialNewimages' => [
                                        [ "<p lang='en' dir='ltr'>\nMy message" ],
                                        '<p lang="en" dir="ltr">' . "\nMy message\n</p>"
@@ -1453,17 +1497,14 @@ class OutputPageTest extends MediaWikiTestCase {
                                ], 'List not at start' => [
                                        [ '* <b>Not a list', false ],
                                        '<p>* <b>Not a list</b></p>',
-                               ],
-                       ],
-                       'addWikiTextTitleTidy' => [
-                               'With title at start' => [
-                                       [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ) ],
+                               ], 'With title at start' => [
+                                       [ '* {{PAGENAME}}', true, Title::newFromText( 'Talk:Some page' ) ],
                                        "<ul><li>Some page</li></ul>\n",
                                ], 'With title at start' => [
-                                       [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ), false ],
+                                       [ '* {{PAGENAME}}', false, Title::newFromText( 'Talk:Some page' ), false ],
                                        "<p>* Some page</p>",
                                ], 'EditPage' => [
-                                       [ "<div class='mw-editintro'>{{PAGENAME}}", Title::newFromText( 'Talk:Some page' ) ],
+                                       [ "<div class='mw-editintro'>{{PAGENAME}}", true, Title::newFromText( 'Talk:Some page' ) ],
                                        '<div class="mw-editintro">' . "Some page\n</div>"
                                ],
                        ],
@@ -1480,16 +1521,29 @@ class OutputPageTest extends MediaWikiTestCase {
                        $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
                                array_merge( [ $args ], array_slice( $val, 1 ) );
                }
-               foreach ( $tests['addWikiTextTidy'] as $key => $val ) {
-                       $args = [ $val[0][0], null, $val[0][1] ?? true, true, false ];
+               foreach ( $tests['addWikiTextAsInterface'] as $key => $val ) {
+                       $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, true ];
                        $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
                                array_merge( [ $args ], array_slice( $val, 1 ) );
                }
-               foreach ( $tests['addWikiTextTitleTidy'] as $key => $val ) {
-                       $args = [ $val[0][0], $val[0][1], $val[0][2] ?? true, true, false ];
+               foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
+                       $args = [ $val[0][0], $val[0][2] ?? null, $val[0][1] ?? true, true, false ];
                        $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
                                array_merge( [ $args ], array_slice( $val, 1 ) );
                }
+               // addWikiTextTidy / addWikiTextTitleTidy were old aliases of
+               // addWikiTextAsContent
+               foreach ( $tests['addWikiTextAsContent'] as $key => $val ) {
+                       if ( count( $val[0] ) > 2 ) {
+                               $args = [ $val[0][0], $val[0][2], $val[0][1] ?? true ];
+                               $tests['addWikiTextTitleTidy']["$key (addWikiTextTitleTidy)"] =
+                                       array_merge( [ $args ], array_slice( $val, 1 ) );
+                       } else {
+                               $args = [ $val[0][0], $val[0][1] ?? true ];
+                               $tests['addWikiTextTidy']["$key (addWikiTextTidy)"] =
+                                       array_merge( [ $args ], array_slice( $val, 1 ) );
+                       }
+               }
 
                // We have to reformat our array to match what PHPUnit wants
                $ret = [];
@@ -1513,6 +1567,26 @@ class OutputPageTest extends MediaWikiTestCase {
                $op->addWikiText( 'a' );
        }
 
+       /**
+        * @covers OutputPage::addWikiTextAsInterface
+        */
+       public function testAddWikiTextAsInterfaceNoTitle() {
+               $this->setExpectedException( MWException::class, 'Title is null' );
+
+               $op = $this->newInstance( [], null, 'notitle' );
+               $op->addWikiTextAsInterface( 'a' );
+       }
+
+       /**
+        * @covers OutputPage::addWikiTextAsContent
+        */
+       public function testAddWikiTextAsContentNoTitle() {
+               $this->setExpectedException( MWException::class, 'Title is null' );
+
+               $op = $this->newInstance( [], null, 'notitle' );
+               $op->addWikiTextAsContent( 'a' );
+       }
+
        /**
         * @covers OutputPage::addWikiMsg
         */
diff --git a/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/McrReadNewRevisionStoreDbTest.php
new file mode 100644 (file)
index 0000000..3efd372
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use TextContent;
+use Title;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class McrReadNewRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+       use McrReadNewSchemaOverride;
+
+       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+               $numberOfSlots = count( $rev->getSlotRoles() );
+
+               // new schema is written
+               $this->assertSelect(
+                       'slots',
+                       [ 'count(*)' ],
+                       [ 'slot_revision_id' => $rev->getId() ],
+                       [ [ (string)$numberOfSlots ] ]
+               );
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
+
+               $this->assertSelect(
+                       $revQuery['tables'],
+                       [ 'count(*)' ],
+                       [
+                               'slot_revision_id' => $rev->getId(),
+                       ],
+                       [ [ (string)$numberOfSlots ] ],
+                       [],
+                       $revQuery['joins']
+               );
+
+               // Legacy schema is still being written
+               $this->assertSelect(
+                       [ 'revision', 'text' ],
+                       [ 'count(*)' ],
+                       [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
+                       [ [ 1 ] ],
+                       [],
+                       [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
+               );
+
+               parent::assertRevisionExistsInDatabase( $rev );
+       }
+
+       /**
+        * @param SlotRecord $a
+        * @param SlotRecord $b
+        */
+       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+               parent::assertSameSlotContent( $a, $b );
+
+               // Assert that the same content ID has been used
+               $this->assertSame( $a->getContentId(), $b->getContentId() );
+       }
+
+       public function provideInsertRevisionOn_successes() {
+               foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Multi-slot revision insertion' => [
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'aux' => new TextContent( 'Egg' ),
+                               ],
+                               'page' => true,
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+               ];
+       }
+
+       public function provideNewNullRevision() {
+               foreach ( parent::provideNewNullRevision() as $case ) {
+                       yield $case;
+               }
+
+               yield [
+                       Title::newFromText( 'UTPage_notAutoCreated' ),
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'aux' => new WikitextContent( 'Omelet' ),
+                               ],
+                       ],
+                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
+               ];
+       }
+
+       public function testGetQueryInfo_NoSlotDataJoin() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $queryInfo = $store->getQueryInfo();
+
+               // with the new schema enabled, query info should not join the main slot info
+               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
+               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Basic array, multiple roles' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 29,
+                               'parent_id' => 1,
+                               'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
+                               'comment' => 'Goat Comment!',
+                               'content' => [
+                                       'main' => new WikitextContent( 'Söme Cöntent' ),
+                                       'aux' => new TextContent( 'Öther Cöntent' ),
+                               ]
+                       ]
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/McrReadNewSchemaOverride.php b/tests/phpunit/includes/Revision/McrReadNewSchemaOverride.php
new file mode 100644 (file)
index 0000000..fdf2629
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the intermediate MCR database
+ * schema for use during schema migration.
+ */
+trait McrReadNewSchemaOverride {
+
+       use PatchFileLocation;
+       use McrSchemaDetection;
+
+       /**
+        * @return int
+        */
+       protected function getMcrMigrationStage() {
+               return SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW;
+       }
+
+       /**
+        * @return string[]
+        */
+       protected function getMcrTablesToReset() {
+               return [ 'content', 'content_models', 'slots', 'slot_roles' ];
+       }
+
+       /**
+        * @return array[]
+        */
+       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+               $overrides = [
+                       'scripts' => [],
+                       'drop' => [],
+                       'create' => [],
+                       'alter' => [],
+               ];
+
+               if ( !$this->hasMcrTables( $db ) ) {
+                       $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' );
+               }
+
+               if ( !$this->hasPreMcrFields( $db ) ) {
+                       $overrides['alter'][] = 'revision';
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'create-pre-mcr-fields', __DIR__ );
+               }
+
+               return $overrides;
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/McrRevisionStoreDbTest.php
new file mode 100644 (file)
index 0000000..84df200
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use TextContent;
+use Title;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the post-migration MCR DB schema.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+       use McrSchemaOverride;
+
+       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+               $numberOfSlots = count( $rev->getSlotRoles() );
+
+               // new schema is written
+               $this->assertSelect(
+                       'slots',
+                       [ 'count(*)' ],
+                       [ 'slot_revision_id' => $rev->getId() ],
+                       [ [ (string)$numberOfSlots ] ]
+               );
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
+
+               $this->assertSelect(
+                       $revQuery['tables'],
+                       [ 'count(*)' ],
+                       [
+                               'slot_revision_id' => $rev->getId(),
+                       ],
+                       [ [ (string)$numberOfSlots ] ],
+                       [],
+                       $revQuery['joins']
+               );
+
+               parent::assertRevisionExistsInDatabase( $rev );
+       }
+
+       /**
+        * @param SlotRecord $a
+        * @param SlotRecord $b
+        */
+       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+               parent::assertSameSlotContent( $a, $b );
+
+               // Assert that the same content ID has been used
+               $this->assertSame( $a->getContentId(), $b->getContentId() );
+       }
+
+       public function provideInsertRevisionOn_successes() {
+               foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Multi-slot revision insertion' => [
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'aux' => new TextContent( 'Egg' ),
+                               ],
+                               'page' => true,
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+               ];
+       }
+
+       public function provideNewNullRevision() {
+               foreach ( parent::provideNewNullRevision() as $case ) {
+                       yield $case;
+               }
+
+               yield [
+                       Title::newFromText( 'UTPage_notAutoCreated' ),
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'aux' => new WikitextContent( 'Omelet' ),
+                               ],
+                       ],
+                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
+               ];
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Basic array, multiple roles' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 29,
+                               'parent_id' => 1,
+                               'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
+                               'comment' => 'Goat Comment!',
+                               'content' => [
+                                       'main' => new WikitextContent( 'Söme Cöntent' ),
+                                       'aux' => new TextContent( 'Öther Cöntent' ),
+                               ]
+                       ]
+               ];
+       }
+
+       public function testGetQueryInfo_NoSlotDataJoin() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $queryInfo = $store->getQueryInfo();
+
+               // with the new schema enabled, query info should not join the main slot info
+               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
+               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+        * @covers \MediaWiki\Revision\RevisionStore::insertSlotRowOn
+        * @covers \MediaWiki\Revision\RevisionStore::insertContentRowOn
+        */
+       public function testInsertRevisionOn_T202032() {
+               // This test only makes sense for MySQL
+               if ( $this->db->getType() !== 'mysql' ) {
+                       $this->assertTrue( true );
+                       return;
+               }
+
+               // NOTE: must be done before checking MAX(rev_id)
+               $page = $this->getTestPage();
+
+               $maxRevId = $this->db->selectField( 'revision', 'MAX(rev_id)' );
+
+               // Construct a slot row that will conflict with the insertion of the next revision ID,
+               // to emulate the failure mode described in T202032. Nothing will ever read this row,
+               // we just need it to trigger a primary key conflict.
+               $this->db->insert( 'slots', [
+                       'slot_revision_id' => $maxRevId + 1,
+                       'slot_role_id' => 1,
+                       'slot_content_id' => 0,
+                       'slot_origin' => 0
+               ], __METHOD__ );
+
+               $rev = new MutableRevisionRecord( $page->getTitle() );
+               $rev->setTimestamp( '20180101000000' );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( 'test' ) );
+               $rev->setUser( $this->getTestUser()->getUser() );
+               $rev->setContent( 'main', new WikitextContent( 'Text' ) );
+               $rev->setPageId( $page->getId() );
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $return = $store->insertRevisionOn( $rev, $this->db );
+
+               $this->assertSame( $maxRevId + 2, $return->getId() );
+
+               // is the new revision correct?
+               $this->assertRevisionCompleteness( $return );
+               $this->assertRevisionRecordsEqual( $rev, $return );
+
+               // can we find it directly in the database?
+               $this->assertRevisionExistsInDatabase( $return );
+
+               // can we load it from the store?
+               $loaded = $store->getRevisionById( $return->getId() );
+               $this->assertRevisionCompleteness( $loaded );
+               $this->assertRevisionRecordsEqual( $return, $loaded );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/McrSchemaDetection.php b/tests/phpunit/includes/Revision/McrSchemaDetection.php
new file mode 100644 (file)
index 0000000..3831ef2
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * Trait providing methods for detecting which MCR schema migration phase the current schema
+ * is compatible with.
+ */
+trait McrSchemaDetection {
+
+       /**
+        * Returns true if MCR-related tables exist in the database.
+        * If yes, the database is compatible with with MIGRATION_NEW.
+        * If hasPreMcrFields() also returns true, the database supports MIGRATION_WRITE_BOTH mode.
+        *
+        * @param IDatabase $db
+        * @return bool
+        */
+       protected function hasMcrTables( IDatabase $db ) {
+               return $db->tableExists( 'slots', __METHOD__ );
+       }
+
+       /**
+        * Returns true if pre-MCR fields still exist in the database.
+        * If yes, the database is compatible with with MIGRATION_OLD mode.
+        * If hasMcrTables() also returns true, the database supports MIGRATION_WRITE_BOTH mode.
+        *
+        * Note that if the database has been updated in MIGRATION_NEW mode,
+        * the rev_text_id field will be 0 for new revisions. This means that
+        * in MIGRATION_OLD mode, reading such revisions will fail, even though
+        * all the necessary fields exist.
+        * This is not relevant for unit tests, since unit tests reset the database content anyway.
+        *
+        * @param IDatabase $db
+        * @return bool
+        */
+       protected function hasPreMcrFields( IDatabase $db ) {
+               return $db->fieldExists( 'revision', 'rev_content_model', __METHOD__ );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/McrSchemaOverride.php b/tests/phpunit/includes/Revision/McrSchemaOverride.php
new file mode 100644 (file)
index 0000000..dbd271a
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the post-migration
+ * MCR database schema.
+ */
+trait McrSchemaOverride {
+
+       use PatchFileLocation;
+       use McrSchemaDetection;
+
+       /**
+        * @return int
+        */
+       protected function getMcrMigrationStage() {
+               return MIGRATION_NEW;
+       }
+
+       /**
+        * @return string[]
+        */
+       protected function getMcrTablesToReset() {
+               return [
+                       'content',
+                       'content_models',
+                       'slots',
+                       'slot_roles',
+               ];
+       }
+
+       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+               $overrides = [
+                       'scripts' => [],
+                       'drop' => [],
+                       'create' => [],
+                       'alter' => [],
+               ];
+
+               if ( !$this->hasMcrTables( $db ) ) {
+                       $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots.sql' );
+               }
+
+               if ( !$this->hasPreMcrFields( $db ) ) {
+                       $overrides['alter'][] = 'revision';
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'drop-pre-mcr-fields', __DIR__ );
+               }
+
+               return $overrides;
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/McrWriteBothRevisionStoreDbTest.php
new file mode 100644 (file)
index 0000000..0385708
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use Revision;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+       use McrWriteBothSchemaOverride;
+
+       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+               $row = parent::revisionToRow( $rev, $options );
+
+               $row->rev_text_id = (string)$rev->getTextId();
+               $row->rev_content_format = (string)$rev->getContentFormat();
+               $row->rev_content_model = (string)$rev->getContentModel();
+
+               return $row;
+       }
+
+       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+               // New schema is being written
+               $this->assertSelect(
+                       'slots',
+                       [ 'count(*)' ],
+                       [ 'slot_revision_id' => $rev->getId() ],
+                       [ [ '1' ] ]
+               );
+
+               $this->assertSelect(
+                       'content',
+                       [ 'count(*)' ],
+                       [ 'content_address' => $rev->getSlot( 'main' )->getAddress() ],
+                       [ [ '1' ] ]
+               );
+
+               // Legacy schema is still being written
+               $this->assertSelect(
+                       [ 'revision', 'text' ],
+                       [ 'count(*)' ],
+                       [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
+                       [ [ 1 ] ],
+                       [],
+                       [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
+               );
+
+               parent::assertRevisionExistsInDatabase( $rev );
+       }
+
+       /**
+        * @param SlotRecord $a
+        * @param SlotRecord $b
+        */
+       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+               parent::assertSameSlotContent( $a, $b );
+
+               // Assert that the same content ID has been used
+               if ( $a->hasContentId() && $b->hasContentId() ) {
+                       $this->assertSame( $a->getContentId(), $b->getContentId() );
+               }
+       }
+
+       public function provideInsertRevisionOn_failures() {
+               foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
+                       yield $case;
+               }
+
+               yield 'slot that is not main slot' => [
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'lalala' => new WikitextContent( 'Duck' ),
+                               ],
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+                       new InvalidArgumentException( 'Only the main slot is supported' )
+               ];
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Basic array, with page & id' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'text_id' => 2,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                               'content_format' => 'text/x-wiki',
+                               'content_model' => 'wikitext',
+                       ]
+               ];
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromArchiveRow
+        * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+        */
+       public function testInsertRevisionFromArchiveRow_unmigratedArchiveRow() {
+               // The main purpose of this test is to assert that after reading an archive
+               // row using the old schema it can be inserted into the revision table,
+               // and a slot row is created based on slot emulated from the old-style archive row,
+               // when none such slot row exists yet.
+
+               $title = $this->getTestPage()->getTitle();
+
+               $this->db->insert(
+                       'text',
+                       [ 'old_text' => 'Just a test', 'old_flags' => 'utf-8' ],
+                       __METHOD__
+               );
+
+               $textId = $this->db->insertId();
+
+               $row = (object)[
+                       'ar_minor_edit' => '0',
+                       'ar_user' => '0',
+                       'ar_user_text' => '127.0.0.1',
+                       'ar_actor' => null,
+                       'ar_len' => '11',
+                       'ar_deleted' => '0',
+                       'ar_rev_id' => 112277,
+                       'ar_timestamp' => $this->db->timestamp( '20180101000000' ),
+                       'ar_sha1' => 'deadbeef',
+                       'ar_page_id' => $title->getArticleID(),
+                       'ar_comment_text' => 'just a test',
+                       'ar_comment_data' => null,
+                       'ar_comment_cid' => null,
+                       'ar_content_format' => null,
+                       'ar_content_model' => null,
+                       'ts_tags' => null,
+                       'ar_id' => 17,
+                       'ar_namespace' => $title->getNamespace(),
+                       'ar_title' => $title->getDBkey(),
+                       'ar_text_id' => $textId,
+                       'ar_parent_id' => 112211,
+               ];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $rev = $store->newRevisionFromArchiveRow( $row );
+
+               // re-insert archived revision
+               $return = $store->insertRevisionOn( $rev, $this->db );
+
+               // is the new revision correct?
+               $this->assertRevisionCompleteness( $return );
+               $this->assertRevisionRecordsEqual( $rev, $return );
+
+               // can we load it from the store?
+               $loaded = $store->getRevisionById( $return->getId() );
+               $this->assertNotNull( $loaded );
+               $this->assertRevisionCompleteness( $loaded );
+               $this->assertRevisionRecordsEqual( $return, $loaded );
+
+               // can we find it directly in the database?
+               $this->assertRevisionExistsInDatabase( $return );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/McrWriteBothSchemaOverride.php b/tests/phpunit/includes/Revision/McrWriteBothSchemaOverride.php
new file mode 100644 (file)
index 0000000..4ca7fdb
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the intermediate MCR database
+ * schema for use during schema migration.
+ */
+trait McrWriteBothSchemaOverride {
+
+       use PatchFileLocation;
+       use McrSchemaDetection;
+
+       /**
+        * @return int
+        */
+       protected function getMcrMigrationStage() {
+               return SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD;
+       }
+
+       /**
+        * @return string[]
+        */
+       protected function getMcrTablesToReset() {
+               return [ 'content', 'content_models', 'slots', 'slot_roles' ];
+       }
+
+       /**
+        * @return array[]
+        */
+       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+               $overrides = [
+                       'scripts' => [],
+                       'drop' => [],
+                       'create' => [],
+                       'alter' => [],
+               ];
+
+               if ( !$this->hasMcrTables( $db ) ) {
+                       $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' );
+               }
+
+               if ( !$this->hasPreMcrFields( $db ) ) {
+                       $overrides['alter'][] = 'revision';
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'create-pre-mcr-fields', __DIR__ );
+               }
+
+               return $overrides;
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/MutableRevisionRecordTest.php b/tests/phpunit/includes/Revision/MutableRevisionRecordTest.php
new file mode 100644 (file)
index 0000000..060099e
--- /dev/null
@@ -0,0 +1,347 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use InvalidArgumentException;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Storage\RevisionSlotsUpdate;
+use MediaWiki\User\UserIdentityValue;
+use MediaWikiTestCase;
+use TextContent;
+use Title;
+use User;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\MutableRevisionRecord
+ * @covers \MediaWiki\Revision\RevisionRecord
+ */
+class MutableRevisionRecordTest extends MediaWikiTestCase {
+
+       use RevisionRecordTests;
+
+       /**
+        * @param array $rowOverrides
+        *
+        * @return MutableRevisionRecord
+        */
+       protected function newRevision( array $rowOverrides = [] ) {
+               $title = Title::newFromText( 'Dummy' );
+               $title->resetArticleID( 17 );
+
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
+               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+               $record = new MutableRevisionRecord( $title );
+
+               if ( isset( $rowOverrides['rev_deleted'] ) ) {
+                       $record->setVisibility( $rowOverrides['rev_deleted'] );
+               }
+
+               if ( isset( $rowOverrides['rev_id'] ) ) {
+                       $record->setId( $rowOverrides['rev_id'] );
+               }
+
+               if ( isset( $rowOverrides['rev_page'] ) ) {
+                       $record->setPageId( $rowOverrides['rev_page'] );
+               }
+
+               $record->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+               $record->setComment( $comment );
+               $record->setUser( $user );
+               $record->setTimestamp( '20101010000000' );
+
+               return $record;
+       }
+
+       public function provideConstructor() {
+               $title = Title::newFromText( 'Dummy' );
+               $title->resetArticleID( 17 );
+
+               yield [
+                       $title,
+                       'acmewiki'
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructor
+        *
+        * @param Title $title
+        * @param bool $wikiId
+        */
+       public function testConstructorAndGetters(
+               Title $title,
+               $wikiId = false
+       ) {
+               $rec = new MutableRevisionRecord( $title, $wikiId );
+
+               $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
+               $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
+       }
+
+       public function provideConstructorFailure() {
+               $title = Title::newFromText( 'Dummy' );
+               $title->resetArticleID( 17 );
+
+               yield 'not a wiki id' => [
+                       $title,
+                       null
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructorFailure
+        *
+        * @param Title $title
+        * @param bool $wikiId
+        */
+       public function testConstructorFailure(
+               Title $title,
+               $wikiId = false
+       ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new MutableRevisionRecord( $title, $wikiId );
+       }
+
+       public function testSetGetId() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertNull( $record->getId() );
+               $record->setId( 888 );
+               $this->assertSame( 888, $record->getId() );
+       }
+
+       public function testSetGetUser() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $user = $this->getTestSysop()->getUser();
+               $this->assertNull( $record->getUser() );
+               $record->setUser( $user );
+               $this->assertSame( $user, $record->getUser() );
+       }
+
+       public function testSetGetPageId() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertSame( 0, $record->getPageId() );
+               $record->setPageId( 999 );
+               $this->assertSame( 999, $record->getPageId() );
+       }
+
+       public function testSetGetParentId() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertNull( $record->getParentId() );
+               $record->setParentId( 100 );
+               $this->assertSame( 100, $record->getParentId() );
+       }
+
+       public function testGetMainContentWhenEmpty() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->setExpectedException( RevisionAccessException::class );
+               $this->assertNull( $record->getContent( SlotRecord::MAIN ) );
+       }
+
+       public function testSetGetMainContent() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $content = new WikitextContent( 'Badger' );
+               $record->setContent( SlotRecord::MAIN, $content );
+               $this->assertSame( $content, $record->getContent( SlotRecord::MAIN ) );
+       }
+
+       public function testGetSlotWhenEmpty() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertFalse( $record->hasSlot( SlotRecord::MAIN ) );
+
+               $this->setExpectedException( RevisionAccessException::class );
+               $record->getSlot( SlotRecord::MAIN );
+       }
+
+       public function testSetGetSlot() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $slot = SlotRecord::newUnsaved(
+                       SlotRecord::MAIN,
+                       new WikitextContent( 'x' )
+               );
+               $record->setSlot( $slot );
+               $this->assertTrue( $record->hasSlot( SlotRecord::MAIN ) );
+               $this->assertSame( $slot, $record->getSlot( SlotRecord::MAIN ) );
+       }
+
+       public function testSetGetMinor() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertFalse( $record->isMinor() );
+               $record->setMinorEdit( true );
+               $this->assertSame( true, $record->isMinor() );
+       }
+
+       public function testSetGetTimestamp() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertNull( $record->getTimestamp() );
+               $record->setTimestamp( '20180101010101' );
+               $this->assertSame( '20180101010101', $record->getTimestamp() );
+       }
+
+       public function testSetGetVisibility() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertSame( 0, $record->getVisibility() );
+               $record->setVisibility( RevisionRecord::DELETED_USER );
+               $this->assertSame( RevisionRecord::DELETED_USER, $record->getVisibility() );
+       }
+
+       public function testSetGetSha1() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertSame( 'phoiac9h4m842xq45sp7s6u21eteeq1', $record->getSha1() );
+               $record->setSha1( 'someHash' );
+               $this->assertSame( 'someHash', $record->getSha1() );
+       }
+
+       public function testGetSlots() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertInstanceOf( MutableRevisionSlots::class, $record->getSlots() );
+       }
+
+       public function testSetGetSize() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $this->assertSame( 0, $record->getSize() );
+               $record->setSize( 775 );
+               $this->assertSame( 775, $record->getSize() );
+       }
+
+       public function testSetGetComment() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $comment = new CommentStoreComment( 1, 'foo' );
+               $this->assertNull( $record->getComment() );
+               $record->setComment( $comment );
+               $this->assertSame( $comment, $record->getComment() );
+       }
+
+       public function testSimpleGetOriginalAndInheritedSlots() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $mainSlot = new SlotRecord(
+                       (object)[
+                               'slot_id' => 1,
+                               'slot_revision_id' => null, // unsaved
+                               'slot_content_id' => 1,
+                               'content_address' => null, // touched
+                               'model_name' => 'x',
+                               'role_name' => 'main',
+                               'slot_origin' => null // touched
+                       ],
+                       new WikitextContent( 'main' )
+               );
+               $auxSlot = new SlotRecord(
+                       (object)[
+                               'slot_id' => 2,
+                               'slot_revision_id' => null, // unsaved
+                               'slot_content_id' => 1,
+                               'content_address' => 'foo', // inherited
+                               'model_name' => 'x',
+                               'role_name' => 'aux',
+                               'slot_origin' => 1 // inherited
+                       ],
+                       new WikitextContent( 'aux' )
+               );
+
+               $record->setSlot( $mainSlot );
+               $record->setSlot( $auxSlot );
+
+               $this->assertSame( [ 'main' ], $record->getOriginalSlots()->getSlotRoles() );
+               $this->assertSame( $mainSlot, $record->getOriginalSlots()->getSlot( SlotRecord::MAIN ) );
+
+               $this->assertSame( [ 'aux' ], $record->getInheritedSlots()->getSlotRoles() );
+               $this->assertSame( $auxSlot, $record->getInheritedSlots()->getSlot( 'aux' ) );
+       }
+
+       public function testSimpleremoveSlot() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+
+               $a = new WikitextContent( 'a' );
+               $b = new WikitextContent( 'b' );
+
+               $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
+               $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
+
+               $record->removeSlot( 'b' );
+
+               $this->assertTrue( $record->hasSlot( 'a' ) );
+               $this->assertFalse( $record->hasSlot( 'b' ) );
+       }
+
+       public function testApplyUpdate() {
+               $update = new RevisionSlotsUpdate();
+
+               $a = new WikitextContent( 'a' );
+               $b = new WikitextContent( 'b' );
+               $c = new WikitextContent( 'c' );
+               $x = new WikitextContent( 'x' );
+
+               $update->modifyContent( 'b', $x );
+               $update->modifyContent( 'c', $x );
+               $update->removeSlot( 'c' );
+               $update->removeSlot( 'd' );
+
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
+               $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
+               $record->inheritSlot( SlotRecord::newSaved( 7, 5, 'c', SlotRecord::newUnsaved( 'c', $c ) ) );
+
+               $record->applyUpdate( $update );
+
+               $this->assertEquals( [ 'b' ], array_keys( $record->getOriginalSlots()->getSlots() ) );
+               $this->assertEquals( $a, $record->getSlot( 'a' )->getContent() );
+               $this->assertEquals( $x, $record->getSlot( 'b' )->getContent() );
+               $this->assertFalse( $record->hasSlot( 'c' ) );
+       }
+
+       public function provideNotReadyForInsertion() {
+               /** @var Title $title */
+               $title = $this->getMock( Title::class );
+
+               /** @var User $user */
+               $user = $this->getMock( User::class );
+
+               /** @var CommentStoreComment $comment */
+               $comment = $this->getMockBuilder( CommentStoreComment::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $content = new TextContent( 'Test' );
+
+               $rev = new MutableRevisionRecord( $title );
+               yield 'empty' => [ $rev ];
+
+               $rev = new MutableRevisionRecord( $title );
+               $rev->setContent( SlotRecord::MAIN, $content );
+               $rev->setUser( $user );
+               $rev->setComment( $comment );
+               yield 'no timestamp' => [ $rev ];
+
+               $rev = new MutableRevisionRecord( $title );
+               $rev->setUser( $user );
+               $rev->setComment( $comment );
+               $rev->setTimestamp( '20101010000000' );
+               yield 'no content' => [ $rev ];
+
+               $rev = new MutableRevisionRecord( $title );
+               $rev->setContent( SlotRecord::MAIN, $content );
+               $rev->setComment( $comment );
+               $rev->setTimestamp( '20101010000000' );
+               yield 'no user' => [ $rev ];
+
+               $rev = new MutableRevisionRecord( $title );
+               $rev->setUser( $user );
+               $rev->setContent( SlotRecord::MAIN, $content );
+               $rev->setTimestamp( '20101010000000' );
+               yield 'no comment' => [ $rev ];
+       }
+
+       /**
+        * @dataProvider provideNotReadyForInsertion
+        */
+       public function testNotReadyForInsertion( $rev ) {
+               $this->assertFalse( $rev->isReadyForInsertion() );
+       }
+}
diff --git a/tests/phpunit/includes/Revision/MutableRevisionSlotsTest.php b/tests/phpunit/includes/Revision/MutableRevisionSlotsTest.php
new file mode 100644 (file)
index 0000000..7279e64
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use Content;
+use InvalidArgumentException;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\SlotRecord;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\MutableRevisionSlots
+ */
+class MutableRevisionSlotsTest extends RevisionSlotsTest {
+
+       /**
+        * @param SlotRecord[] $slots
+        * @return RevisionSlots
+        */
+       protected function newRevisionSlots( $slots = [] ) {
+               return new MutableRevisionSlots( $slots );
+       }
+
+       public function provideConstructorFailue() {
+               yield 'array or the wrong thing' => [
+                       [ 1, 2, 3 ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructorFailue
+        * @param $slots
+        *
+        * @covers \MediaWiki\Revision\RevisionSlots::__construct
+        * @covers \MediaWiki\Revision\RevisionSlots::setSlotsInternal
+        */
+       public function testConstructorFailue( $slots ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+
+               new MutableRevisionSlots( $slots );
+       }
+
+       public function testSetMultipleSlots() {
+               $slots = new MutableRevisionSlots();
+
+               $this->assertSame( [], $slots->getSlots() );
+
+               $slotA = SlotRecord::newUnsaved( 'some', new WikitextContent( 'A' ) );
+               $slots->setSlot( $slotA );
+               $this->assertTrue( $slots->hasSlot( 'some' ) );
+               $this->assertSame( $slotA, $slots->getSlot( 'some' ) );
+               $this->assertSame( [ 'some' => $slotA ], $slots->getSlots() );
+
+               $slotB = SlotRecord::newUnsaved( 'other', new WikitextContent( 'B' ) );
+               $slots->setSlot( $slotB );
+               $this->assertTrue( $slots->hasSlot( 'other' ) );
+               $this->assertSame( $slotB, $slots->getSlot( 'other' ) );
+               $this->assertSame( [ 'some' => $slotA, 'other' => $slotB ], $slots->getSlots() );
+       }
+
+       public function testSetExistingSlotOverwritesSlot() {
+               $slots = new MutableRevisionSlots();
+
+               $this->assertSame( [], $slots->getSlots() );
+
+               $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $slots->setSlot( $slotA );
+               $this->assertSame( $slotA, $slots->getSlot( SlotRecord::MAIN ) );
+               $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
+
+               $slotB = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'B' ) );
+               $slots->setSlot( $slotB );
+               $this->assertSame( $slotB, $slots->getSlot( SlotRecord::MAIN ) );
+               $this->assertSame( [ 'main' => $slotB ], $slots->getSlots() );
+       }
+
+       /**
+        * @param string $role
+        * @param Content $content
+        * @return SlotRecord
+        */
+       private function newSavedSlot( $role, Content $content ) {
+               return SlotRecord::newSaved( 7, 7, 'xyz', SlotRecord::newUnsaved( $role, $content ) );
+       }
+
+       public function testInheritSlotOverwritesSlot() {
+               $slots = new MutableRevisionSlots();
+               $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $slots->setSlot( $slotA );
+               $slotB = $this->newSavedSlot( SlotRecord::MAIN, new WikitextContent( 'B' ) );
+               $slotC = $this->newSavedSlot( 'foo', new WikitextContent( 'C' ) );
+               $slots->inheritSlot( $slotB );
+               $slots->inheritSlot( $slotC );
+               $this->assertSame( [ 'main', 'foo' ], $slots->getSlotRoles() );
+               $this->assertNotSame( $slotB, $slots->getSlot( SlotRecord::MAIN ) );
+               $this->assertNotSame( $slotC, $slots->getSlot( 'foo' ) );
+               $this->assertTrue( $slots->getSlot( SlotRecord::MAIN )->isInherited() );
+               $this->assertTrue( $slots->getSlot( 'foo' )->isInherited() );
+               $this->assertSame( $slotB->getContent(), $slots->getSlot( SlotRecord::MAIN )->getContent() );
+               $this->assertSame( $slotC->getContent(), $slots->getSlot( 'foo' )->getContent() );
+       }
+
+       public function testSetContentOfExistingSlotOverwritesContent() {
+               $slots = new MutableRevisionSlots();
+
+               $this->assertSame( [], $slots->getSlots() );
+
+               $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $slots->setSlot( $slotA );
+               $this->assertSame( $slotA, $slots->getSlot( SlotRecord::MAIN ) );
+               $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
+
+               $newContent = new WikitextContent( 'B' );
+               $slots->setContent( SlotRecord::MAIN, $newContent );
+               $this->assertSame( $newContent, $slots->getContent( SlotRecord::MAIN ) );
+       }
+
+       public function testRemoveExistingSlot() {
+               $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $slots = new MutableRevisionSlots( [ $slotA ] );
+
+               $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
+
+               $slots->removeSlot( SlotRecord::MAIN );
+               $this->assertSame( [], $slots->getSlots() );
+               $this->setExpectedException( RevisionAccessException::class );
+               $slots->getSlot( SlotRecord::MAIN );
+       }
+
+       public function testNewFromParentRevisionSlots() {
+               /** @var SlotRecord[] $parentSlots */
+               $parentSlots = [
+                       'some' => $this->newSavedSlot( 'some', new WikitextContent( 'X' ) ),
+                       'other' => $this->newSavedSlot( 'other', new WikitextContent( 'Y' ) ),
+               ];
+               $slots = MutableRevisionSlots::newFromParentRevisionSlots( $parentSlots );
+               $this->assertSame( [ 'some', 'other' ], $slots->getSlotRoles() );
+               $this->assertNotSame( $parentSlots['some'], $slots->getSlot( 'some' ) );
+               $this->assertNotSame( $parentSlots['other'], $slots->getSlot( 'other' ) );
+               $this->assertTrue( $slots->getSlot( 'some' )->isInherited() );
+               $this->assertTrue( $slots->getSlot( 'other' )->isInherited() );
+               $this->assertSame( $parentSlots['some']->getContent(), $slots->getContent( 'some' ) );
+               $this->assertSame( $parentSlots['other']->getContent(), $slots->getContent( 'other' ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/NoContentModelRevisionStoreDbTest.php
new file mode 100644 (file)
index 0000000..59481f0
--- /dev/null
@@ -0,0 +1,191 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Revision;
+
+/**
+ * Tests RevisionStore against the pre-MCR, pre-ContentHandler DB schema.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+       use PreMcrSchemaOverride;
+
+       protected function getContentHandlerUseDB() {
+               return false;
+       }
+
+       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+               $row = parent::revisionToRow( $rev, $options );
+
+               $row->rev_text_id = (string)$rev->getTextId();
+
+               return $row;
+       }
+
+       public function provideGetArchiveQueryInfo() {
+               yield [
+                       [
+                               'tables' => [ 'archive' ],
+                               'fields' => array_merge(
+                                       $this->getDefaultArchiveFields(),
+                                       [
+                                               'ar_comment_text' => 'ar_comment',
+                                               'ar_comment_data' => 'NULL',
+                                               'ar_comment_cid' => 'NULL',
+                                               'ar_user_text' => 'ar_user_text',
+                                               'ar_user' => 'ar_user',
+                                               'ar_actor' => 'NULL',
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+       }
+
+       public function provideGetQueryInfo() {
+               yield [
+                       [],
+                       [
+                               'tables' => [ 'revision' ],
+                               'fields' => array_merge(
+                                       $this->getDefaultQueryFields(),
+                                       $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields()
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield [
+                       [ 'page' ],
+                       [
+                               'tables' => [ 'revision', 'page' ],
+                               'fields' => array_merge(
+                                       $this->getDefaultQueryFields(),
+                                       $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
+                                       [
+                                               'page_namespace',
+                                               'page_title',
+                                               'page_id',
+                                               'page_latest',
+                                               'page_is_redirect',
+                                               'page_len',
+                                       ]
+                               ),
+                               'joins' => [
+                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                               ],
+                       ]
+               ];
+               yield [
+                       [ 'user' ],
+                       [
+                               'tables' => [ 'revision', 'user' ],
+                               'fields' => array_merge(
+                                       $this->getDefaultQueryFields(),
+                                       $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
+                                       [
+                                               'user_name',
+                                       ]
+                               ),
+                               'joins' => [
+                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+                               ],
+                       ]
+               ];
+               yield [
+                       [ 'text' ],
+                       [
+                               'tables' => [ 'revision', 'text' ],
+                               'fields' => array_merge(
+                                       $this->getDefaultQueryFields(),
+                                       $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
+                                       [
+                                               'old_text',
+                                               'old_flags',
+                                       ]
+                               ),
+                               'joins' => [
+                                       'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
+                               ],
+                       ]
+               ];
+       }
+
+       public function provideGetSlotsQueryInfo() {
+               $db = wfGetDB( DB_REPLICA );
+
+               yield [
+                       [],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield [
+                       [ 'content' ],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+                                               'content_size' => 'slots.rev_len',
+                                               'content_sha1' => 'slots.rev_sha1',
+                                               'content_address' =>
+                                                       $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
+                                               'model_name' => 'NULL',
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Basic array, with page & id' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'text_id' => 2,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                       ]
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php b/tests/phpunit/includes/Revision/PreMcrRevisionStoreDbTest.php
new file mode 100644 (file)
index 0000000..4345335
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use MediaWiki\Revision\RevisionRecord;
+use Revision;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the pre-MCR DB schema.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+       use PreMcrSchemaOverride;
+
+       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+               $row = parent::revisionToRow( $rev, $options );
+
+               $row->rev_text_id = (string)$rev->getTextId();
+               $row->rev_content_format = (string)$rev->getContentFormat();
+               $row->rev_content_model = (string)$rev->getContentModel();
+
+               return $row;
+       }
+
+       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+               // Legacy schema is still being written
+               $this->assertSelect(
+                       [ 'revision', 'text' ],
+                       [ 'count(*)' ],
+                       [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
+                       [ [ 1 ] ],
+                       [],
+                       [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
+               );
+
+               parent::assertRevisionExistsInDatabase( $rev );
+       }
+
+       public function provideInsertRevisionOn_failures() {
+               foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
+                       yield $case;
+               }
+
+               yield 'slot that is not main slot' => [
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'lalala' => new WikitextContent( 'Duck' ),
+                               ],
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+                       new InvalidArgumentException( 'Only the main slot is supported' )
+               ];
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Basic array, with page & id' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'text_id' => 2,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                               'content_format' => 'text/x-wiki',
+                               'content_model' => 'wikitext',
+                       ]
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/PreMcrSchemaOverride.php b/tests/phpunit/includes/Revision/PreMcrSchemaOverride.php
new file mode 100644 (file)
index 0000000..fb7ef77
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the pre-MCR database schema.
+ */
+trait PreMcrSchemaOverride {
+
+       use PatchFileLocation;
+       use McrSchemaDetection;
+
+       /**
+        * @return int
+        */
+       protected function getMcrMigrationStage() {
+               return MIGRATION_OLD;
+       }
+
+       /**
+        * @return string[]
+        */
+       protected function getMcrTablesToReset() {
+               return [];
+       }
+
+       /**
+        * @return array[]
+        */
+       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+               $overrides = [
+                       'scripts' => [],
+                       'drop' => [],
+                       'create' => [],
+                       'alter' => [],
+               ];
+
+               if ( $this->hasMcrTables( $db ) ) {
+                       $overrides['drop'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/drop-mcr-tables', __DIR__ );
+               }
+
+               if ( !$this->hasPreMcrFields( $db ) ) {
+                       $overrides['alter'][] = 'revision';
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/create-pre-mcr-fields', __DIR__ );
+               }
+
+               return $overrides;
+       }
+
+}
index 2ee1ab4..08a8fa6 100644 (file)
@@ -4,17 +4,17 @@ namespace MediaWiki\Tests\Revision;
 
 use Content;
 use Language;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\MutableRevisionSlots;
 use MediaWiki\Revision\RenderedRevision;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionArchiveRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SuppressedDataException;
-use MediaWiki\User\UserIdentityValue;
+use MediaWiki\Revision\RevisionArchiveRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
 use MediaWikiTestCase;
+use MediaWiki\User\UserIdentityValue;
 use ParserOptions;
 use ParserOutput;
 use PHPUnit\Framework\MockObject\MockObject;
diff --git a/tests/phpunit/includes/Revision/RevisionArchiveRecordTest.php b/tests/phpunit/includes/Revision/RevisionArchiveRecordTest.php
new file mode 100644 (file)
index 0000000..6262642
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use InvalidArgumentException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionArchiveRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\User\UserIdentity;
+use MediaWiki\User\UserIdentityValue;
+use MediaWikiTestCase;
+use TextContent;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\RevisionArchiveRecord
+ * @covers \MediaWiki\Revision\RevisionRecord
+ */
+class RevisionArchiveRecordTest extends MediaWikiTestCase {
+
+       use RevisionRecordTests;
+
+       /**
+        * @param array $rowOverrides
+        *
+        * @return RevisionArchiveRecord
+        */
+       protected function newRevision( array $rowOverrides = [] ) {
+               $title = Title::newFromText( 'Dummy' );
+               $title->resetArticleID( 17 );
+
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
+               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+               $slots = new RevisionSlots( [ $main, $aux ] );
+
+               $row = [
+                       'ar_id' => '5',
+                       'ar_rev_id' => '7',
+                       'ar_page_id' => strval( $title->getArticleID() ),
+                       'ar_timestamp' => '20200101000000',
+                       'ar_deleted' => 0,
+                       'ar_minor_edit' => 0,
+                       'ar_parent_id' => '5',
+                       'ar_len' => $slots->computeSize(),
+                       'ar_sha1' => $slots->computeSha1(),
+               ];
+
+               foreach ( $rowOverrides as $field => $value ) {
+                       $field = preg_replace( '/^rev_/', 'ar_', $field );
+                       $row[$field] = $value;
+               }
+
+               return new RevisionArchiveRecord( $title, $user, $comment, (object)$row, $slots );
+       }
+
+       public function provideConstructor() {
+               $title = Title::newFromText( 'Dummy' );
+               $title->resetArticleID( 17 );
+
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
+               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+               $slots = new RevisionSlots( [ $main, $aux ] );
+
+               $protoRow = [
+                       'ar_id' => '5',
+                       'ar_rev_id' => '7',
+                       'ar_page_id' => strval( $title->getArticleID() ),
+                       'ar_timestamp' => '20200101000000',
+                       'ar_deleted' => 0,
+                       'ar_minor_edit' => 0,
+                       'ar_parent_id' => '5',
+                       'ar_len' => $slots->computeSize(),
+                       'ar_sha1' => $slots->computeSha1(),
+               ];
+
+               $row = $protoRow;
+               yield 'all info' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots,
+                       'acmewiki'
+               ];
+
+               $row = $protoRow;
+               $row['ar_minor_edit'] = '1';
+               $row['ar_deleted'] = strval( RevisionRecord::DELETED_USER );
+
+               yield 'minor deleted' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+               unset( $row['ar_parent'] );
+
+               yield 'no parent' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+               $row['ar_len'] = null;
+               $row['ar_sha1'] = '';
+
+               yield 'ar_len is null, ar_sha1 is ""' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+               yield 'no length, no hash' => [
+                       Title::newFromText( 'DummyDoesNotExist' ),
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructor
+        *
+        * @param Title $title
+        * @param UserIdentity $user
+        * @param CommentStoreComment $comment
+        * @param object $row
+        * @param RevisionSlots $slots
+        * @param bool $wikiId
+        */
+       public function testConstructorAndGetters(
+               Title $title,
+               UserIdentity $user,
+               CommentStoreComment $comment,
+               $row,
+               RevisionSlots $slots,
+               $wikiId = false
+       ) {
+               $rec = new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId );
+
+               $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
+               $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
+               $this->assertSame( $comment, $rec->getComment(), 'getComment' );
+
+               $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
+               $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
+
+               $this->assertSame( (int)$row->ar_id, $rec->getArchiveId(), 'getArchiveId' );
+               $this->assertSame( (int)$row->ar_rev_id, $rec->getId(), 'getId' );
+               $this->assertSame( (int)$row->ar_page_id, $rec->getPageId(), 'getId' );
+               $this->assertSame( $row->ar_timestamp, $rec->getTimestamp(), 'getTimestamp' );
+               $this->assertSame( (int)$row->ar_deleted, $rec->getVisibility(), 'getVisibility' );
+               $this->assertSame( (bool)$row->ar_minor_edit, $rec->isMinor(), 'getIsMinor' );
+
+               if ( isset( $row->ar_parent_id ) ) {
+                       $this->assertSame( (int)$row->ar_parent_id, $rec->getParentId(), 'getParentId' );
+               } else {
+                       $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
+               }
+
+               if ( isset( $row->ar_len ) ) {
+                       $this->assertSame( (int)$row->ar_len, $rec->getSize(), 'getSize' );
+               } else {
+                       $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
+               }
+
+               if ( !empty( $row->ar_sha1 ) ) {
+                       $this->assertSame( $row->ar_sha1, $rec->getSha1(), 'getSha1' );
+               } else {
+                       $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
+               }
+       }
+
+       public function provideConstructorFailure() {
+               $title = Title::newFromText( 'Dummy' );
+               $title->resetArticleID( 17 );
+
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
+
+               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+               $slots = new RevisionSlots( [ $main, $aux ] );
+
+               $protoRow = [
+                       'ar_id' => '5',
+                       'ar_rev_id' => '7',
+                       'ar_page_id' => strval( $title->getArticleID() ),
+                       'ar_timestamp' => '20200101000000',
+                       'ar_deleted' => 0,
+                       'ar_minor_edit' => 0,
+                       'ar_parent_id' => '5',
+                       'ar_len' => $slots->computeSize(),
+                       'ar_sha1' => $slots->computeSha1(),
+               ];
+
+               yield 'not a row' => [
+                       $title,
+                       $user,
+                       $comment,
+                       'not a row',
+                       $slots,
+                       'acmewiki'
+               ];
+
+               $row = $protoRow;
+               $row['ar_timestamp'] = 'kittens';
+
+               yield 'bad timestamp' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+
+               yield 'bad wiki' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots,
+                       12345
+               ];
+
+               // NOTE: $title->getArticleID does *not* have to match ar_page_id in all cases!
+       }
+
+       /**
+        * @dataProvider provideConstructorFailure
+        *
+        * @param Title $title
+        * @param UserIdentity $user
+        * @param CommentStoreComment $comment
+        * @param object $row
+        * @param RevisionSlots $slots
+        * @param bool $wikiId
+        */
+       public function testConstructorFailure(
+               Title $title,
+               UserIdentity $user,
+               CommentStoreComment $comment,
+               $row,
+               RevisionSlots $slots,
+               $wikiId = false
+       ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php b/tests/phpunit/includes/Revision/RevisionQueryInfoTest.php
new file mode 100644 (file)
index 0000000..7188cf5
--- /dev/null
@@ -0,0 +1,1162 @@
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\SlotRecord;
+use MediaWikiTestCase;
+use Revision;
+
+/**
+ * Tests RevisionStore against the post-migration MCR DB schema.
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ */
+class RevisionQueryInfoTest extends MediaWikiTestCase {
+
+       protected function getRevisionQueryFields( $returnTextIdField = true ) {
+               $fields = [
+                       'rev_id',
+                       'rev_page',
+                       'rev_timestamp',
+                       'rev_minor_edit',
+                       'rev_deleted',
+                       'rev_len',
+                       'rev_parent_id',
+                       'rev_sha1',
+               ];
+               if ( $returnTextIdField ) {
+                       $fields[] = 'rev_text_id';
+               }
+               return $fields;
+       }
+
+       protected function getArchiveQueryFields( $returnTextFields = true ) {
+               $fields = [
+                       'ar_id',
+                       'ar_page_id',
+                       'ar_namespace',
+                       'ar_title',
+                       'ar_rev_id',
+                       'ar_timestamp',
+                       'ar_minor_edit',
+                       'ar_deleted',
+                       'ar_len',
+                       'ar_parent_id',
+                       'ar_sha1',
+               ];
+               if ( $returnTextFields ) {
+                       $fields[] = 'ar_text_id';
+               }
+               return $fields;
+       }
+
+       protected function getOldCommentQueryFields( $prefix ) {
+               return [
+                       "{$prefix}_comment_text" => "{$prefix}_comment",
+                       "{$prefix}_comment_data" => 'NULL',
+                       "{$prefix}_comment_cid" => 'NULL',
+               ];
+       }
+
+       protected function getCompatCommentQueryFields( $prefix ) {
+               return [
+                       "{$prefix}_comment_text"
+                               => "COALESCE( comment_{$prefix}_comment.comment_text, {$prefix}_comment )",
+                       "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
+                       "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id",
+               ];
+       }
+
+       protected function getNewCommentQueryFields( $prefix ) {
+               return [
+                       "{$prefix}_comment_text" => "comment_{$prefix}_comment.comment_text",
+                       "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
+                       "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id",
+               ];
+       }
+
+       protected function getOldActorQueryFields( $prefix ) {
+               return [
+                       "{$prefix}_user" => "{$prefix}_user",
+                       "{$prefix}_user_text" => "{$prefix}_user_text",
+                       "{$prefix}_actor" => 'NULL',
+               ];
+       }
+
+       protected function getNewActorQueryFields( $prefix, $tmp = false ) {
+               return [
+                       "{$prefix}_user" => "actor_{$prefix}_user.actor_user",
+                       "{$prefix}_user_text" => "actor_{$prefix}_user.actor_name",
+                       "{$prefix}_actor" => $tmp ?: "{$prefix}_actor",
+               ];
+       }
+
+       protected function getNewActorJoins( $prefix ) {
+               return [
+                       "temp_{$prefix}_user" => [
+                               "JOIN",
+                               "temp_{$prefix}_user.revactor_{$prefix} = {$prefix}_id",
+                       ],
+                       "actor_{$prefix}_user" => [
+                               "JOIN",
+                               "actor_{$prefix}_user.actor_id = temp_{$prefix}_user.revactor_actor",
+                       ],
+               ];
+       }
+
+       protected function getCompatCommentJoins( $prefix ) {
+               return [
+                       "temp_{$prefix}_comment" => [
+                               "LEFT JOIN",
+                               "temp_{$prefix}_comment.revcomment_{$prefix} = {$prefix}_id",
+                       ],
+                       "comment_{$prefix}_comment" => [
+                               "LEFT JOIN",
+                               "comment_{$prefix}_comment.comment_id = temp_{$prefix}_comment.revcomment_comment_id",
+                       ],
+               ];
+       }
+
+       protected function getTextQueryFields() {
+               return [
+                       'old_text',
+                       'old_flags',
+               ];
+       }
+
+       protected function getPageQueryFields() {
+               return [
+                       'page_namespace',
+                       'page_title',
+                       'page_id',
+                       'page_latest',
+                       'page_is_redirect',
+                       'page_len',
+               ];
+       }
+
+       protected function getUserQueryFields() {
+               return [
+                       'user_name',
+               ];
+       }
+
+       protected function getContentHandlerQueryFields( $prefix ) {
+               return [
+                       "{$prefix}_content_format",
+                       "{$prefix}_content_model",
+               ];
+       }
+
+       public function provideArchiveQueryInfo() {
+               yield 'MCR, comment, actor' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+                       ],
+                       [
+                               'tables' => [
+                                       'archive',
+                                       'actor_ar_user' => 'actor',
+                                       'comment_ar_comment' => 'comment',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getArchiveQueryFields( false ),
+                                       $this->getNewActorQueryFields( 'ar' ),
+                                       $this->getNewCommentQueryFields( 'ar' )
+                               ),
+                               'joins' => [
+                                       'comment_ar_comment'
+                                               => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
+                                       'actor_ar_user' => [ 'JOIN', 'actor_ar_user.actor_id = ar_actor' ],
+                               ],
+                       ]
+               ];
+               yield 'read-new MCR, comment, actor' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+                       ],
+                       [
+                               'tables' => [
+                                       'archive',
+                                       'actor_ar_user' => 'actor',
+                                       'comment_ar_comment' => 'comment',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getArchiveQueryFields( false ),
+                                       $this->getNewActorQueryFields( 'ar' ),
+                                       $this->getCompatCommentQueryFields( 'ar' )
+                               ),
+                               'joins' => [
+                                       'comment_ar_comment'
+                                               => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
+                                       'actor_ar_user' => [ 'JOIN', 'actor_ar_user.actor_id = ar_actor' ],
+                               ],
+                       ]
+               ];
+               yield 'MCR write-both/read-old' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                       ],
+                       [
+                               'tables' => [
+                                       'archive',
+                                       'comment_ar_comment' => 'comment',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getArchiveQueryFields( true ),
+                                       $this->getContentHandlerQueryFields( 'ar' ),
+                                       $this->getOldActorQueryFields( 'ar' ),
+                                       $this->getCompatCommentQueryFields( 'ar' )
+                               ),
+                               'joins' => [
+                                       'comment_ar_comment'
+                                               => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
+                               ],
+                       ]
+               ];
+               yield 'pre-MCR, no model' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       [
+                               'tables' => [
+                                       'archive',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getArchiveQueryFields( true ),
+                                       $this->getOldActorQueryFields( 'ar' ),
+                                       $this->getOldCommentQueryFields( 'ar' )
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+       }
+
+       public function provideQueryInfo() {
+               // TODO: more option variations
+               yield 'MCR, page, user, comment, actor' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+                       ],
+                       [ 'page', 'user' ],
+                       [
+                               'tables' => [
+                                       'revision',
+                                       'page',
+                                       'user',
+                                       'temp_rev_user' => 'revision_actor_temp',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'actor_rev_user' => 'actor',
+                                       'comment_rev_comment' => 'comment',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( false ),
+                                       $this->getPageQueryFields(),
+                                       $this->getUserQueryFields(),
+                                       $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+                                       $this->getNewCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => [
+                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                                       'user' => [
+                                               'LEFT JOIN',
+                                               [ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ],
+                                       ],
+                                       'comment_rev_comment' => [
+                                               'JOIN',
+                                               'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id',
+                                       ],
+                                       'actor_rev_user' => [
+                                               'JOIN',
+                                               'actor_rev_user.actor_id = temp_rev_user.revactor_actor',
+                                       ],
+                                       'temp_rev_user' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
+                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                               ],
+                       ]
+               ];
+               yield 'MCR read-new, page, user, comment, actor' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                       ],
+                       [ 'page', 'user' ],
+                       [
+                               'tables' => [
+                                       'revision',
+                                       'page',
+                                       'user',
+                                       'temp_rev_user' => 'revision_actor_temp',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'actor_rev_user' => 'actor',
+                                       'comment_rev_comment' => 'comment',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( false ),
+                                       $this->getPageQueryFields(),
+                                       $this->getUserQueryFields(),
+                                       $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+                                       $this->getCompatCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => array_merge(
+                                       [
+                                               'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                                               'user' => [
+                                                       'LEFT JOIN',
+                                                       [
+                                                               'actor_rev_user.actor_user != 0',
+                                                               'user_id = actor_rev_user.actor_user',
+                                                       ]
+                                               ],
+                                       ],
+                                       $this->getNewActorJoins( 'rev' ),
+                                       $this->getCompatCommentJoins( 'rev' )
+                               ),
+                       ]
+               ];
+               yield 'MCR read-new' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                       ],
+                       [ 'page', 'user' ],
+                       [
+                               'tables' => [
+                                       'revision',
+                                       'page',
+                                       'user',
+                                       'temp_rev_user' => 'revision_actor_temp',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'actor_rev_user' => 'actor',
+                                       'comment_rev_comment' => 'comment',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( false ),
+                                       $this->getPageQueryFields(),
+                                       $this->getUserQueryFields(),
+                                       $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+                                       $this->getCompatCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => array_merge(
+                                       [
+                                               'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                                               'user' => [
+                                                       'LEFT JOIN',
+                                                       [
+                                                               'actor_rev_user.actor_user != 0',
+                                                               'user_id = actor_rev_user.actor_user'
+                                                       ]
+                                               ],
+                                       ],
+                                       $this->getNewActorJoins( 'rev' ),
+                                       $this->getCompatCommentJoins( 'rev' )
+                               ),
+                       ]
+               ];
+               yield 'MCR write-both/read-old' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                       ],
+                       [],
+                       [
+                               'tables' => [
+                                       'revision',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( true ),
+                                       $this->getContentHandlerQueryFields( 'rev' ),
+                                       $this->getOldActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+                                       $this->getCompatCommentQueryFields( 'rev' )
+                               ),
+                       'joins' => array_merge(
+                               $this->getCompatCommentJoins( 'rev' )
+                       ),
+                       ]
+               ];
+               yield 'MCR write-both/read-old, page, user' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                       ],
+                       [ 'page', 'user' ],
+                       [
+                               'tables' => [
+                                       'revision',
+                                       'page',
+                                       'user',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( true ),
+                                       $this->getContentHandlerQueryFields( 'rev' ),
+                                       $this->getUserQueryFields(),
+                                       $this->getPageQueryFields(),
+                                       $this->getOldActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+                                       $this->getCompatCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => array_merge(
+                                       [
+                                               'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                                               'user' => [
+                                                       'LEFT JOIN',
+                                                       [
+                                                               'rev_user != 0',
+                                                               'user_id = rev_user'
+                                                       ]
+                                               ],
+                                       ],
+                                       $this->getCompatCommentJoins( 'rev' )
+                               ),
+                       ]
+               ];
+               yield 'pre-MCR' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       [],
+                       [
+                               'tables' => [ 'revision' ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( true ),
+                                       $this->getContentHandlerQueryFields( 'rev' ),
+                                       $this->getOldActorQueryFields( 'rev' ),
+                                       $this->getOldCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield 'pre-MCR, page, user' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       [ 'page', 'user' ],
+                       [
+                               'tables' => [ 'revision', 'page', 'user' ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( true ),
+                                       $this->getContentHandlerQueryFields( 'rev' ),
+                                       $this->getPageQueryFields(),
+                                       $this->getUserQueryFields(),
+                                       $this->getOldActorQueryFields( 'rev' ),
+                                       $this->getOldCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => [
+                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+                               ],
+                       ]
+               ];
+               yield 'pre-MCR, no model' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       [],
+                       [
+                               'tables' => [ 'revision' ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( true ),
+                                       $this->getOldActorQueryFields( 'rev' ),
+                                       $this->getOldCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => [],
+                       ],
+               ];
+               yield 'pre-MCR, no model, page' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       [ 'page' ],
+                       [
+                               'tables' => [ 'revision', 'page' ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( true ),
+                                       $this->getPageQueryFields(),
+                                       $this->getOldActorQueryFields( 'rev' ),
+                                       $this->getOldCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => [
+                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ], ],
+                               ],
+                       ],
+               ];
+               yield 'pre-MCR, no model, user' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       [ 'user' ],
+                       [
+                               'tables' => [ 'revision', 'user' ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( true ),
+                                       $this->getUserQueryFields(),
+                                       $this->getOldActorQueryFields( 'rev' ),
+                                       $this->getOldCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => [
+                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+                               ],
+                       ],
+               ];
+               yield 'pre-MCR, no model, text' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       [ 'text' ],
+                       [
+                               'tables' => [ 'revision', 'text' ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( true ),
+                                       $this->getTextQueryFields(),
+                                       $this->getOldActorQueryFields( 'rev' ),
+                                       $this->getOldCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => [
+                                       'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
+                               ],
+                       ],
+               ];
+               yield 'pre-MCR, no model, text, page, user' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       [ 'text', 'page', 'user' ],
+                       [
+                               'tables' => [
+                                       'revision', 'page', 'user', 'text'
+                               ],
+                               'fields' => array_merge(
+                                       $this->getRevisionQueryFields( true ),
+                                       $this->getPageQueryFields(),
+                                       $this->getUserQueryFields(),
+                                       $this->getTextQueryFields(),
+                                       $this->getOldActorQueryFields( 'rev' ),
+                                       $this->getOldCommentQueryFields( 'rev' )
+                               ),
+                               'joins' => [
+                                       'page' => [
+                                               'INNER JOIN',
+                                               [ 'page_id = rev_page' ],
+                                       ],
+                                       'user' => [
+                                               'LEFT JOIN',
+                                               [
+                                                       'rev_user != 0',
+                                                       'user_id = rev_user',
+                                               ],
+                                       ],
+                                       'text' => [
+                                               'INNER JOIN',
+                                               [ 'rev_text_id=old_id' ],
+                                       ],
+                               ],
+                       ],
+               ];
+       }
+
+       public function provideSlotsQueryInfo() {
+               yield 'MCR, no options' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+                       ],
+                       [],
+                       [
+                               'tables' => [
+                                       'slots'
+                               ],
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                               ],
+                               'joins' => [],
+                       ]
+               ];
+               yield 'MCR, role option' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+                       ],
+                       [ 'role' ],
+                       [
+                               'tables' => [
+                                       'slots',
+                                       'slot_roles',
+                               ],
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                                       'role_name',
+                               ],
+                               'joins' => [
+                                       'slot_roles' => [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ],
+                               ],
+                       ]
+               ];
+               yield 'MCR read-new, content option' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                       ],
+                       [ 'content' ],
+                       [
+                               'tables' => [
+                                       'slots',
+                                       'content',
+                               ],
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                                       'content_size',
+                                       'content_sha1',
+                                       'content_address',
+                                       'content_model',
+                               ],
+                               'joins' => [
+                                       'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+                               ],
+                       ]
+               ];
+               yield 'MCR read-new, content and model options' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                       ],
+                       [ 'content', 'model' ],
+                       [
+                               'tables' => [
+                                       'slots',
+                                       'content',
+                                       'content_models',
+                               ],
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                                       'content_size',
+                                       'content_sha1',
+                                       'content_address',
+                                       'content_model',
+                                       'model_name',
+                               ],
+                               'joins' => [
+                                       'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+                                       'content_models' => [ 'LEFT JOIN', [ 'content_model = model_id' ] ],
+                               ],
+                       ]
+               ];
+
+               $db = wfGetDB( DB_REPLICA );
+
+               yield 'MCR write-both/read-old' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                       ],
+                       [],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield 'MCR write-both/read-old, content' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                       ],
+                       [ 'content' ],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+                                               'content_size' => 'slots.rev_len',
+                                               'content_sha1' => 'slots.rev_sha1',
+                                               'content_address' => $db->buildConcat( [
+                                                       $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
+                                               'model_name' => 'slots.rev_content_model',
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield 'MCR write-both/read-old, content, model, role' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                       ],
+                       [ 'content', 'model', 'role' ],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+                                               'content_size' => 'slots.rev_len',
+                                               'content_sha1' => 'slots.rev_sha1',
+                                               'content_address' => $db->buildConcat( [
+                                                       $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
+                                               'model_name' => 'slots.rev_content_model',
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield 'pre-MCR' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_OLD,
+                       ],
+                       [],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield 'pre-MCR, content' => [
+                       [
+                               'wgMultiContentRevisionSchemaMigrationStage'
+                                       => SCHEMA_COMPAT_OLD,
+                       ],
+                       [ 'content' ],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+                                               'content_size' => 'slots.rev_len',
+                                               'content_sha1' => 'slots.rev_sha1',
+                                               'content_address' =>
+                                                       $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
+                                               'model_name' => 'slots.rev_content_model',
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+       }
+
+       public function provideSelectFields() {
+               yield 'with model, comment, and actor' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                       ],
+                       'fields' => array_merge(
+                               [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_actor' => 'NULL',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                               ],
+                               $this->getContentHandlerQueryFields( 'rev' ),
+                               [
+                                       'rev_comment_old' => 'rev_comment',
+                                       'rev_comment_pk' => 'rev_id',
+                               ]
+                       ),
+               ];
+               yield 'no mode, no comment, no actor' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       'fields' => array_merge(
+                               [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_actor' => 'NULL',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                               ],
+                               $this->getOldCommentQueryFields( 'rev' )
+                       ),
+               ];
+       }
+
+       public function provideSelectArchiveFields() {
+               yield 'with model, comment, and actor' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                       ],
+                       'fields' => array_merge(
+                               [
+                                       'ar_id',
+                                       'ar_page_id',
+                                       'ar_rev_id',
+                                       'ar_text_id',
+                                       'ar_timestamp',
+                                       'ar_user_text',
+                                       'ar_user',
+                                       'ar_actor' => 'NULL',
+                                       'ar_minor_edit',
+                                       'ar_deleted',
+                                       'ar_len',
+                                       'ar_parent_id',
+                                       'ar_sha1',
+                               ],
+                               $this->getContentHandlerQueryFields( 'ar' ),
+                               [
+                                       'ar_comment_old' => 'ar_comment',
+                                       'ar_comment_id' => 'ar_comment_id',
+                               ]
+                       ),
+               ];
+               yield 'no mode, no comment, no actor' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                               'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+                       ],
+                       'fields' => array_merge(
+                               [
+                                       'ar_id',
+                                       'ar_page_id',
+                                       'ar_rev_id',
+                                       'ar_text_id',
+                                       'ar_timestamp',
+                                       'ar_user_text',
+                                       'ar_user',
+                                       'ar_actor' => 'NULL',
+                                       'ar_minor_edit',
+                                       'ar_deleted',
+                                       'ar_len',
+                                       'ar_parent_id',
+                                       'ar_sha1',
+                               ],
+                               $this->getOldCommentQueryFields( 'ar' )
+                       ),
+               ];
+       }
+
+       /**
+        * @dataProvider provideSelectFields
+        * @covers Revision::selectFields
+        */
+       public function testRevisionSelectFields( $migrationStageSettings, $expected ) {
+               $this->setMwGlobals( $migrationStageSettings );
+               $this->overrideMwServices();
+
+               $this->hideDeprecated( 'Revision::selectFields' );
+               $this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectFields() );
+       }
+
+       /**
+        * @dataProvider provideSelectArchiveFields
+        * @covers Revision::selectArchiveFields
+        */
+       public function testRevisionSelectArchiveFields( $migrationStageSettings, $expected ) {
+               $this->setMwGlobals( $migrationStageSettings );
+               $this->overrideMwServices();
+
+               $this->hideDeprecated( 'Revision::selectArchiveFields' );
+               $this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectArchiveFields() );
+       }
+
+       /**
+        * @covers Revision::userJoinCond
+        */
+       public function testRevisionUserJoinCond() {
+               $this->hideDeprecated( 'Revision::userJoinCond' );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD );
+               $this->overrideMwServices();
+               $this->assertEquals(
+                       [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+                       Revision::userJoinCond()
+               );
+       }
+
+       /**
+        * @covers Revision::pageJoinCond
+        */
+       public function testRevisionPageJoinCond() {
+               $this->hideDeprecated( 'Revision::pageJoinCond' );
+               $this->assertEquals(
+                       [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                       Revision::pageJoinCond()
+               );
+       }
+
+       /**
+        * @covers Revision::selectTextFields
+        */
+       public function testRevisionSelectTextFields() {
+               $this->hideDeprecated( 'Revision::selectTextFields' );
+               $this->assertEquals(
+                       $this->getTextQueryFields(),
+                       Revision::selectTextFields()
+               );
+       }
+
+       /**
+        * @covers Revision::selectPageFields
+        */
+       public function testRevisionSelectPageFields() {
+               $this->hideDeprecated( 'Revision::selectPageFields' );
+               $this->assertEquals(
+                       $this->getPageQueryFields(),
+                       Revision::selectPageFields()
+               );
+       }
+
+       /**
+        * @covers Revision::selectUserFields
+        */
+       public function testRevisionSelectUserFields() {
+               $this->hideDeprecated( 'Revision::selectUserFields' );
+               $this->assertEquals(
+                       $this->getUserQueryFields(),
+                       Revision::selectUserFields()
+               );
+       }
+
+       /**
+        * @covers Revision::getArchiveQueryInfo
+        * @dataProvider provideArchiveQueryInfo
+        */
+       public function testRevisionGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
+               $this->setMwGlobals( $migrationStageSettings );
+               $this->overrideMwServices();
+
+               $queryInfo = Revision::getArchiveQueryInfo();
+               $this->assertQueryInfoEquals( $expected, $queryInfo );
+       }
+
+       /**
+        * @covers Revision::getQueryInfo
+        * @dataProvider provideQueryInfo
+        */
+       public function testRevisionGetQueryInfo( $migrationStageSettings, $options, $expected ) {
+               $this->setMwGlobals( $migrationStageSettings );
+               $this->overrideMwServices();
+
+               $queryInfo = Revision::getQueryInfo( $options );
+               $this->assertQueryInfoEquals( $expected, $queryInfo );
+       }
+
+       /**
+        * @dataProvider provideQueryInfo
+        * @covers \MediaWiki\Revision\RevisionStore::getQueryInfo
+        */
+       public function testRevisionStoreGetQueryInfo( $migrationStageSettings, $options, $expected ) {
+               $this->setMwGlobals( $migrationStageSettings );
+               $this->overrideMwServices();
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+               $queryInfo = $store->getQueryInfo( $options );
+               $this->assertQueryInfoEquals( $expected, $queryInfo );
+       }
+
+       /**
+        * @dataProvider provideSlotsQueryInfo
+        * @covers \MediaWiki\Revision\RevisionStore::getSlotsQueryInfo
+        */
+       public function testRevisionStoreGetSlotsQueryInfo(
+               $migrationStageSettings,
+               $options,
+               $expected
+       ) {
+               $this->setMwGlobals( $migrationStageSettings );
+               $this->overrideMwServices();
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+               $queryInfo = $store->getSlotsQueryInfo( $options );
+               $this->assertQueryInfoEquals( $expected, $queryInfo );
+       }
+
+       /**
+        * @dataProvider provideArchiveQueryInfo
+        * @covers \MediaWiki\Revision\RevisionStore::getArchiveQueryInfo
+        */
+       public function testRevisionStoreGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
+               $this->setMwGlobals( $migrationStageSettings );
+               $this->overrideMwServices();
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+               $queryInfo = $store->getArchiveQueryInfo();
+               $this->assertQueryInfoEquals( $expected, $queryInfo );
+       }
+
+       private function assertQueryInfoEquals( $expected, $queryInfo ) {
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['tables'],
+                       $queryInfo['tables'],
+                       'tables'
+               );
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['fields'],
+                       $queryInfo['fields'],
+                       'fields'
+               );
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['joins'],
+                       $queryInfo['joins'],
+                       'joins'
+               );
+       }
+
+       /**
+        * Assert that the two arrays passed are equal, ignoring the order of the values that integer
+        * keys.
+        *
+        * Note: Failures of this assertion can be slightly confusing as the arrays are actually
+        * split into a string key array and an int key array before assertions occur.
+        *
+        * @param array $expected
+        * @param array $actual
+        */
+       private function assertArrayEqualsIgnoringIntKeyOrder(
+               array $expected,
+               array $actual,
+               $message = null
+       ) {
+               $this->objectAssociativeSort( $expected );
+               $this->objectAssociativeSort( $actual );
+
+               // Separate the int key values from the string key values so that assertion failures are
+               // easier to understand.
+               $expectedIntKeyValues = [];
+               $actualIntKeyValues = [];
+
+               // Remove all int keys and re add them at the end after sorting by value
+               // This will result in all int keys being in the same order with same ints at the end of
+               // the array
+               foreach ( $expected as $key => $value ) {
+                       if ( is_int( $key ) ) {
+                               unset( $expected[$key] );
+                               $expectedIntKeyValues[] = $value;
+                       }
+               }
+               foreach ( $actual as $key => $value ) {
+                       if ( is_int( $key ) ) {
+                               unset( $actual[$key] );
+                               $actualIntKeyValues[] = $value;
+                       }
+               }
+
+               $this->objectAssociativeSort( $expected );
+               $this->objectAssociativeSort( $actual );
+
+               $this->objectAssociativeSort( $expectedIntKeyValues );
+               $this->objectAssociativeSort( $actualIntKeyValues );
+
+               $this->assertEquals( $expected, $actual, $message );
+               $this->assertEquals( $expectedIntKeyValues, $actualIntKeyValues, $message );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/RevisionRecordTests.php b/tests/phpunit/includes/Revision/RevisionRecordTests.php
new file mode 100644 (file)
index 0000000..a53cecc
--- /dev/null
@@ -0,0 +1,528 @@
+<?php
+
+// phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotClassTrait
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use LogicException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionStoreRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
+use MediaWiki\User\UserIdentityValue;
+use TextContent;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\RevisionRecord
+ *
+ * @note Expects to be used in classes that extend MediaWikiTestCase.
+ */
+trait RevisionRecordTests {
+
+       /**
+        * @param array $rowOverrides
+        *
+        * @return RevisionRecord
+        */
+       protected abstract function newRevision( array $rowOverrides = [] );
+
+       private function provideAudienceCheckData( $field ) {
+               yield 'field accessible for oversighter (ALL)' => [
+                       RevisionRecord::SUPPRESSED_ALL,
+                       [ 'oversight' ],
+                       true,
+                       false
+               ];
+
+               yield 'field accessible for oversighter' => [
+                       RevisionRecord::DELETED_RESTRICTED | $field,
+                       [ 'oversight' ],
+                       true,
+                       false
+               ];
+
+               yield 'field not accessible for sysops (ALL)' => [
+                       RevisionRecord::SUPPRESSED_ALL,
+                       [ 'sysop' ],
+                       false,
+                       false
+               ];
+
+               yield 'field not accessible for sysops' => [
+                       RevisionRecord::DELETED_RESTRICTED | $field,
+                       [ 'sysop' ],
+                       false,
+                       false
+               ];
+
+               yield 'field accessible for sysops' => [
+                       $field,
+                       [ 'sysop' ],
+                       true,
+                       false
+               ];
+
+               yield 'field suppressed for logged in users' => [
+                       $field,
+                       [ 'user' ],
+                       false,
+                       false
+               ];
+
+               yield 'unrelated field suppressed' => [
+                       $field === RevisionRecord::DELETED_COMMENT
+                               ? RevisionRecord::DELETED_USER
+                               : RevisionRecord::DELETED_COMMENT,
+                       [ 'user' ],
+                       true,
+                       true
+               ];
+
+               yield 'nothing suppressed' => [
+                       0,
+                       [ 'user' ],
+                       true,
+                       true
+               ];
+       }
+
+       public function testSerialization_fails() {
+               $this->setExpectedException( LogicException::class );
+               $rev = $this->newRevision();
+               serialize( $rev );
+       }
+
+       public function provideGetComment_audience() {
+               return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT );
+       }
+
+       private function forceStandardPermissions() {
+               $this->setMwGlobals(
+                       'wgGroupPermissions',
+                       [
+                               'user' => [
+                                       'viewsuppressed' => false,
+                                       'suppressrevision' => false,
+                                       'deletedtext' => false,
+                                       'deletedhistory' => false,
+                               ],
+                               'sysop' => [
+                                       'viewsuppressed' => false,
+                                       'suppressrevision' => false,
+                                       'deletedtext' => true,
+                                       'deletedhistory' => true,
+                               ],
+                               'oversight' => [
+                                       'deletedtext' => true,
+                                       'deletedhistory' => true,
+                                       'viewsuppressed' => true,
+                                       'suppressrevision' => true,
+                               ],
+                       ]
+               );
+       }
+
+       /**
+        * @dataProvider provideGetComment_audience
+        */
+       public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
+               $this->forceStandardPermissions();
+
+               $user = $this->getTestUser( $groups )->getUser();
+               $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
+
+               $this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' );
+
+               $this->assertSame(
+                       $publicCan,
+                       $rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null,
+                       'public can'
+               );
+               $this->assertSame(
+                       $userCan,
+                       $rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null,
+                       'user can'
+               );
+       }
+
+       public function provideGetUser_audience() {
+               return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER );
+       }
+
+       /**
+        * @dataProvider provideGetUser_audience
+        */
+       public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
+               $this->forceStandardPermissions();
+
+               $user = $this->getTestUser( $groups )->getUser();
+               $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
+
+               $this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' );
+
+               $this->assertSame(
+                       $publicCan,
+                       $rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null,
+                       'public can'
+               );
+               $this->assertSame(
+                       $userCan,
+                       $rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null,
+                       'user can'
+               );
+       }
+
+       public function provideGetSlot_audience() {
+               return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
+       }
+
+       /**
+        * @dataProvider provideGetSlot_audience
+        */
+       public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
+               $this->forceStandardPermissions();
+
+               $user = $this->getTestUser( $groups )->getUser();
+               $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
+
+               // NOTE: slot meta-data is never suppressed, just the content is!
+               $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ), 'hasSlot is never suppressed' );
+               $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw meta' );
+               $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ),
+                       'public meta' );
+
+               $this->assertNotNull(
+                       $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ),
+                       'user can'
+               );
+
+               try {
+                       $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC )->getContent();
+                       $exception = null;
+               } catch ( SuppressedDataException $ex ) {
+                       $exception = $ex;
+               }
+
+               $this->assertSame(
+                       $publicCan,
+                       $exception === null,
+                       'public can'
+               );
+
+               try {
+                       $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )->getContent();
+                       $exception = null;
+               } catch ( SuppressedDataException $ex ) {
+                       $exception = $ex;
+               }
+
+               $this->assertSame(
+                       $userCan,
+                       $exception === null,
+                       'user can'
+               );
+       }
+
+       /**
+        * @dataProvider provideGetSlot_audience
+        */
+       public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
+               $this->forceStandardPermissions();
+
+               $user = $this->getTestUser( $groups )->getUser();
+               $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
+
+               $this->assertNotNull( $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw can' );
+
+               $this->assertSame(
+                       $publicCan,
+                       $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ) !== null,
+                       'public can'
+               );
+               $this->assertSame(
+                       $userCan,
+                       $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ) !== null,
+                       'user can'
+               );
+       }
+
+       public function testGetSlot() {
+               $rev = $this->newRevision();
+
+               $slot = $rev->getSlot( SlotRecord::MAIN );
+               $this->assertNotNull( $slot, 'getSlot()' );
+               $this->assertSame( 'main', $slot->getRole(), 'getRole()' );
+       }
+
+       public function testHasSlot() {
+               $rev = $this->newRevision();
+
+               $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ) );
+               $this->assertFalse( $rev->hasSlot( 'xyz' ) );
+       }
+
+       public function testGetContent() {
+               $rev = $this->newRevision();
+
+               $content = $rev->getSlot( SlotRecord::MAIN );
+               $this->assertNotNull( $content, 'getContent()' );
+               $this->assertSame( CONTENT_MODEL_TEXT, $content->getModel(), 'getModel()' );
+       }
+
+       public function provideUserCanBitfield() {
+               yield [ 0, 0, [], null, true ];
+               // Bitfields match, user has no permissions
+               yield [
+                       RevisionRecord::DELETED_TEXT,
+                       RevisionRecord::DELETED_TEXT,
+                       [],
+                       null,
+                       false
+               ];
+               yield [
+                       RevisionRecord::DELETED_COMMENT,
+                       RevisionRecord::DELETED_COMMENT,
+                       [],
+                       null,
+                       false,
+               ];
+               yield [
+                       RevisionRecord::DELETED_USER,
+                       RevisionRecord::DELETED_USER,
+                       [],
+                       null,
+                       false
+               ];
+               yield [
+                       RevisionRecord::DELETED_RESTRICTED,
+                       RevisionRecord::DELETED_RESTRICTED,
+                       [],
+                       null,
+                       false,
+               ];
+               // Bitfields match, user (admin) does have permissions
+               yield [
+                       RevisionRecord::DELETED_TEXT,
+                       RevisionRecord::DELETED_TEXT,
+                       [ 'sysop' ],
+                       null,
+                       true,
+               ];
+               yield [
+                       RevisionRecord::DELETED_COMMENT,
+                       RevisionRecord::DELETED_COMMENT,
+                       [ 'sysop' ],
+                       null,
+                       true,
+               ];
+               yield [
+                       RevisionRecord::DELETED_USER,
+                       RevisionRecord::DELETED_USER,
+                       [ 'sysop' ],
+                       null,
+                       true,
+               ];
+               // Bitfields match, user (admin) does not have permissions
+               yield [
+                       RevisionRecord::DELETED_RESTRICTED,
+                       RevisionRecord::DELETED_RESTRICTED,
+                       [ 'sysop' ],
+                       null,
+                       false,
+               ];
+               // Bitfields match, user (oversight) does have permissions
+               yield [
+                       RevisionRecord::DELETED_RESTRICTED,
+                       RevisionRecord::DELETED_RESTRICTED,
+                       [ 'oversight' ],
+                       null,
+                       true,
+               ];
+               // Check permissions using the title
+               yield [
+                       RevisionRecord::DELETED_TEXT,
+                       RevisionRecord::DELETED_TEXT,
+                       [ 'sysop' ],
+                       __METHOD__,
+                       true,
+               ];
+               yield [
+                       RevisionRecord::DELETED_TEXT,
+                       RevisionRecord::DELETED_TEXT,
+                       [],
+                       __METHOD__,
+                       false,
+               ];
+       }
+
+       /**
+        * @dataProvider provideUserCanBitfield
+        * @covers \MediaWiki\Revision\RevisionRecord::userCanBitfield
+        */
+       public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
+               if ( is_string( $title ) ) {
+                       // NOTE: Data providers cannot instantiate Title objects! See T202641.
+                       $title = Title::newFromText( $title );
+               }
+
+               $this->forceStandardPermissions();
+
+               $user = $this->getTestUser( $userGroups )->getUser();
+
+               $this->assertSame(
+                       $expected,
+                       RevisionRecord::userCanBitfield( $bitField, $field, $user, $title )
+               );
+       }
+
+       public function provideHasSameContent() {
+               // Create some slots with content
+               $mainA = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'A' ) );
+               $mainB = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'B' ) );
+               $auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
+               $auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
+
+               $initialRecordSpec = [ [ $mainA ], 12 ];
+
+               return [
+                       'same record object' => [
+                               true,
+                               $initialRecordSpec,
+                               $initialRecordSpec,
+                       ],
+                       'same record content, different object' => [
+                               true,
+                               [ [ $mainA ], 12 ],
+                               [ [ $mainA ], 13 ],
+                       ],
+                       'same record content, aux slot, different object' => [
+                               true,
+                               [ [ $auxA ], 12 ],
+                               [ [ $auxB ], 13 ],
+                       ],
+                       'different content' => [
+                               false,
+                               [ [ $mainA ], 12 ],
+                               [ [ $mainB ], 13 ],
+                       ],
+                       'different content and number of slots' => [
+                               false,
+                               [ [ $mainA ], 12 ],
+                               [ [ $mainA, $mainB ], 13 ],
+                       ],
+               ];
+       }
+
+       /**
+        * @note Do not call directly from a data provider! Data providers cannot instantiate
+        * Title objects! See T202641.
+        *
+        * @param SlotRecord[] $slots
+        * @param int $revId
+        * @return RevisionStoreRecord
+        */
+       private function makeHasSameContentTestRecord( array $slots, $revId ) {
+               $title = Title::newFromText( 'provideHasSameContent' );
+               $title->resetArticleID( 19 );
+               $slots = new RevisionSlots( $slots );
+
+               return new RevisionStoreRecord(
+                       $title,
+                       new UserIdentityValue( 11, __METHOD__, 0 ),
+                       CommentStoreComment::newUnsavedComment( __METHOD__ ),
+                       (object)[
+                               'rev_id' => strval( $revId ),
+                               'rev_page' => strval( $title->getArticleID() ),
+                               'rev_timestamp' => '20200101000000',
+                               'rev_deleted' => 0,
+                               'rev_minor_edit' => 0,
+                               'rev_parent_id' => '5',
+                               'rev_len' => $slots->computeSize(),
+                               'rev_sha1' => $slots->computeSha1(),
+                               'page_latest' => '18',
+                       ],
+                       $slots
+               );
+       }
+
+       /**
+        * @dataProvider provideHasSameContent
+        * @covers \MediaWiki\Revision\RevisionRecord::hasSameContent
+        * @group Database
+        */
+       public function testHasSameContent(
+               $expected,
+               $recordSpec1,
+               $recordSpec2
+       ) {
+               $record1 = $this->makeHasSameContentTestRecord( ...$recordSpec1 );
+               $record2 = $this->makeHasSameContentTestRecord( ...$recordSpec2 );
+
+               $this->assertSame(
+                       $expected,
+                       $record1->hasSameContent( $record2 )
+               );
+       }
+
+       public function provideIsDeleted() {
+               yield 'no deletion' => [
+                       0,
+                       [
+                               RevisionRecord::DELETED_TEXT => false,
+                               RevisionRecord::DELETED_COMMENT => false,
+                               RevisionRecord::DELETED_USER => false,
+                               RevisionRecord::DELETED_RESTRICTED => false,
+                       ]
+               ];
+               yield 'text deleted' => [
+                       RevisionRecord::DELETED_TEXT,
+                       [
+                               RevisionRecord::DELETED_TEXT => true,
+                               RevisionRecord::DELETED_COMMENT => false,
+                               RevisionRecord::DELETED_USER => false,
+                               RevisionRecord::DELETED_RESTRICTED => false,
+                       ]
+               ];
+               yield 'text and comment deleted' => [
+                       RevisionRecord::DELETED_TEXT + RevisionRecord::DELETED_COMMENT,
+                       [
+                               RevisionRecord::DELETED_TEXT => true,
+                               RevisionRecord::DELETED_COMMENT => true,
+                               RevisionRecord::DELETED_USER => false,
+                               RevisionRecord::DELETED_RESTRICTED => false,
+                       ]
+               ];
+               yield 'all 4 deleted' => [
+                       RevisionRecord::DELETED_TEXT +
+                       RevisionRecord::DELETED_COMMENT +
+                       RevisionRecord::DELETED_RESTRICTED +
+                       RevisionRecord::DELETED_USER,
+                       [
+                               RevisionRecord::DELETED_TEXT => true,
+                               RevisionRecord::DELETED_COMMENT => true,
+                               RevisionRecord::DELETED_USER => true,
+                               RevisionRecord::DELETED_RESTRICTED => true,
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideIsDeleted
+        * @covers \MediaWiki\Revision\RevisionRecord::isDeleted
+        */
+       public function testIsDeleted( $revDeleted, $assertionMap ) {
+               $rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] );
+               foreach ( $assertionMap as $deletionLevel => $expected ) {
+                       $this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) );
+               }
+       }
+
+       public function testIsReadyForInsertion() {
+               $rev = $this->newRevision();
+               $this->assertTrue( $rev->isReadyForInsertion() );
+       }
+
+}
index ca13899..469f281 100644 (file)
@@ -6,12 +6,12 @@ use CommentStoreComment;
 use Content;
 use Language;
 use LogicException;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\Revision\RevisionRenderer;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\User\UserIdentityValue;
+use MediaWiki\Revision\SlotRecord;
 use MediaWikiTestCase;
+use MediaWiki\User\UserIdentityValue;
 use ParserOptions;
 use ParserOutput;
 use PHPUnit\Framework\MockObject\MockObject;
diff --git a/tests/phpunit/includes/Revision/RevisionSlotsTest.php b/tests/phpunit/includes/Revision/RevisionSlotsTest.php
new file mode 100644 (file)
index 0000000..d8e7d92
--- /dev/null
@@ -0,0 +1,257 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\SlotRecord;
+use MediaWikiTestCase;
+use TextContent;
+use WikitextContent;
+
+class RevisionSlotsTest extends MediaWikiTestCase {
+
+       /**
+        * @param SlotRecord[] $slots
+        * @return RevisionSlots
+        */
+       protected function newRevisionSlots( $slots = [] ) {
+               return new RevisionSlots( $slots );
+       }
+
+       public function provideConstructorFailue() {
+               yield 'not an array or callable' => [
+                       'foo'
+               ];
+               yield 'array of the wrong thing' => [
+                       [ 1, 2, 3 ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructorFailue
+        * @param $slots
+        *
+        * @covers \MediaWiki\Revision\RevisionSlots::__construct
+        * @covers \MediaWiki\Revision\RevisionSlots::setSlotsInternal
+        */
+       public function testConstructorFailue( $slots ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+
+               new RevisionSlots( $slots );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionSlots::getSlot
+        */
+       public function testGetSlot() {
+               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
+               $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
+
+               $this->assertSame( $mainSlot, $slots->getSlot( SlotRecord::MAIN ) );
+               $this->assertSame( $auxSlot, $slots->getSlot( 'aux' ) );
+               $this->setExpectedException( RevisionAccessException::class );
+               $slots->getSlot( 'nothere' );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionSlots::hasSlot
+        */
+       public function testHasSlot() {
+               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
+               $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
+
+               $this->assertTrue( $slots->hasSlot( SlotRecord::MAIN ) );
+               $this->assertTrue( $slots->hasSlot( 'aux' ) );
+               $this->assertFalse( $slots->hasSlot( 'AUX' ) );
+               $this->assertFalse( $slots->hasSlot( 'xyz' ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionSlots::getContent
+        */
+       public function testGetContent() {
+               $mainContent = new WikitextContent( 'A' );
+               $auxContent = new WikitextContent( 'B' );
+               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, $mainContent );
+               $auxSlot = SlotRecord::newUnsaved( 'aux', $auxContent );
+               $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
+
+               $this->assertSame( $mainContent, $slots->getContent( SlotRecord::MAIN ) );
+               $this->assertSame( $auxContent, $slots->getContent( 'aux' ) );
+               $this->setExpectedException( RevisionAccessException::class );
+               $slots->getContent( 'nothere' );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionSlots::getSlotRoles
+        */
+       public function testGetSlotRoles_someSlots() {
+               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
+               $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
+
+               $this->assertSame( [ 'main', 'aux' ], $slots->getSlotRoles() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionSlots::getSlotRoles
+        */
+       public function testGetSlotRoles_noSlots() {
+               $slots = $this->newRevisionSlots( [] );
+
+               $this->assertSame( [], $slots->getSlotRoles() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionSlots::getSlots
+        */
+       public function testGetSlots() {
+               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
+               $slotsArray = [ $mainSlot, $auxSlot ];
+               $slots = $this->newRevisionSlots( $slotsArray );
+
+               $this->assertEquals( [ 'main' => $mainSlot, 'aux' => $auxSlot ], $slots->getSlots() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionSlots::getInheritedSlots
+        */
+       public function testGetInheritedSlots() {
+               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $auxSlot = SlotRecord::newInherited(
+                       SlotRecord::newSaved(
+                               7, 7, 'foo',
+                               SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
+                       )
+               );
+               $slotsArray = [ $mainSlot, $auxSlot ];
+               $slots = $this->newRevisionSlots( $slotsArray );
+
+               $this->assertEquals( [ 'aux' => $auxSlot ], $slots->getInheritedSlots() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionSlots::getOriginalSlots
+        */
+       public function testGetOriginalSlots() {
+               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $auxSlot = SlotRecord::newInherited(
+                       SlotRecord::newSaved(
+                               7, 7, 'foo',
+                               SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
+                       )
+               );
+               $slotsArray = [ $mainSlot, $auxSlot ];
+               $slots = $this->newRevisionSlots( $slotsArray );
+
+               $this->assertEquals( [ 'main' => $mainSlot ], $slots->getOriginalSlots() );
+       }
+
+       public function provideComputeSize() {
+               yield [ 1, [ 'A' ] ];
+               yield [ 2, [ 'AA' ] ];
+               yield [ 4, [ 'AA', 'X', 'H' ] ];
+       }
+
+       /**
+        * @dataProvider provideComputeSize
+        * @covers \MediaWiki\Revision\RevisionSlots::computeSize
+        */
+       public function testComputeSize( $expected, $contentStrings ) {
+               $slotsArray = [];
+               foreach ( $contentStrings as $key => $contentString ) {
+                       $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) );
+               }
+               $slots = $this->newRevisionSlots( $slotsArray );
+
+               $this->assertSame( $expected, $slots->computeSize() );
+       }
+
+       public function provideComputeSha1() {
+               yield [ 'ctqm7794fr2dp1taki8a88ovwnvmnmj', [ 'A' ] ];
+               yield [ 'eyq8wiwlcofnaiy4eid97gyfy60uw51', [ 'AA' ] ];
+               yield [ 'lavctqfpxartyjr31f853drgfl4kj1g', [ 'AA', 'X', 'H' ] ];
+       }
+
+       /**
+        * @dataProvider provideComputeSha1
+        * @covers \MediaWiki\Revision\RevisionSlots::computeSha1
+        * @note this test is a bit brittle as the hashes are hardcoded, perhaps just check that strings
+        *       are returned and different Slots objects return different strings?
+        */
+       public function testComputeSha1( $expected, $contentStrings ) {
+               $slotsArray = [];
+               foreach ( $contentStrings as $key => $contentString ) {
+                       $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) );
+               }
+               $slots = $this->newRevisionSlots( $slotsArray );
+
+               $this->assertSame( $expected, $slots->computeSha1() );
+       }
+
+       public function provideHasSameContent() {
+               $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) );
+               $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) );
+               $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) );
+               $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ );
+               $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) );
+
+               $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+               $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+               $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] );
+               $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] );
+               $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] );
+
+               yield 'same instance' => [ $a, $a, true ];
+               yield 'same slots' => [ $a, $a2, true ];
+               yield 'same content' => [ $a, $a3, true ];
+
+               yield 'different roles' => [ $a, $b, false ];
+               yield 'different content' => [ $a, $c, false ];
+       }
+
+       /**
+        * @dataProvider provideHasSameContent
+        * @covers \MediaWiki\Revision\RevisionSlots::hasSameContent
+        */
+       public function testHasSameContent( RevisionSlots $a, RevisionSlots $b, $same ) {
+               $this->assertSame( $same, $a->hasSameContent( $b ) );
+               $this->assertSame( $same, $b->hasSameContent( $a ) );
+       }
+
+       public function provideGetRolesWithDifferentContent() {
+               $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) );
+               $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) );
+               $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) );
+               $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ );
+               $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) );
+
+               $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+               $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+               $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] );
+               $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] );
+               $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] );
+
+               yield 'same instance' => [ $a, $a, [] ];
+               yield 'same slots' => [ $a, $a2, [] ];
+               yield 'same content' => [ $a, $a3, [] ];
+
+               yield 'different roles' => [ $a, $b, [ 'x', 'y' ] ];
+               yield 'different content' => [ $a, $c, [ 'z' ] ];
+       }
+
+       /**
+        * @dataProvider provideGetRolesWithDifferentContent
+        * @covers \MediaWiki\Revision\RevisionSlots::getRolesWithDifferentContent
+        */
+       public function testGetRolesWithDifferentContent( RevisionSlots $a, RevisionSlots $b, $roles ) {
+               $this->assertArrayEquals( $roles, $a->getRolesWithDifferentContent( $b ) );
+               $this->assertArrayEquals( $roles, $b->getRolesWithDifferentContent( $a ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
new file mode 100644 (file)
index 0000000..0d6a439
--- /dev/null
@@ -0,0 +1,1667 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use Content;
+use Exception;
+use HashBagOStuff;
+use InvalidArgumentException;
+use Language;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\User\UserIdentityValue;
+use MediaWikiTestCase;
+use PHPUnit_Framework_MockObject_MockObject;
+use Revision;
+use TestUserRegistry;
+use Title;
+use WANObjectCache;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DatabaseDomain;
+use Wikimedia\Rdbms\DatabaseSqlite;
+use Wikimedia\Rdbms\FakeResultWrapper;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\TransactionProfiler;
+use WikiPage;
+use WikitextContent;
+
+/**
+ * @group Database
+ * @group RevisionStore
+ */
+abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
+
+       /**
+        * @var Title
+        */
+       private $testPageTitle;
+
+       /**
+        * @var WikiPage
+        */
+       private $testPage;
+
+       /**
+        * @return int
+        */
+       abstract protected function getMcrMigrationStage();
+
+       /**
+        * @return bool
+        */
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+       /**
+        * @return string[]
+        */
+       abstract protected function getMcrTablesToReset();
+
+       public function setUp() {
+               parent::setUp();
+               $this->tablesUsed[] = 'archive';
+               $this->tablesUsed[] = 'page';
+               $this->tablesUsed[] = 'revision';
+               $this->tablesUsed[] = 'comment';
+
+               $this->tablesUsed += $this->getMcrTablesToReset();
+
+               $this->setMwGlobals( [
+                       'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
+                       'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
+                       'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                       'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+               ] );
+
+               $this->overrideMwServices();
+       }
+
+       protected function addCoreDBData() {
+               // Blank out. This would fail with a modified schema, and we don't need it.
+       }
+
+       /**
+        * @return Title
+        */
+       protected function getTestPageTitle() {
+               if ( $this->testPageTitle ) {
+                       return $this->testPageTitle;
+               }
+
+               $this->testPageTitle = Title::newFromText( 'UTPage-' . __CLASS__ );
+               return $this->testPageTitle;
+       }
+       /**
+        * @return WikiPage
+        */
+       protected function getTestPage() {
+               if ( $this->testPage ) {
+                       return $this->testPage;
+               }
+
+               $title = $this->getTestPageTitle();
+               $this->testPage = WikiPage::factory( $title );
+
+               if ( !$this->testPage->exists() ) {
+                       // Make sure we don't write to the live db.
+                       $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
+
+                       $user = static::getTestSysop()->getUser();
+
+                       $this->testPage->doEditContent(
+                               new WikitextContent( 'UTContent-' . __CLASS__ ),
+                               'UTPageSummary-' . __CLASS__,
+                               EDIT_NEW | EDIT_SUPPRESS_RC,
+                               false,
+                               $user
+                       );
+               }
+
+               return $this->testPage;
+       }
+
+       /**
+        * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
+        */
+       private function getLoadBalancerMock( array $server ) {
+               $domain = new DatabaseDomain( $server['dbname'], null, $server['tablePrefix'] );
+
+               $lb = $this->getMockBuilder( LoadBalancer::class )
+                       ->setMethods( [ 'reallyOpenConnection' ] )
+                       ->setConstructorArgs( [
+                               [ 'servers' => [ $server ], 'localDomain' => $domain ]
+                       ] )
+                       ->getMock();
+
+               $lb->method( 'reallyOpenConnection' )->willReturnCallback(
+                       function ( array $server, $dbNameOverride ) {
+                               return $this->getDatabaseMock( $server );
+                       }
+               );
+
+               return $lb;
+       }
+
+       /**
+        * @return Database|PHPUnit_Framework_MockObject_MockObject
+        */
+       private function getDatabaseMock( array $params ) {
+               $db = $this->getMockBuilder( DatabaseSqlite::class )
+                       ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] )
+                       ->setConstructorArgs( [ $params ] )
+                       ->getMock();
+
+               $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) );
+               $db->method( 'isOpen' )->willReturn( true );
+
+               return $db;
+       }
+
+       public function provideDomainCheck() {
+               yield [ false, 'test', '' ];
+               yield [ 'test', 'test', '' ];
+
+               yield [ false, 'test', 'foo_' ];
+               yield [ 'test-foo_', 'test', 'foo_' ];
+
+               yield [ false, 'dash-test', '' ];
+               yield [ 'dash-test', 'dash-test', '' ];
+
+               yield [ false, 'underscore_test', 'foo_' ];
+               yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ];
+       }
+
+       /**
+        * @dataProvider provideDomainCheck
+        * @covers \MediaWiki\Revision\RevisionStore::checkDatabaseWikiId
+        */
+       public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) {
+               $this->setMwGlobals(
+                       [
+                               'wgDBname' => $dbName,
+                               'wgDBprefix' => $dbPrefix,
+                       ]
+               );
+
+               $loadBalancer = $this->getLoadBalancerMock(
+                       [
+                               'host' => '*dummy*',
+                               'dbDirectory' => '*dummy*',
+                               'user' => 'test',
+                               'password' => 'test',
+                               'flags' => 0,
+                               'variables' => [],
+                               'schema' => '',
+                               'cliMode' => true,
+                               'agent' => '',
+                               'load' => 100,
+                               'profiler' => null,
+                               'trxProfiler' => new TransactionProfiler(),
+                               'connLogger' => new \Psr\Log\NullLogger(),
+                               'queryLogger' => new \Psr\Log\NullLogger(),
+                               'errorLogger' => function () {
+                               },
+                               'deprecationLogger' => function () {
+                               },
+                               'type' => 'test',
+                               'dbname' => $dbName,
+                               'tablePrefix' => $dbPrefix,
+                       ]
+               );
+               $db = $loadBalancer->getConnection( DB_REPLICA );
+
+               /** @var SqlBlobStore $blobStore */
+               $blobStore = $this->getMockBuilder( SqlBlobStore::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $store = new RevisionStore(
+                       $loadBalancer,
+                       $blobStore,
+                       new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
+                       MediaWikiServices::getInstance()->getCommentStore(),
+                       MediaWikiServices::getInstance()->getContentModelStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       $this->getMcrMigrationStage(),
+                       MediaWikiServices::getInstance()->getActorMigration(),
+                       $wikiId
+               );
+
+               $count = $store->countRevisionsByPageId( $db, 0 );
+
+               // Dummy check to make PhpUnit happy. We are really only interested in
+               // countRevisionsByPageId not failing due to the DB domain check.
+               $this->assertSame( 0, $count );
+       }
+
+       protected function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) {
+               $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() );
+               $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() );
+               $this->assertEquals( $l1->getFragment(), $l2->getFragment() );
+               $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() );
+       }
+
+       protected function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) {
+               $this->assertEquals(
+                       $r1->getPageAsLinkTarget()->getNamespace(),
+                       $r2->getPageAsLinkTarget()->getNamespace()
+               );
+
+               $this->assertEquals(
+                       $r1->getPageAsLinkTarget()->getText(),
+                       $r2->getPageAsLinkTarget()->getText()
+               );
+
+               if ( $r1->getParentId() ) {
+                       $this->assertEquals( $r1->getParentId(), $r2->getParentId() );
+               }
+
+               $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() );
+               $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() );
+               $this->assertEquals( $r1->getComment(), $r2->getComment() );
+               $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() );
+               $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() );
+               $this->assertEquals( $r1->getSha1(), $r2->getSha1() );
+               $this->assertEquals( $r1->getSize(), $r2->getSize() );
+               $this->assertEquals( $r1->getPageId(), $r2->getPageId() );
+               $this->assertArrayEquals( $r1->getSlotRoles(), $r2->getSlotRoles() );
+               $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() );
+               $this->assertEquals( $r1->isMinor(), $r2->isMinor() );
+               foreach ( $r1->getSlotRoles() as $role ) {
+                       $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) );
+                       $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) );
+               }
+               foreach ( [
+                       RevisionRecord::DELETED_TEXT,
+                       RevisionRecord::DELETED_COMMENT,
+                       RevisionRecord::DELETED_USER,
+                       RevisionRecord::DELETED_RESTRICTED,
+               ] as $field ) {
+                       $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) );
+               }
+       }
+
+       protected function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) {
+               $this->assertSame( $s1->getRole(), $s2->getRole() );
+               $this->assertSame( $s1->getModel(), $s2->getModel() );
+               $this->assertSame( $s1->getFormat(), $s2->getFormat() );
+               $this->assertSame( $s1->getSha1(), $s2->getSha1() );
+               $this->assertSame( $s1->getSize(), $s2->getSize() );
+               $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) );
+
+               $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null;
+               $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null;
+       }
+
+       protected function assertRevisionCompleteness( RevisionRecord $r ) {
+               $this->assertTrue( $r->hasSlot( SlotRecord::MAIN ) );
+               $this->assertInstanceOf( SlotRecord::class, $r->getSlot( SlotRecord::MAIN ) );
+               $this->assertInstanceOf( Content::class, $r->getContent( SlotRecord::MAIN ) );
+
+               foreach ( $r->getSlotRoles() as $role ) {
+                       $this->assertSlotCompleteness( $r, $r->getSlot( $role ) );
+               }
+       }
+
+       protected function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) {
+               $this->assertTrue( $slot->hasAddress() );
+               $this->assertSame( $r->getId(), $slot->getRevision() );
+
+               $this->assertInstanceOf( Content::class, $slot->getContent() );
+       }
+
+       /**
+        * @param mixed[] $details
+        *
+        * @return RevisionRecord
+        */
+       private function getRevisionRecordFromDetailsArray( $details = [] ) {
+               // Convert some values that can't be provided by dataProviders
+               if ( isset( $details['user'] ) && $details['user'] === true ) {
+                       $details['user'] = $this->getTestUser()->getUser();
+               }
+               if ( isset( $details['page'] ) && $details['page'] === true ) {
+                       $details['page'] = $this->getTestPage()->getId();
+               }
+               if ( isset( $details['parent'] ) && $details['parent'] === true ) {
+                       $details['parent'] = $this->getTestPage()->getLatest();
+               }
+
+               // Create the RevisionRecord with any available data
+               $rev = new MutableRevisionRecord( $this->getTestPageTitle() );
+               isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null;
+               isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null;
+               isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null;
+               isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null;
+               isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null;
+               isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null;
+               isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null;
+               isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null;
+               isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null;
+               isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null;
+               isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null;
+
+               if ( isset( $details['content'] ) ) {
+                       foreach ( $details['content'] as $role => $content ) {
+                               $rev->setContent( $role, $content );
+                       }
+               }
+
+               return $rev;
+       }
+
+       public function provideInsertRevisionOn_successes() {
+               yield 'Bare minimum revision insertion' => [
+                       [
+                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+                               'page' => true,
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+               ];
+               yield 'Detailed revision insertion' => [
+                       [
+                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+                               'parent' => true,
+                               'page' => true,
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                               'minor' => true,
+                               'visibility' => RevisionRecord::DELETED_RESTRICTED,
+                       ],
+               ];
+       }
+
+       protected function getRandomCommentStoreComment() {
+               return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) );
+       }
+
+       /**
+        * @dataProvider provideInsertRevisionOn_successes
+        * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+        * @covers \MediaWiki\Revision\RevisionStore::insertSlotRowOn
+        * @covers \MediaWiki\Revision\RevisionStore::insertContentRowOn
+        */
+       public function testInsertRevisionOn_successes(
+               array $revDetails = []
+       ) {
+               $title = $this->getTestPageTitle();
+               $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
+
+               $this->overrideMwServices();
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
+
+               // is the new revision correct?
+               $this->assertRevisionCompleteness( $return );
+               $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() );
+               $this->assertRevisionRecordsEqual( $rev, $return );
+
+               // can we load it from the store?
+               $loaded = $store->getRevisionById( $return->getId() );
+               $this->assertRevisionCompleteness( $loaded );
+               $this->assertRevisionRecordsEqual( $return, $loaded );
+
+               // can we find it directly in the database?
+               $this->assertRevisionExistsInDatabase( $return );
+       }
+
+       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+               $row = $this->revisionToRow( new Revision( $rev ), [] );
+
+               // unset nulled fields
+               unset( $row->rev_content_model );
+               unset( $row->rev_content_format );
+
+               // unset fake fields
+               unset( $row->rev_comment_text );
+               unset( $row->rev_comment_data );
+               unset( $row->rev_comment_cid );
+               unset( $row->rev_comment_id );
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $queryInfo = $store->getQueryInfo( [ 'user' ] );
+
+               $row = get_object_vars( $row );
+               $this->assertSelect(
+                       $queryInfo['tables'],
+                       array_keys( $row ),
+                       [ 'rev_id' => $rev->getId() ],
+                       [ array_values( $row ) ],
+                       [],
+                       $queryInfo['joins']
+               );
+       }
+
+       /**
+        * @param SlotRecord $a
+        * @param SlotRecord $b
+        */
+       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+               // Assert that the same blob address has been used.
+               $this->assertSame( $a->getAddress(), $b->getAddress() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+        */
+       public function testInsertRevisionOn_blobAddressExists() {
+               $title = $this->getTestPageTitle();
+               $revDetails = [
+                       'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+                       'parent' => true,
+                       'comment' => $this->getRandomCommentStoreComment(),
+                       'timestamp' => '20171117010101',
+                       'user' => true,
+               ];
+
+               $this->overrideMwServices();
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+               // Insert the first revision
+               $revOne = $this->getRevisionRecordFromDetailsArray( $revDetails );
+               $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) );
+               $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() );
+               $this->assertRevisionRecordsEqual( $revOne, $firstReturn );
+
+               // Insert a second revision inheriting the same blob address
+               $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( SlotRecord::MAIN ) );
+               $revTwo = $this->getRevisionRecordFromDetailsArray( $revDetails );
+               $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) );
+               $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() );
+               $this->assertRevisionRecordsEqual( $revTwo, $secondReturn );
+
+               $firstMainSlot = $firstReturn->getSlot( SlotRecord::MAIN );
+               $secondMainSlot = $secondReturn->getSlot( SlotRecord::MAIN );
+
+               $this->assertSameSlotContent( $firstMainSlot, $secondMainSlot );
+
+               // And that different revisions have been created.
+               $this->assertNotSame( $firstReturn->getId(), $secondReturn->getId() );
+
+               // Make sure the slot rows reference the correct revision
+               $this->assertSame( $firstReturn->getId(), $firstMainSlot->getRevision() );
+               $this->assertSame( $secondReturn->getId(), $secondMainSlot->getRevision() );
+       }
+
+       public function provideInsertRevisionOn_failures() {
+               yield 'no slot' => [
+                       [
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+                       new InvalidArgumentException( 'main slot must be provided' )
+               ];
+               yield 'no main slot' => [
+                       [
+                               'slot' => SlotRecord::newUnsaved( 'aux', new WikitextContent( 'Turkey' ) ),
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+                       new InvalidArgumentException( 'main slot must be provided' )
+               ];
+               yield 'no timestamp' => [
+                       [
+                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'user' => true,
+                       ],
+                       new IncompleteRevisionException( 'timestamp field must not be NULL!' )
+               ];
+               yield 'no comment' => [
+                       [
+                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+                       new IncompleteRevisionException( 'comment must not be NULL!' )
+               ];
+               yield 'no user' => [
+                       [
+                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                       ],
+                       new IncompleteRevisionException( 'user must not be NULL!' )
+               ];
+       }
+
+       /**
+        * @dataProvider provideInsertRevisionOn_failures
+        * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+        */
+       public function testInsertRevisionOn_failures(
+               array $revDetails = [],
+               Exception $exception
+       ) {
+               $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+               $this->setExpectedException(
+                       get_class( $exception ),
+                       $exception->getMessage(),
+                       $exception->getCode()
+               );
+               $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
+       }
+
+       public function provideNewNullRevision() {
+               yield [
+                       Title::newFromText( 'UTPage_notAutoCreated' ),
+                       [ 'content' => [ 'main' => new WikitextContent( 'Flubber1' ) ] ],
+                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ),
+                       true,
+               ];
+               yield [
+                       Title::newFromText( 'UTPage_notAutoCreated' ),
+                       [ 'content' => [ 'main' => new WikitextContent( 'Flubber2' ) ] ],
+                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ),
+                       false,
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewNullRevision
+        * @covers \MediaWiki\Revision\RevisionStore::newNullRevision
+        * @covers \MediaWiki\Revision\RevisionStore::findSlotContentId
+        */
+       public function testNewNullRevision( Title $title, $revDetails, $comment, $minor = false ) {
+               $this->overrideMwServices();
+
+               $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
+               $page = WikiPage::factory( $title );
+
+               if ( !$page->exists() ) {
+                       $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__, EDIT_NEW );
+               }
+
+               $revDetails['page'] = $page->getId();
+               $revDetails['timestamp'] = wfTimestampNow();
+               $revDetails['comment'] = CommentStoreComment::newUnsavedComment( 'Base' );
+               $revDetails['user'] = $user;
+
+               $baseRev = $this->getRevisionRecordFromDetailsArray( $revDetails );
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+               $dbw = wfGetDB( DB_MASTER );
+               $baseRev = $store->insertRevisionOn( $baseRev, $dbw );
+               $page->updateRevisionOn( $dbw, new Revision( $baseRev ), $page->getLatest() );
+
+               $record = $store->newNullRevision(
+                       wfGetDB( DB_MASTER ),
+                       $title,
+                       $comment,
+                       $minor,
+                       $user
+               );
+
+               $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() );
+               $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() );
+               $this->assertEquals( $comment, $record->getComment() );
+               $this->assertEquals( $minor, $record->isMinor() );
+               $this->assertEquals( $user->getName(), $record->getUser()->getName() );
+               $this->assertEquals( $baseRev->getId(), $record->getParentId() );
+
+               $this->assertArrayEquals(
+                       $baseRev->getSlotRoles(),
+                       $record->getSlotRoles()
+               );
+
+               foreach ( $baseRev->getSlotRoles() as $role ) {
+                       $parentSlot = $baseRev->getSlot( $role );
+                       $slot = $record->getSlot( $role );
+
+                       $this->assertTrue( $slot->isInherited(), 'isInherited' );
+                       $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' );
+                       $this->assertSameSlotContent( $parentSlot, $slot );
+               }
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newNullRevision
+        */
+       public function testNewNullRevision_nonExistingTitle() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $record = $store->newNullRevision(
+                       wfGetDB( DB_MASTER ),
+                       Title::newFromText( __METHOD__ . '.iDontExist!' ),
+                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ),
+                       false,
+                       TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser()
+               );
+               $this->assertNull( $record );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getRcIdIfUnpatrolled
+        */
+       public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() {
+               $page = $this->getTestPage();
+               $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               /** @var Revision $rev */
+               $rev = $status->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revisionRecord = $store->getRevisionById( $rev->getId() );
+               $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
+
+               $this->assertGreaterThan( 0, $result );
+               $this->assertSame(
+                       $store->getRecentChange( $revisionRecord )->getAttribute( 'rc_id' ),
+                       $result
+               );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getRcIdIfUnpatrolled
+        */
+       public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() {
+               // This assumes that sysops are auto patrolled
+               $sysop = $this->getTestSysop()->getUser();
+               $page = $this->getTestPage();
+               $status = $page->doEditContent(
+                       new WikitextContent( __METHOD__ ),
+                       __METHOD__,
+                       0,
+                       false,
+                       $sysop
+               );
+               /** @var Revision $rev */
+               $rev = $status->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revisionRecord = $store->getRevisionById( $rev->getId() );
+               $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
+
+               $this->assertSame( 0, $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getRecentChange
+        */
+       public function testGetRecentChange() {
+               $page = $this->getTestPage();
+               $content = new WikitextContent( __METHOD__ );
+               $status = $page->doEditContent( $content, __METHOD__ );
+               /** @var Revision $rev */
+               $rev = $status->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revRecord = $store->getRevisionById( $rev->getId() );
+               $recentChange = $store->getRecentChange( $revRecord );
+
+               $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) );
+               $this->assertEquals( $rev->getRecentChange(), $recentChange );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getRevisionById
+        */
+       public function testGetRevisionById() {
+               $page = $this->getTestPage();
+               $content = new WikitextContent( __METHOD__ );
+               $status = $page->doEditContent( $content, __METHOD__ );
+               /** @var Revision $rev */
+               $rev = $status->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revRecord = $store->getRevisionById( $rev->getId() );
+
+               $this->assertSame( $rev->getId(), $revRecord->getId() );
+               $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
+               $this->assertSame( __METHOD__, $revRecord->getComment()->text );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getRevisionByTitle
+        */
+       public function testGetRevisionByTitle() {
+               $page = $this->getTestPage();
+               $content = new WikitextContent( __METHOD__ );
+               $status = $page->doEditContent( $content, __METHOD__ );
+               /** @var Revision $rev */
+               $rev = $status->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revRecord = $store->getRevisionByTitle( $page->getTitle() );
+
+               $this->assertSame( $rev->getId(), $revRecord->getId() );
+               $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
+               $this->assertSame( __METHOD__, $revRecord->getComment()->text );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getRevisionByPageId
+        */
+       public function testGetRevisionByPageId() {
+               $page = $this->getTestPage();
+               $content = new WikitextContent( __METHOD__ );
+               $status = $page->doEditContent( $content, __METHOD__ );
+               /** @var Revision $rev */
+               $rev = $status->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revRecord = $store->getRevisionByPageId( $page->getId() );
+
+               $this->assertSame( $rev->getId(), $revRecord->getId() );
+               $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
+               $this->assertSame( __METHOD__, $revRecord->getComment()->text );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getRevisionByTimestamp
+        */
+       public function testGetRevisionByTimestamp() {
+               // Make sure there is 1 second between the last revision and the rev we create...
+               // Otherwise we might not get the correct revision and the test may fail...
+               // :(
+               $page = $this->getTestPage();
+               sleep( 1 );
+               $content = new WikitextContent( __METHOD__ );
+               $status = $page->doEditContent( $content, __METHOD__ );
+               /** @var Revision $rev */
+               $rev = $status->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revRecord = $store->getRevisionByTimestamp(
+                       $page->getTitle(),
+                       $rev->getTimestamp()
+               );
+
+               $this->assertSame( $rev->getId(), $revRecord->getId() );
+               $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
+               $this->assertSame( __METHOD__, $revRecord->getComment()->text );
+       }
+
+       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+               // XXX: the WikiPage object loads another RevisionRecord from the database. Not great.
+               $page = WikiPage::factory( $rev->getTitle() );
+
+               $fields = [
+                       'rev_id' => (string)$rev->getId(),
+                       'rev_page' => (string)$rev->getPage(),
+                       'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ),
+                       'rev_user_text' => (string)$rev->getUserText(),
+                       'rev_user' => (string)$rev->getUser(),
+                       'rev_minor_edit' => $rev->isMinor() ? '1' : '0',
+                       'rev_deleted' => (string)$rev->getVisibility(),
+                       'rev_len' => (string)$rev->getSize(),
+                       'rev_parent_id' => (string)$rev->getParentId(),
+                       'rev_sha1' => (string)$rev->getSha1(),
+               ];
+
+               if ( in_array( 'page', $options ) ) {
+                       $fields += [
+                               'page_namespace' => (string)$page->getTitle()->getNamespace(),
+                               'page_title' => $page->getTitle()->getDBkey(),
+                               'page_id' => (string)$page->getId(),
+                               'page_latest' => (string)$page->getLatest(),
+                               'page_is_redirect' => $page->isRedirect() ? '1' : '0',
+                               'page_len' => (string)$page->getContent()->getSize(),
+                       ];
+               }
+
+               if ( in_array( 'user', $options ) ) {
+                       $fields += [
+                               'user_name' => (string)$rev->getUserText(),
+                       ];
+               }
+
+               if ( in_array( 'comment', $options ) ) {
+                       $fields += [
+                               'rev_comment_text' => $rev->getComment(),
+                               'rev_comment_data' => null,
+                               'rev_comment_cid' => null,
+                       ];
+               }
+
+               if ( $rev->getId() ) {
+                       $fields += [
+                               'rev_id' => (string)$rev->getId(),
+                       ];
+               }
+
+               return (object)$fields;
+       }
+
+       protected function assertRevisionRecordMatchesRevision(
+               Revision $rev,
+               RevisionRecord $record
+       ) {
+               $this->assertSame( $rev->getId(), $record->getId() );
+               $this->assertSame( $rev->getPage(), $record->getPageId() );
+               $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() );
+               $this->assertSame( $rev->getUserText(), $record->getUser()->getName() );
+               $this->assertSame( $rev->getUser(), $record->getUser()->getId() );
+               $this->assertSame( $rev->isMinor(), $record->isMinor() );
+               $this->assertSame( $rev->getVisibility(), $record->getVisibility() );
+               $this->assertSame( $rev->getSize(), $record->getSize() );
+               /**
+                * @note As of MW 1.31, the database schema allows the parent ID to be
+                * NULL to indicate that it is unknown.
+                */
+               $expectedParent = $rev->getParentId();
+               if ( $expectedParent === null ) {
+                       $expectedParent = 0;
+               }
+               $this->assertSame( $expectedParent, $record->getParentId() );
+               $this->assertSame( $rev->getSha1(), $record->getSha1() );
+               $this->assertSame( $rev->getComment(), $record->getComment()->text );
+               $this->assertSame( $rev->getContentFormat(),
+                       $record->getContent( SlotRecord::MAIN )->getDefaultFormat() );
+               $this->assertSame( $rev->getContentModel(), $record->getContent( SlotRecord::MAIN )->getModel() );
+               $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() );
+
+               $revRec = $rev->getRevisionRecord();
+               $revMain = $revRec->getSlot( SlotRecord::MAIN );
+               $recMain = $record->getSlot( SlotRecord::MAIN );
+
+               $this->assertSame( $revMain->hasOrigin(), $recMain->hasOrigin(), 'hasOrigin' );
+               $this->assertSame( $revMain->hasAddress(), $recMain->hasAddress(), 'hasAddress' );
+               $this->assertSame( $revMain->hasContentId(), $recMain->hasContentId(), 'hasContentId' );
+
+               if ( $revMain->hasOrigin() ) {
+                       $this->assertSame( $revMain->getOrigin(), $recMain->getOrigin(), 'getOrigin' );
+               }
+
+               if ( $revMain->hasAddress() ) {
+                       $this->assertSame( $revMain->getAddress(), $recMain->getAddress(), 'getAddress' );
+               }
+
+               if ( $revMain->hasContentId() ) {
+                       $this->assertSame( $revMain->getContentId(), $recMain->getContentId(), 'getContentId' );
+               }
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+        * @covers \MediaWiki\Revision\RevisionStore::getQueryInfo
+        */
+       public function testNewRevisionFromRow_getQueryInfo() {
+               $page = $this->getTestPage();
+               $text = __METHOD__ . 'a-ä';
+               /** @var Revision $rev */
+               $rev = $page->doEditContent(
+                       new WikitextContent( $text ),
+                       __METHOD__ . 'a'
+               )->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $info = $store->getQueryInfo();
+               $row = $this->db->selectRow(
+                       $info['tables'],
+                       $info['fields'],
+                       [ 'rev_id' => $rev->getId() ],
+                       __METHOD__,
+                       [],
+                       $info['joins']
+               );
+               $record = $store->newRevisionFromRow(
+                       $row,
+                       [],
+                       $page->getTitle()
+               );
+               $this->assertRevisionRecordMatchesRevision( $rev, $record );
+               $this->assertSame( $text, $rev->getContent()->serialize() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+        */
+       public function testNewRevisionFromRow_anonEdit() {
+               $page = $this->getTestPage();
+               $text = __METHOD__ . 'a-ä';
+               /** @var Revision $rev */
+               $rev = $page->doEditContent(
+                       new WikitextContent( $text ),
+                       __METHOD__ . 'a'
+               )->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $record = $store->newRevisionFromRow(
+                       $this->revisionToRow( $rev ),
+                       [],
+                       $page->getTitle()
+               );
+               $this->assertRevisionRecordMatchesRevision( $rev, $record );
+               $this->assertSame( $text, $rev->getContent()->serialize() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+        */
+       public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
+               $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
+               $this->overrideMwServices();
+               $page = $this->getTestPage();
+               $text = __METHOD__ . 'a-ä';
+               /** @var Revision $rev */
+               $rev = $page->doEditContent(
+                       new WikitextContent( $text ),
+                       __METHOD__ . 'a'
+               )->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $record = $store->newRevisionFromRow(
+                       $this->revisionToRow( $rev ),
+                       [],
+                       $page->getTitle()
+               );
+               $this->assertRevisionRecordMatchesRevision( $rev, $record );
+               $this->assertSame( $text, $rev->getContent()->serialize() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+        */
+       public function testNewRevisionFromRow_userEdit() {
+               $page = $this->getTestPage();
+               $text = __METHOD__ . 'b-ä';
+               /** @var Revision $rev */
+               $rev = $page->doEditContent(
+                       new WikitextContent( $text ),
+                       __METHOD__ . 'b',
+                       0,
+                       false,
+                       $this->getTestUser()->getUser()
+               )->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $record = $store->newRevisionFromRow(
+                       $this->revisionToRow( $rev ),
+                       [],
+                       $page->getTitle()
+               );
+               $this->assertRevisionRecordMatchesRevision( $rev, $record );
+               $this->assertSame( $text, $rev->getContent()->serialize() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromArchiveRow
+        * @covers \MediaWiki\Revision\RevisionStore::getArchiveQueryInfo
+        */
+       public function testNewRevisionFromArchiveRow_getArchiveQueryInfo() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $title = Title::newFromText( __METHOD__ );
+               $text = __METHOD__ . '-bä';
+               $page = WikiPage::factory( $title );
+               /** @var Revision $orig */
+               $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
+                       ->value['revision'];
+               $page->doDeleteArticle( __METHOD__ );
+
+               $db = wfGetDB( DB_MASTER );
+               $arQuery = $store->getArchiveQueryInfo();
+               $res = $db->select(
+                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+                       __METHOD__, [], $arQuery['joins']
+               );
+               $this->assertTrue( is_object( $res ), 'query failed' );
+
+               $row = $res->fetchObject();
+               $res->free();
+               $record = $store->newRevisionFromArchiveRow( $row );
+
+               $this->assertRevisionRecordMatchesRevision( $orig, $record );
+               $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromArchiveRow
+        */
+       public function testNewRevisionFromArchiveRow_legacyEncoding() {
+               $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
+               $this->overrideMwServices();
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $title = Title::newFromText( __METHOD__ );
+               $text = __METHOD__ . '-bä';
+               $page = WikiPage::factory( $title );
+               /** @var Revision $orig */
+               $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
+                       ->value['revision'];
+               $page->doDeleteArticle( __METHOD__ );
+
+               $db = wfGetDB( DB_MASTER );
+               $arQuery = $store->getArchiveQueryInfo();
+               $res = $db->select(
+                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+                       __METHOD__, [], $arQuery['joins']
+               );
+               $this->assertTrue( is_object( $res ), 'query failed' );
+
+               $row = $res->fetchObject();
+               $res->free();
+               $record = $store->newRevisionFromArchiveRow( $row );
+
+               $this->assertRevisionRecordMatchesRevision( $orig, $record );
+               $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromArchiveRow
+        */
+       public function testNewRevisionFromArchiveRow_no_user() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+               $row = (object)[
+                       'ar_id' => '1',
+                       'ar_page_id' => '2',
+                       'ar_namespace' => '0',
+                       'ar_title' => 'Something',
+                       'ar_rev_id' => '2',
+                       'ar_text_id' => '47',
+                       'ar_timestamp' => '20180528192356',
+                       'ar_minor_edit' => '0',
+                       'ar_deleted' => '0',
+                       'ar_len' => '78',
+                       'ar_parent_id' => '0',
+                       'ar_sha1' => 'deadbeef',
+                       'ar_comment_text' => 'whatever',
+                       'ar_comment_data' => null,
+                       'ar_comment_cid' => null,
+                       'ar_user' => '0',
+                       'ar_user_text' => '', // this is the important bit
+                       'ar_actor' => null,
+                       'ar_content_format' => null,
+                       'ar_content_model' => null,
+               ];
+
+               \Wikimedia\suppressWarnings();
+               $record = $store->newRevisionFromArchiveRow( $row );
+               \Wikimedia\suppressWarnings( true );
+
+               $this->assertInstanceOf( RevisionRecord::class, $record );
+               $this->assertInstanceOf( UserIdentityValue::class, $record->getUser() );
+               $this->assertSame( 'Unknown user', $record->getUser()->getName() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+        */
+       public function testNewRevisionFromRow_no_user() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $title = Title::newFromText( __METHOD__ );
+
+               $row = (object)[
+                       'rev_id' => '2',
+                       'rev_page' => '2',
+                       'page_namespace' => '0',
+                       'page_title' => $title->getText(),
+                       'rev_text_id' => '47',
+                       'rev_timestamp' => '20180528192356',
+                       'rev_minor_edit' => '0',
+                       'rev_deleted' => '0',
+                       'rev_len' => '78',
+                       'rev_parent_id' => '0',
+                       'rev_sha1' => 'deadbeef',
+                       'rev_comment_text' => 'whatever',
+                       'rev_comment_data' => null,
+                       'rev_comment_cid' => null,
+                       'rev_user' => '0',
+                       'rev_user_text' => '', // this is the important bit
+                       'rev_actor' => null,
+                       'rev_content_format' => null,
+                       'rev_content_model' => null,
+               ];
+
+               \Wikimedia\suppressWarnings();
+               $record = $store->newRevisionFromRow( $row, 0, $title );
+               \Wikimedia\suppressWarnings( true );
+
+               $this->assertNotNull( $record );
+               $this->assertNotNull( $record->getUser() );
+               $this->assertNotEmpty( $record->getUser()->getName() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+        */
+       public function testInsertRevisionOn_archive() {
+               // This is a round trip test for deletion and undeletion of a
+               // revision row via the archive table.
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $title = Title::newFromText( __METHOD__ );
+
+               $page = WikiPage::factory( $title );
+               /** @var Revision $origRev */
+               $page->doEditContent( new WikitextContent( "First" ), __METHOD__ . '-first' );
+               $origRev = $page->doEditContent( new WikitextContent( "Foo" ), __METHOD__ )
+                       ->value['revision'];
+               $orig = $origRev->getRevisionRecord();
+               $page->doDeleteArticle( __METHOD__ );
+
+               // re-create page, so we can later load revisions for it
+               $page->doEditContent( new WikitextContent( 'Two' ), __METHOD__ );
+
+               $db = wfGetDB( DB_MASTER );
+               $arQuery = $store->getArchiveQueryInfo();
+               $row = $db->selectRow(
+                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+                       __METHOD__, [], $arQuery['joins']
+               );
+
+               $this->assertNotFalse( $row, 'query failed' );
+
+               $record = $store->newRevisionFromArchiveRow(
+                       $row,
+                       0,
+                       $title,
+                       [ 'page_id' => $title->getArticleID() ]
+               );
+
+               $restored = $store->insertRevisionOn( $record, $db );
+
+               // is the new revision correct?
+               $this->assertRevisionCompleteness( $restored );
+               $this->assertRevisionRecordsEqual( $record, $restored );
+
+               // does the new revision use the original slot?
+               $recMain = $record->getSlot( SlotRecord::MAIN );
+               $restMain = $restored->getSlot( SlotRecord::MAIN );
+               $this->assertSame( $recMain->getAddress(), $restMain->getAddress() );
+               $this->assertSame( $recMain->getContentId(), $restMain->getContentId() );
+               $this->assertSame( $recMain->getOrigin(), $restMain->getOrigin() );
+               $this->assertSame( 'Foo', $restMain->getContent()->serialize() );
+
+               // can we load it from the store?
+               $loaded = $store->getRevisionById( $restored->getId() );
+               $this->assertNotNull( $loaded );
+               $this->assertRevisionCompleteness( $loaded );
+               $this->assertRevisionRecordsEqual( $restored, $loaded );
+
+               // can we find it directly in the database?
+               $this->assertRevisionExistsInDatabase( $restored );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::loadRevisionFromId
+        */
+       public function testLoadRevisionFromId() {
+               $title = Title::newFromText( __METHOD__ );
+               $page = WikiPage::factory( $title );
+               /** @var Revision $rev */
+               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+                       ->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() );
+               $this->assertRevisionRecordMatchesRevision( $rev, $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::loadRevisionFromPageId
+        */
+       public function testLoadRevisionFromPageId() {
+               $title = Title::newFromText( __METHOD__ );
+               $page = WikiPage::factory( $title );
+               /** @var Revision $rev */
+               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+                       ->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() );
+               $this->assertRevisionRecordMatchesRevision( $rev, $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::loadRevisionFromTitle
+        */
+       public function testLoadRevisionFromTitle() {
+               $title = Title::newFromText( __METHOD__ );
+               $page = WikiPage::factory( $title );
+               /** @var Revision $rev */
+               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+                       ->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title );
+               $this->assertRevisionRecordMatchesRevision( $rev, $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::loadRevisionFromTimestamp
+        */
+       public function testLoadRevisionFromTimestamp() {
+               $title = Title::newFromText( __METHOD__ );
+               $page = WikiPage::factory( $title );
+               /** @var Revision $revOne */
+               $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+                       ->value['revision'];
+               // Sleep to ensure different timestamps... )(evil)
+               sleep( 1 );
+               /** @var Revision $revTwo */
+               $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' )
+                       ->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $this->assertNull(
+                       $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' )
+               );
+               $this->assertSame(
+                       $revOne->getId(),
+                       $store->loadRevisionFromTimestamp(
+                               wfGetDB( DB_MASTER ),
+                               $title,
+                               $revOne->getTimestamp()
+                       )->getId()
+               );
+               $this->assertSame(
+                       $revTwo->getId(),
+                       $store->loadRevisionFromTimestamp(
+                               wfGetDB( DB_MASTER ),
+                               $title,
+                               $revTwo->getTimestamp()
+                       )->getId()
+               );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::listRevisionSizes
+        */
+       public function testGetParentLengths() {
+               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+               /** @var Revision $revOne */
+               $revOne = $page->doEditContent(
+                       new WikitextContent( __METHOD__ ), __METHOD__
+               )->value['revision'];
+               /** @var Revision $revTwo */
+               $revTwo = $page->doEditContent(
+                       new WikitextContent( __METHOD__ . '2' ), __METHOD__
+               )->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $this->assertSame(
+                       [
+                               $revOne->getId() => strlen( __METHOD__ ),
+                       ],
+                       $store->listRevisionSizes(
+                               wfGetDB( DB_MASTER ),
+                               [ $revOne->getId() ]
+                       )
+               );
+               $this->assertSame(
+                       [
+                               $revOne->getId() => strlen( __METHOD__ ),
+                               $revTwo->getId() => strlen( __METHOD__ ) + 1,
+                       ],
+                       $store->listRevisionSizes(
+                               wfGetDB( DB_MASTER ),
+                               [ $revOne->getId(), $revTwo->getId() ]
+                       )
+               );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getPreviousRevision
+        */
+       public function testGetPreviousRevision() {
+               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+               /** @var Revision $revOne */
+               $revOne = $page->doEditContent(
+                       new WikitextContent( __METHOD__ ), __METHOD__
+               )->value['revision'];
+               /** @var Revision $revTwo */
+               $revTwo = $page->doEditContent(
+                       new WikitextContent( __METHOD__ . '2' ), __METHOD__
+               )->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $this->assertNull(
+                       $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) )
+               );
+               $this->assertSame(
+                       $revOne->getId(),
+                       $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId()
+               );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getNextRevision
+        */
+       public function testGetNextRevision() {
+               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+               /** @var Revision $revOne */
+               $revOne = $page->doEditContent(
+                       new WikitextContent( __METHOD__ ), __METHOD__
+               )->value['revision'];
+               /** @var Revision $revTwo */
+               $revTwo = $page->doEditContent(
+                       new WikitextContent( __METHOD__ . '2' ), __METHOD__
+               )->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $this->assertSame(
+                       $revTwo->getId(),
+                       $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId()
+               );
+               $this->assertNull(
+                       $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) )
+               );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getTimestampFromId
+        */
+       public function testGetTimestampFromId_found() {
+               $page = $this->getTestPage();
+               /** @var Revision $rev */
+               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+                       ->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $result = $store->getTimestampFromId(
+                       $page->getTitle(),
+                       $rev->getId()
+               );
+
+               $this->assertSame( $rev->getTimestamp(), $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getTimestampFromId
+        */
+       public function testGetTimestampFromId_notFound() {
+               $page = $this->getTestPage();
+               /** @var Revision $rev */
+               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+                       ->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $result = $store->getTimestampFromId(
+                       $page->getTitle(),
+                       $rev->getId() + 1
+               );
+
+               $this->assertFalse( $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::countRevisionsByPageId
+        */
+       public function testCountRevisionsByPageId() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+
+               $this->assertSame(
+                       0,
+                       $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
+               );
+               $page->doEditContent( new WikitextContent( 'a' ), 'a' );
+               $this->assertSame(
+                       1,
+                       $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
+               );
+               $page->doEditContent( new WikitextContent( 'b' ), 'b' );
+               $this->assertSame(
+                       2,
+                       $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
+               );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::countRevisionsByTitle
+        */
+       public function testCountRevisionsByTitle() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+
+               $this->assertSame(
+                       0,
+                       $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
+               );
+               $page->doEditContent( new WikitextContent( 'a' ), 'a' );
+               $this->assertSame(
+                       1,
+                       $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
+               );
+               $page->doEditContent( new WikitextContent( 'b' ), 'b' );
+               $this->assertSame(
+                       2,
+                       $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
+               );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::userWasLastToEdit
+        */
+       public function testUserWasLastToEdit_false() {
+               $sysop = $this->getTestSysop()->getUser();
+               $page = $this->getTestPage();
+               $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $result = $store->userWasLastToEdit(
+                       wfGetDB( DB_MASTER ),
+                       $page->getId(),
+                       $sysop->getId(),
+                       '20160101010101'
+               );
+               $this->assertFalse( $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::userWasLastToEdit
+        */
+       public function testUserWasLastToEdit_true() {
+               $startTime = wfTimestampNow();
+               $sysop = $this->getTestSysop()->getUser();
+               $page = $this->getTestPage();
+               $page->doEditContent(
+                       new WikitextContent( __METHOD__ ),
+                       __METHOD__,
+                       0,
+                       false,
+                       $sysop
+               );
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $result = $store->userWasLastToEdit(
+                       wfGetDB( DB_MASTER ),
+                       $page->getId(),
+                       $sysop->getId(),
+                       $startTime
+               );
+               $this->assertTrue( $result );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getKnownCurrentRevision
+        */
+       public function testGetKnownCurrentRevision() {
+               $page = $this->getTestPage();
+               /** @var Revision $rev */
+               $rev = $page->doEditContent(
+                       new WikitextContent( __METHOD__ . 'b' ),
+                       __METHOD__ . 'b',
+                       0,
+                       false,
+                       $this->getTestUser()->getUser()
+               )->value['revision'];
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $record = $store->getKnownCurrentRevision(
+                       $page->getTitle(),
+                       $rev->getId()
+               );
+
+               $this->assertRevisionRecordMatchesRevision( $rev, $record );
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               yield 'Basic array, content object' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                               'content' => new WikitextContent( 'Some Content' ),
+                       ]
+               ];
+               yield 'Basic array, serialized text' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                               'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
+                       ]
+               ];
+               yield 'Basic array, serialized text, utf-8 flags' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                               'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
+                               'flags' => 'utf-8',
+                       ]
+               ];
+               yield 'Basic array, with title' => [
+                       [
+                               'title' => Title::newFromText( 'SomeText' ),
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                               'content' => new WikitextContent( 'Some Content' ),
+                       ]
+               ];
+               yield 'Basic array, no user field' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.3',
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                               'content' => new WikitextContent( 'Some Content' ),
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
+        */
+       public function testNewMutableRevisionFromArray( array $array ) {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+               // HACK: if $array['page'] is given, make sure that the page exists
+               if ( isset( $array['page'] ) ) {
+                       $t = Title::newFromID( $array['page'] );
+                       if ( !$t || !$t->exists() ) {
+                               $t = Title::makeTitle( NS_MAIN, __METHOD__ );
+                               $info = $this->insertPage( $t );
+                               $array['page'] = $info['id'];
+                       }
+               }
+
+               $result = $store->newMutableRevisionFromArray( $array );
+
+               if ( isset( $array['id'] ) ) {
+                       $this->assertSame( $array['id'], $result->getId() );
+               }
+               if ( isset( $array['page'] ) ) {
+                       $this->assertSame( $array['page'], $result->getPageId() );
+               }
+               $this->assertSame( $array['timestamp'], $result->getTimestamp() );
+               $this->assertSame( $array['user_text'], $result->getUser()->getName() );
+               if ( isset( $array['user'] ) ) {
+                       $this->assertSame( $array['user'], $result->getUser()->getId() );
+               }
+               $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() );
+               $this->assertSame( $array['deleted'], $result->getVisibility() );
+               $this->assertSame( $array['len'], $result->getSize() );
+               $this->assertSame( $array['parent_id'], $result->getParentId() );
+               $this->assertSame( $array['sha1'], $result->getSha1() );
+               $this->assertSame( $array['comment'], $result->getComment()->text );
+               if ( isset( $array['content'] ) ) {
+                       foreach ( $array['content'] as $role => $content ) {
+                               $this->assertTrue(
+                                       $result->getContent( $role )->equals( $content )
+                               );
+                       }
+               } elseif ( isset( $array['text'] ) ) {
+                       $this->assertSame( $array['text'],
+                               $result->getSlot( SlotRecord::MAIN )->getContent()->serialize() );
+               } elseif ( isset( $array['content_format'] ) ) {
+                       $this->assertSame(
+                               $array['content_format'],
+                               $result->getSlot( SlotRecord::MAIN )->getContent()->getDefaultFormat()
+                       );
+                       $this->assertSame( $array['content_model'], $result->getSlot( SlotRecord::MAIN )->getModel() );
+               }
+       }
+
+       /**
+        * @dataProvider provideNewMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
+        */
+       public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) {
+               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+               $blobStore = new SqlBlobStore( $lb, $cache );
+               $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
+
+               $factory = $this->getMockBuilder( BlobStoreFactory::class )
+                       ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $factory->expects( $this->any() )
+                       ->method( 'newBlobStore' )
+                       ->willReturn( $blobStore );
+               $factory->expects( $this->any() )
+                       ->method( 'newSqlBlobStore' )
+                       ->willReturn( $blobStore );
+
+               $this->setService( 'BlobStoreFactory', $factory );
+
+               $this->testNewMutableRevisionFromArray( $array );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php b/tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php
new file mode 100644 (file)
index 0000000..9904b3b
--- /dev/null
@@ -0,0 +1,179 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use ActorMigration;
+use CommentStore;
+use MediaWiki\Logger\Spi as LoggerSpi;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreFactory;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\NameTableStoreFactory;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWikiTestCase;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use WANObjectCache;
+use Wikimedia\Rdbms\ILBFactory;
+use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\TestingAccessWrapper;
+
+class RevisionStoreFactoryTest extends MediaWikiTestCase {
+
+       public function testValidConstruction_doesntCauseErrors() {
+               new RevisionStoreFactory(
+                       $this->getMockLoadBalancerFactory(),
+                       $this->getMockBlobStoreFactory(),
+                       $this->getNameTableStoreFactory(),
+                       $this->getHashWANObjectCache(),
+                       $this->getMockCommentStore(),
+                       ActorMigration::newMigration(),
+                       MIGRATION_OLD,
+                       $this->getMockLoggerSpi(),
+                       true
+               );
+               $this->assertTrue( true );
+       }
+
+       public function provideWikiIds() {
+               yield [ true ];
+               yield [ false ];
+               yield [ 'somewiki' ];
+               yield [ 'somewiki', MIGRATION_OLD , false ];
+               yield [ 'somewiki', MIGRATION_NEW , true ];
+       }
+
+       /**
+        * @dataProvider provideWikiIds
+        */
+       public function testGetRevisionStore(
+               $wikiId,
+               $mcrMigrationStage = MIGRATION_OLD,
+               $contentHandlerUseDb = true
+       ) {
+               $lbFactory = $this->getMockLoadBalancerFactory();
+               $blobStoreFactory = $this->getMockBlobStoreFactory();
+               $nameTableStoreFactory = $this->getNameTableStoreFactory();
+               $cache = $this->getHashWANObjectCache();
+               $commentStore = $this->getMockCommentStore();
+               $actorMigration = ActorMigration::newMigration();
+               $loggerProvider = $this->getMockLoggerSpi();
+
+               $factory = new RevisionStoreFactory(
+                       $lbFactory,
+                       $blobStoreFactory,
+                       $nameTableStoreFactory,
+                       $cache,
+                       $commentStore,
+                       $actorMigration,
+                       $mcrMigrationStage,
+                       $loggerProvider,
+                       $contentHandlerUseDb
+               );
+
+               $store = $factory->getRevisionStore( $wikiId );
+               $wrapper = TestingAccessWrapper::newFromObject( $store );
+
+               // ensure the correct object type is returned
+               $this->assertInstanceOf( RevisionStore::class, $store );
+
+               // ensure the RevisionStore is for the given wikiId
+               $this->assertSame( $wikiId, $wrapper->wikiId );
+
+               // ensure all other required services are correctly set
+               $this->assertSame( $cache, $wrapper->cache );
+               $this->assertSame( $commentStore, $wrapper->commentStore );
+               $this->assertSame( $mcrMigrationStage, $wrapper->mcrMigrationStage );
+               $this->assertSame( $actorMigration, $wrapper->actorMigration );
+               $this->assertSame( $contentHandlerUseDb, $store->getContentHandlerUseDB() );
+
+               $this->assertInstanceOf( ILoadBalancer::class, $wrapper->loadBalancer );
+               $this->assertInstanceOf( BlobStore::class, $wrapper->blobStore );
+               $this->assertInstanceOf( NameTableStore::class, $wrapper->contentModelStore );
+               $this->assertInstanceOf( NameTableStore::class, $wrapper->slotRoleStore );
+               $this->assertInstanceOf( LoggerInterface::class, $wrapper->logger );
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
+        */
+       private function getMockLoadBalancer() {
+               return $this->getMockBuilder( ILoadBalancer::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|ILBFactory
+        */
+       private function getMockLoadBalancerFactory() {
+               $mock = $this->getMockBuilder( ILBFactory::class )
+                       ->disableOriginalConstructor()->getMock();
+
+               $mock->method( 'getMainLB' )
+                       ->willReturnCallback( function () {
+                               return $this->getMockLoadBalancer();
+                       } );
+
+               return $mock;
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
+        */
+       private function getMockSqlBlobStore() {
+               return $this->getMockBuilder( SqlBlobStore::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|BlobStoreFactory
+        */
+       private function getMockBlobStoreFactory() {
+               $mock = $this->getMockBuilder( BlobStoreFactory::class )
+                       ->disableOriginalConstructor()->getMock();
+
+               $mock->method( 'newSqlBlobStore' )
+                       ->willReturnCallback( function () {
+                               return $this->getMockSqlBlobStore();
+                       } );
+
+               return $mock;
+       }
+
+       /**
+        * @return NameTableStoreFactory
+        */
+       private function getNameTableStoreFactory() {
+               return new NameTableStoreFactory(
+                       $this->getMockLoadBalancerFactory(),
+                       $this->getHashWANObjectCache(),
+                       new NullLogger() );
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
+        */
+       private function getMockCommentStore() {
+               return $this->getMockBuilder( CommentStore::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       private function getHashWANObjectCache() {
+               return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|LoggerSpi
+        */
+       private function getMockLoggerSpi() {
+               $mock = $this->getMock( LoggerSpi::class );
+
+               $mock->method( 'getLogger' )
+                       ->willReturn( new NullLogger() );
+
+               return $mock;
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/RevisionStoreRecordTest.php b/tests/phpunit/includes/Revision/RevisionStoreRecordTest.php
new file mode 100644 (file)
index 0000000..f1479da
--- /dev/null
@@ -0,0 +1,366 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use InvalidArgumentException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionStoreRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\User\UserIdentity;
+use MediaWiki\User\UserIdentityValue;
+use MediaWikiTestCase;
+use TextContent;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\RevisionStoreRecord
+ * @covers \MediaWiki\Revision\RevisionRecord
+ */
+class RevisionStoreRecordTest extends MediaWikiTestCase {
+
+       use RevisionRecordTests;
+
+       /**
+        * @param array $rowOverrides
+        *
+        * @return RevisionStoreRecord
+        */
+       protected function newRevision( array $rowOverrides = [] ) {
+               $title = Title::newFromText( 'Dummy' );
+               $title->resetArticleID( 17 );
+
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
+               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+               $slots = new RevisionSlots( [ $main, $aux ] );
+
+               $row = [
+                       'rev_id' => '7',
+                       'rev_page' => strval( $title->getArticleID() ),
+                       'rev_timestamp' => '20200101000000',
+                       'rev_deleted' => 0,
+                       'rev_minor_edit' => 0,
+                       'rev_parent_id' => '5',
+                       'rev_len' => $slots->computeSize(),
+                       'rev_sha1' => $slots->computeSha1(),
+                       'page_latest' => '18',
+               ];
+
+               $row = array_merge( $row, $rowOverrides );
+
+               return new RevisionStoreRecord( $title, $user, $comment, (object)$row, $slots );
+       }
+
+       public function provideConstructor() {
+               $title = Title::newFromText( 'Dummy' );
+               $title->resetArticleID( 17 );
+
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
+               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+               $slots = new RevisionSlots( [ $main, $aux ] );
+
+               $protoRow = [
+                       'rev_id' => '7',
+                       'rev_page' => strval( $title->getArticleID() ),
+                       'rev_timestamp' => '20200101000000',
+                       'rev_deleted' => 0,
+                       'rev_minor_edit' => 0,
+                       'rev_parent_id' => '5',
+                       'rev_len' => $slots->computeSize(),
+                       'rev_sha1' => $slots->computeSha1(),
+                       'page_latest' => '18',
+               ];
+
+               $row = $protoRow;
+               yield 'all info' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots,
+                       'acmewiki'
+               ];
+
+               $row = $protoRow;
+               $row['rev_minor_edit'] = '1';
+               $row['rev_deleted'] = strval( RevisionRecord::DELETED_USER );
+
+               yield 'minor deleted' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+               $row['page_latest'] = $row['rev_id'];
+
+               yield 'latest' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+               unset( $row['rev_parent'] );
+
+               yield 'no parent' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+               $row['rev_len'] = null;
+               $row['rev_sha1'] = '';
+
+               yield 'rev_len is null, rev_sha1 is ""' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+               yield 'no length, no hash' => [
+                       Title::newFromText( 'DummyDoesNotExist' ),
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructor
+        *
+        * @param Title $title
+        * @param UserIdentity $user
+        * @param CommentStoreComment $comment
+        * @param object $row
+        * @param RevisionSlots $slots
+        * @param bool $wikiId
+        */
+       public function testConstructorAndGetters(
+               Title $title,
+               UserIdentity $user,
+               CommentStoreComment $comment,
+               $row,
+               RevisionSlots $slots,
+               $wikiId = false
+       ) {
+               $rec = new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
+
+               $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
+               $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
+               $this->assertSame( $comment, $rec->getComment(), 'getComment' );
+
+               $this->assertSame( $slots, $rec->getSlots(), 'getSlots' );
+               $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
+               $this->assertSame( $slots->getSlots(), $rec->getSlots()->getSlots(), 'getSlots' );
+               $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
+
+               $this->assertSame( (int)$row->rev_id, $rec->getId(), 'getId' );
+               $this->assertSame( (int)$row->rev_page, $rec->getPageId(), 'getId' );
+               $this->assertSame( $row->rev_timestamp, $rec->getTimestamp(), 'getTimestamp' );
+               $this->assertSame( (int)$row->rev_deleted, $rec->getVisibility(), 'getVisibility' );
+               $this->assertSame( (bool)$row->rev_minor_edit, $rec->isMinor(), 'getIsMinor' );
+
+               if ( isset( $row->rev_parent_id ) ) {
+                       $this->assertSame( (int)$row->rev_parent_id, $rec->getParentId(), 'getParentId' );
+               } else {
+                       $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
+               }
+
+               if ( isset( $row->rev_len ) ) {
+                       $this->assertSame( (int)$row->rev_len, $rec->getSize(), 'getSize' );
+               } else {
+                       $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
+               }
+
+               if ( !empty( $row->rev_sha1 ) ) {
+                       $this->assertSame( $row->rev_sha1, $rec->getSha1(), 'getSha1' );
+               } else {
+                       $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
+               }
+
+               if ( isset( $row->page_latest ) ) {
+                       $this->assertSame(
+                               (int)$row->rev_id === (int)$row->page_latest,
+                               $rec->isCurrent(),
+                               'isCurrent'
+                       );
+               } else {
+                       $this->assertSame(
+                               false,
+                               $rec->isCurrent(),
+                               'isCurrent'
+                       );
+               }
+       }
+
+       public function provideConstructorFailure() {
+               $title = Title::newFromText( 'Dummy' );
+               $title->resetArticleID( 17 );
+
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
+
+               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+               $slots = new RevisionSlots( [ $main, $aux ] );
+
+               $protoRow = [
+                       'rev_id' => '7',
+                       'rev_page' => strval( $title->getArticleID() ),
+                       'rev_timestamp' => '20200101000000',
+                       'rev_deleted' => 0,
+                       'rev_minor_edit' => 0,
+                       'rev_parent_id' => '5',
+                       'rev_len' => $slots->computeSize(),
+                       'rev_sha1' => $slots->computeSha1(),
+                       'page_latest' => '18',
+               ];
+
+               yield 'not a row' => [
+                       $title,
+                       $user,
+                       $comment,
+                       'not a row',
+                       $slots,
+                       'acmewiki'
+               ];
+
+               $row = $protoRow;
+               $row['rev_timestamp'] = 'kittens';
+
+               yield 'bad timestamp' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+               $row['rev_page'] = 99;
+
+               yield 'page ID mismatch' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots
+               ];
+
+               $row = $protoRow;
+
+               yield 'bad wiki' => [
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$row,
+                       $slots,
+                       12345
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructorFailure
+        *
+        * @param Title $title
+        * @param UserIdentity $user
+        * @param CommentStoreComment $comment
+        * @param object $row
+        * @param RevisionSlots $slots
+        * @param bool $wikiId
+        */
+       public function testConstructorFailure(
+               Title $title,
+               UserIdentity $user,
+               CommentStoreComment $comment,
+               $row,
+               RevisionSlots $slots,
+               $wikiId = false
+       ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
+       }
+
+       public function provideIsCurrent() {
+               yield [
+                       [
+                               'rev_id' => 11,
+                               'page_latest' => 11,
+                       ],
+                       true,
+               ];
+               yield [
+                       [
+                               'rev_id' => 11,
+                               'page_latest' => 10,
+                       ],
+                       false,
+               ];
+       }
+
+       /**
+        * @dataProvider provideIsCurrent
+        */
+       public function testIsCurrent( $row, $current ) {
+               $rev = $this->newRevision( $row );
+
+               $this->assertSame( $current, $rev->isCurrent(), 'isCurrent()' );
+       }
+
+       public function provideGetSlot_audience_latest() {
+               return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
+       }
+
+       /**
+        * @dataProvider provideGetSlot_audience_latest
+        */
+       public function testGetSlot_audience_latest( $visibility, $groups, $userCan, $publicCan ) {
+               $this->forceStandardPermissions();
+
+               $user = $this->getTestUser( $groups )->getUser();
+               $rev = $this->newRevision(
+                       [
+                               'rev_deleted' => $visibility,
+                               'rev_id' => 11,
+                               'page_latest' => 11, // revision is current
+                       ]
+               );
+
+               // NOTE: slot meta-data is never suppressed, just the content is!
+               $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw can' );
+               $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ),
+                       'public can' );
+
+               $this->assertNotNull(
+                       $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ),
+                       'user can'
+               );
+
+               $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getContent();
+               // NOTE: the content of the current revision is never suppressed!
+               // Check that getContent() doesn't throw SuppressedDataException
+               $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC )->getContent();
+               $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )->getContent();
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/RevisionStoreTest.php b/tests/phpunit/includes/Revision/RevisionStoreTest.php
new file mode 100644 (file)
index 0000000..2093b41
--- /dev/null
@@ -0,0 +1,566 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStore;
+use HashBagOStuff;
+use InvalidArgumentException;
+use Language;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWikiTestCase;
+use MWException;
+use Title;
+use WANObjectCache;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\TestingAccessWrapper;
+use WikitextContent;
+
+class RevisionStoreTest extends MediaWikiTestCase {
+
+       private function useTextId() {
+               global $wgMultiContentRevisionSchemaMigrationStage;
+
+               return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD );
+       }
+
+       /**
+        * @param LoadBalancer $loadBalancer
+        * @param SqlBlobStore $blobStore
+        * @param WANObjectCache $WANObjectCache
+        *
+        * @return RevisionStore
+        */
+       private function getRevisionStore(
+               $loadBalancer = null,
+               $blobStore = null,
+               $WANObjectCache = null
+       ) {
+               global $wgMultiContentRevisionSchemaMigrationStage;
+               // the migration stage should be irrelevant, since all the tests that interact with
+               // the database are in RevisionStoreDbTest, not here.
+
+               return new RevisionStore(
+                       $loadBalancer ?: $this->getMockLoadBalancer(),
+                       $blobStore ?: $this->getMockSqlBlobStore(),
+                       $WANObjectCache ?: $this->getHashWANObjectCache(),
+                       MediaWikiServices::getInstance()->getCommentStore(),
+                       MediaWikiServices::getInstance()->getContentModelStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       $wgMultiContentRevisionSchemaMigrationStage,
+                       MediaWikiServices::getInstance()->getActorMigration()
+               );
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
+        */
+       private function getMockLoadBalancer() {
+               return $this->getMockBuilder( LoadBalancer::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|Database
+        */
+       private function getMockDatabase() {
+               return $this->getMockBuilder( Database::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
+        */
+       private function getMockSqlBlobStore() {
+               return $this->getMockBuilder( SqlBlobStore::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       /**
+        * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
+        */
+       private function getMockCommentStore() {
+               return $this->getMockBuilder( CommentStore::class )
+                       ->disableOriginalConstructor()->getMock();
+       }
+
+       private function getHashWANObjectCache() {
+               return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
+       }
+
+       public function provideSetContentHandlerUseDB() {
+               return [
+                       // ContentHandlerUseDB can be true of false pre migration.
+                       [ false, SCHEMA_COMPAT_OLD, false ],
+                       [ true, SCHEMA_COMPAT_OLD, false ],
+                       // During and after migration it can not be false...
+                       [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, true ],
+                       [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, true ],
+                       [ false, SCHEMA_COMPAT_NEW, true ],
+                       // ...but it can be true.
+                       [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
+                       [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
+                       [ true, SCHEMA_COMPAT_NEW, false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideSetContentHandlerUseDB
+        * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
+        * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
+        */
+       public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
+               if ( $expectedFail ) {
+                       $this->setExpectedException( MWException::class );
+               }
+
+               $nameTables = MediaWikiServices::getInstance()->getNameTableStoreFactory();
+
+               $store = new RevisionStore(
+                       $this->getMockLoadBalancer(),
+                       $this->getMockSqlBlobStore(),
+                       $this->getHashWANObjectCache(),
+                       $this->getMockCommentStore(),
+                       $nameTables->getContentModels(),
+                       $nameTables->getSlotRoles(),
+                       $migrationMode,
+                       MediaWikiServices::getInstance()->getActorMigration()
+               );
+
+               $store->setContentHandlerUseDB( $contentHandlerDb );
+               $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
+       }
+
+       public function testGetTitle_successFromPageId() {
+               $mockLoadBalancer = $this->getMockLoadBalancer();
+               // Title calls wfGetDB() so we have to set the main service
+               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+               $db = $this->getMockDatabase();
+               // Title calls wfGetDB() which uses a regular Connection
+               $mockLoadBalancer->expects( $this->atLeastOnce() )
+                       ->method( 'getConnection' )
+                       ->willReturn( $db );
+
+               // First call to Title::newFromID, faking no result (db lag?)
+               $db->expects( $this->at( 0 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'page',
+                               $this->anything(),
+                               [ 'page_id' => 1 ]
+                       )
+                       ->willReturn( (object)[
+                               'page_namespace' => '1',
+                               'page_title' => 'Food',
+                       ] );
+
+               $store = $this->getRevisionStore( $mockLoadBalancer );
+               $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+
+               $this->assertSame( 1, $title->getNamespace() );
+               $this->assertSame( 'Food', $title->getDBkey() );
+       }
+
+       public function testGetTitle_successFromPageIdOnFallback() {
+               $mockLoadBalancer = $this->getMockLoadBalancer();
+               // Title calls wfGetDB() so we have to set the main service
+               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+               $db = $this->getMockDatabase();
+               // Title calls wfGetDB() which uses a regular Connection
+               // Assert that the first call uses a REPLICA and the second falls back to master
+               $mockLoadBalancer->expects( $this->exactly( 2 ) )
+                       ->method( 'getConnection' )
+                       ->willReturn( $db );
+               // RevisionStore getTitle uses a ConnectionRef
+               $mockLoadBalancer->expects( $this->atLeastOnce() )
+                       ->method( 'getConnectionRef' )
+                       ->willReturn( $db );
+
+               // First call to Title::newFromID, faking no result (db lag?)
+               $db->expects( $this->at( 0 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'page',
+                               $this->anything(),
+                               [ 'page_id' => 1 ]
+                       )
+                       ->willReturn( false );
+
+               // First select using rev_id, faking no result (db lag?)
+               $db->expects( $this->at( 1 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               [ 'revision', 'page' ],
+                               $this->anything(),
+                               [ 'rev_id' => 2 ]
+                       )
+                       ->willReturn( false );
+
+               // Second call to Title::newFromID, no result
+               $db->expects( $this->at( 2 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'page',
+                               $this->anything(),
+                               [ 'page_id' => 1 ]
+                       )
+                       ->willReturn( (object)[
+                               'page_namespace' => '2',
+                               'page_title' => 'Foodey',
+                       ] );
+
+               $store = $this->getRevisionStore( $mockLoadBalancer );
+               $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+
+               $this->assertSame( 2, $title->getNamespace() );
+               $this->assertSame( 'Foodey', $title->getDBkey() );
+       }
+
+       public function testGetTitle_successFromRevId() {
+               $mockLoadBalancer = $this->getMockLoadBalancer();
+               // Title calls wfGetDB() so we have to set the main service
+               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+               $db = $this->getMockDatabase();
+               // Title calls wfGetDB() which uses a regular Connection
+               $mockLoadBalancer->expects( $this->atLeastOnce() )
+                       ->method( 'getConnection' )
+                       ->willReturn( $db );
+               // RevisionStore getTitle uses a ConnectionRef
+               $mockLoadBalancer->expects( $this->atLeastOnce() )
+                       ->method( 'getConnectionRef' )
+                       ->willReturn( $db );
+
+               // First call to Title::newFromID, faking no result (db lag?)
+               $db->expects( $this->at( 0 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'page',
+                               $this->anything(),
+                               [ 'page_id' => 1 ]
+                       )
+                       ->willReturn( false );
+
+               // First select using rev_id, faking no result (db lag?)
+               $db->expects( $this->at( 1 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               [ 'revision', 'page' ],
+                               $this->anything(),
+                               [ 'rev_id' => 2 ]
+                       )
+                       ->willReturn( (object)[
+                               'page_namespace' => '1',
+                               'page_title' => 'Food2',
+                       ] );
+
+               $store = $this->getRevisionStore( $mockLoadBalancer );
+               $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+
+               $this->assertSame( 1, $title->getNamespace() );
+               $this->assertSame( 'Food2', $title->getDBkey() );
+       }
+
+       public function testGetTitle_successFromRevIdOnFallback() {
+               $mockLoadBalancer = $this->getMockLoadBalancer();
+               // Title calls wfGetDB() so we have to set the main service
+               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+               $db = $this->getMockDatabase();
+               // Title calls wfGetDB() which uses a regular Connection
+               // Assert that the first call uses a REPLICA and the second falls back to master
+               $mockLoadBalancer->expects( $this->exactly( 2 ) )
+                       ->method( 'getConnection' )
+                       ->willReturn( $db );
+               // RevisionStore getTitle uses a ConnectionRef
+               $mockLoadBalancer->expects( $this->atLeastOnce() )
+                       ->method( 'getConnectionRef' )
+                       ->willReturn( $db );
+
+               // First call to Title::newFromID, faking no result (db lag?)
+               $db->expects( $this->at( 0 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'page',
+                               $this->anything(),
+                               [ 'page_id' => 1 ]
+                       )
+                       ->willReturn( false );
+
+               // First select using rev_id, faking no result (db lag?)
+               $db->expects( $this->at( 1 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               [ 'revision', 'page' ],
+                               $this->anything(),
+                               [ 'rev_id' => 2 ]
+                       )
+                       ->willReturn( false );
+
+               // Second call to Title::newFromID, no result
+               $db->expects( $this->at( 2 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'page',
+                               $this->anything(),
+                               [ 'page_id' => 1 ]
+                       )
+                       ->willReturn( false );
+
+               // Second select using rev_id, result
+               $db->expects( $this->at( 3 ) )
+                       ->method( 'selectRow' )
+                       ->with(
+                               [ 'revision', 'page' ],
+                               $this->anything(),
+                               [ 'rev_id' => 2 ]
+                       )
+                       ->willReturn( (object)[
+                               'page_namespace' => '2',
+                               'page_title' => 'Foodey',
+                       ] );
+
+               $store = $this->getRevisionStore( $mockLoadBalancer );
+               $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+
+               $this->assertSame( 2, $title->getNamespace() );
+               $this->assertSame( 'Foodey', $title->getDBkey() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getTitle
+        */
+       public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
+               $mockLoadBalancer = $this->getMockLoadBalancer();
+               // Title calls wfGetDB() so we have to set the main service
+               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+               $db = $this->getMockDatabase();
+               // Title calls wfGetDB() which uses a regular Connection
+               // Assert that the first call uses a REPLICA and the second falls back to master
+
+               // RevisionStore getTitle uses getConnectionRef
+               // Title::newFromID uses getConnection
+               foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
+                       $mockLoadBalancer->expects( $this->exactly( 2 ) )
+                               ->method( $method )
+                               ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
+                                       static $callCounter = 0;
+                                       $callCounter++;
+                                       // The first call should be to a REPLICA, and the second a MASTER.
+                                       if ( $callCounter === 1 ) {
+                                               $this->assertSame( DB_REPLICA, $masterOrReplica );
+                                       } elseif ( $callCounter === 2 ) {
+                                               $this->assertSame( DB_MASTER, $masterOrReplica );
+                                       }
+                                       return $db;
+                               } );
+               }
+               // First and third call to Title::newFromID, faking no result
+               foreach ( [ 0, 2 ] as $counter ) {
+                       $db->expects( $this->at( $counter ) )
+                               ->method( 'selectRow' )
+                               ->with(
+                                       'page',
+                                       $this->anything(),
+                                       [ 'page_id' => 1 ]
+                               )
+                               ->willReturn( false );
+               }
+
+               foreach ( [ 1, 3 ] as $counter ) {
+                       $db->expects( $this->at( $counter ) )
+                               ->method( 'selectRow' )
+                               ->with(
+                                       [ 'revision', 'page' ],
+                                       $this->anything(),
+                                       [ 'rev_id' => 2 ]
+                               )
+                               ->willReturn( false );
+               }
+
+               $store = $this->getRevisionStore( $mockLoadBalancer );
+
+               $this->setExpectedException( RevisionAccessException::class );
+               $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+       }
+
+       public function provideNewRevisionFromRow_legacyEncoding_applied() {
+               yield 'windows-1252, old_flags is empty' => [
+                       'windows-1252',
+                       'en',
+                       [
+                               'old_flags' => '',
+                               'old_text' => "S\xF6me Content",
+                       ],
+                       'Söme Content'
+               ];
+
+               yield 'windows-1252, old_flags is null' => [
+                       'windows-1252',
+                       'en',
+                       [
+                               'old_flags' => null,
+                               'old_text' => "S\xF6me Content",
+                       ],
+                       'Söme Content'
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
+        *
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+        */
+       public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
+               if ( !$this->useTextId() ) {
+                       $this->markTestSkipped( 'No longer applicable with MCR schema' );
+               }
+
+               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+
+               $blobStore = new SqlBlobStore( $lb, $cache );
+               $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
+
+               $store = $this->getRevisionStore( $lb, $blobStore, $cache );
+
+               $record = $store->newRevisionFromRow(
+                       $this->makeRow( $row ),
+                       0,
+                       Title::newFromText( __METHOD__ . '-UTPage' )
+               );
+
+               $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+        */
+       public function testNewRevisionFromRow_legacyEncoding_ignored() {
+               if ( !$this->useTextId() ) {
+                       $this->markTestSkipped( 'No longer applicable with MCR schema' );
+               }
+
+               $row = [
+                       'old_flags' => 'utf-8',
+                       'old_text' => 'Söme Content',
+               ];
+
+               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+
+               $blobStore = new SqlBlobStore( $lb, $cache );
+               $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
+
+               $store = $this->getRevisionStore( $lb, $blobStore, $cache );
+
+               $record = $store->newRevisionFromRow(
+                       $this->makeRow( $row ),
+                       0,
+                       Title::newFromText( __METHOD__ . '-UTPage' )
+               );
+               $this->assertSame( 'Söme Content', $record->getContent( SlotRecord::MAIN )->serialize() );
+       }
+
+       private function makeRow( array $array ) {
+               $row = $array + [
+                               'rev_id' => 7,
+                               'rev_page' => 5,
+                               'rev_timestamp' => '20110101000000',
+                               'rev_user_text' => 'Tester',
+                               'rev_user' => 17,
+                               'rev_minor_edit' => 0,
+                               'rev_deleted' => 0,
+                               'rev_len' => 100,
+                               'rev_parent_id' => 0,
+                               'rev_sha1' => 'deadbeef',
+                               'rev_comment_text' => 'Testing',
+                               'rev_comment_data' => '{}',
+                               'rev_comment_cid' => 111,
+                               'page_namespace' => 0,
+                               'page_title' => 'TEST',
+                               'page_id' => 5,
+                               'page_latest' => 7,
+                               'page_is_redirect' => 0,
+                               'page_len' => 100,
+                               'user_name' => 'Tester',
+                       ];
+
+               if ( $this->useTextId() ) {
+                       $row += [
+                               'rev_content_format' => CONTENT_FORMAT_TEXT,
+                               'rev_content_model' => CONTENT_MODEL_TEXT,
+                               'rev_text_id' => 11,
+                               'old_id' => 11,
+                               'old_text' => 'Hello World',
+                               'old_flags' => 'utf-8',
+                       ];
+               } else {
+                       if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
+                               $row['content'] = [
+                                       'main' => new WikitextContent( $array['old_text'] ),
+                               ];
+                       }
+               }
+
+               return (object)$row;
+       }
+
+       public function provideMigrationConstruction() {
+               return [
+                       [ SCHEMA_COMPAT_OLD, false ],
+                       [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
+                       [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
+                       [ SCHEMA_COMPAT_NEW, false ],
+                       [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH, true ],
+                       [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, true ],
+                       [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH, true ],
+               ];
+       }
+
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::__construct
+        * @dataProvider provideMigrationConstruction
+        */
+       public function testMigrationConstruction( $migration, $expectException ) {
+               if ( $expectException ) {
+                       $this->setExpectedException( InvalidArgumentException::class );
+               }
+               $loadBalancer = $this->getMockLoadBalancer();
+               $blobStore = $this->getMockSqlBlobStore();
+               $cache = $this->getHashWANObjectCache();
+               $commentStore = $this->getMockCommentStore();
+               $services = MediaWikiServices::getInstance();
+               $nameTables = $services->getNameTableStoreFactory();
+               $contentModelStore = $nameTables->getContentModels();
+               $slotRoleStore = $nameTables->getSlotRoles();
+               $store = new RevisionStore(
+                       $loadBalancer,
+                       $blobStore,
+                       $cache,
+                       $commentStore,
+                       $nameTables->getContentModels(),
+                       $nameTables->getSlotRoles(),
+                       $migration,
+                       $services->getActorMigration()
+               );
+               if ( !$expectException ) {
+                       $store = TestingAccessWrapper::newFromObject( $store );
+                       $this->assertSame( $loadBalancer, $store->loadBalancer );
+                       $this->assertSame( $blobStore, $store->blobStore );
+                       $this->assertSame( $cache, $store->cache );
+                       $this->assertSame( $commentStore, $store->commentStore );
+                       $this->assertSame( $contentModelStore, $store->contentModelStore );
+                       $this->assertSame( $slotRoleStore, $store->slotRoleStore );
+                       $this->assertSame( $migration, $store->mcrMigrationStage );
+               }
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/SlotRecordTest.php b/tests/phpunit/includes/Revision/SlotRecordTest.php
new file mode 100644 (file)
index 0000000..ea26808
--- /dev/null
@@ -0,0 +1,408 @@
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
+use MediaWikiTestCase;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRecord
+ */
+class SlotRecordTest extends MediaWikiTestCase {
+
+       private function makeRow( $data = [] ) {
+               $data = $data + [
+                       'slot_id' => 1234,
+                       'slot_content_id' => 33,
+                       'content_size' => '5',
+                       'content_sha1' => 'someHash',
+                       'content_address' => 'tt:456',
+                       'model_name' => CONTENT_MODEL_WIKITEXT,
+                       'format_name' => CONTENT_FORMAT_WIKITEXT,
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '1',
+                       'role_name' => 'myRole',
+               ];
+               return (object)$data;
+       }
+
+       public function testCompleteConstruction() {
+               $row = $this->makeRow();
+               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasContentId() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertTrue( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getNativeData() );
+               $this->assertSame( 5, $record->getSize() );
+               $this->assertSame( 'someHash', $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 1, $record->getOrigin() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( 33, $record->getContentId() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testConstructionDeferred() {
+               $row = $this->makeRow( [
+                       'content_size' => null, // to be computed
+                       'content_sha1' => null, // to be computed
+                       'format_name' => function () {
+                               return CONTENT_FORMAT_WIKITEXT;
+                       },
+                       'slot_revision_id' => '2',
+                       'slot_origin' => '2',
+                       'slot_content_id' => function () {
+                               return null;
+                       },
+               ] );
+
+               $content = function () {
+                       return new WikitextContent( 'A' );
+               };
+
+               $record = new SlotRecord( $row, $content );
+
+               $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasRevision() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertSame( 'A', $record->getContent()->getNativeData() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotNull( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 2, $record->getRevision() );
+               $this->assertSame( 'tt:456', $record->getAddress() );
+               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function testNewUnsaved() {
+               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
+
+               $this->assertFalse( $record->hasAddress() );
+               $this->assertFalse( $record->hasContentId() );
+               $this->assertFalse( $record->hasRevision() );
+               $this->assertFalse( $record->isInherited() );
+               $this->assertFalse( $record->hasOrigin() );
+               $this->assertSame( 'A', $record->getContent()->getNativeData() );
+               $this->assertSame( 1, $record->getSize() );
+               $this->assertNotNull( $record->getSha1() );
+               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+               $this->assertSame( 'myRole', $record->getRole() );
+       }
+
+       public function provideInvalidConstruction() {
+               yield 'both null' => [ null, null ];
+               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
+               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
+               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
+               yield 'null content' => [ (object)[], null ];
+       }
+
+       /**
+        * @dataProvider provideInvalidConstruction
+        */
+       public function testInvalidConstruction( $row, $content ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               new SlotRecord( $row, $content );
+       }
+
+       public function testGetContentId_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getContentId();
+       }
+
+       public function testGetAddress_fails() {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getAddress();
+       }
+
+       public function provideIncomplete() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               yield 'unsaved' => [ $unsaved ];
+
+               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $inherited = SlotRecord::newInherited( $parent );
+               yield 'inherited' => [ $inherited ];
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetRevision_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getRevision();
+       }
+
+       /**
+        * @dataProvider provideIncomplete
+        */
+       public function testGetOrigin_fails( SlotRecord $record ) {
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+               $this->setExpectedException( IncompleteRevisionException::class );
+
+               $record->getOrigin();
+       }
+
+       public function provideHashStability() {
+               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
+               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
+       }
+
+       /**
+        * @dataProvider provideHashStability
+        */
+       public function testHashStability( $text, $hash ) {
+               // Changing the output of the hash function will break things horribly!
+
+               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
+
+               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
+               $this->assertSame( $hash, $record->getSha1() );
+       }
+
+       public function testNewWithSuppressedContent() {
+               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+               $output = SlotRecord::newWithSuppressedContent( $input );
+
+               $this->setExpectedException( SuppressedDataException::class );
+               $output->getContent();
+       }
+
+       public function testNewInherited() {
+               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
+               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, before saving revision meta-data.
+               $inherited = SlotRecord::newInherited( $parent );
+
+               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
+               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
+               $this->assertSame( $parent->getContent(), $inherited->getContent() );
+               $this->assertTrue( $inherited->isInherited() );
+               $this->assertTrue( $inherited->hasOrigin() );
+               $this->assertFalse( $inherited->hasRevision() );
+
+               // make sure we didn't mess with the internal state of $parent
+               $this->assertFalse( $parent->isInherited() );
+               $this->assertSame( 7, $parent->getRevision() );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved(
+                       10,
+                       $inherited->getContentId(),
+                       $inherited->getAddress(),
+                       $inherited
+               );
+               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
+               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
+               $this->assertSame( $parent->getContent(), $saved->getContent() );
+               $this->assertTrue( $saved->isInherited() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertSame( 10, $saved->getRevision() );
+
+               // make sure we didn't mess with the internal state of $parent or $inherited
+               $this->assertSame( 7, $parent->getRevision() );
+               $this->assertFalse( $inherited->hasRevision() );
+       }
+
+       public function testNewSaved() {
+               // This would happen while doing an edit, before saving revision meta-data.
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               // This would happen while doing an edit, after saving the revision meta-data
+               // and content meta-data.
+               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
+               $this->assertFalse( $saved->isInherited() );
+               $this->assertTrue( $saved->hasOrigin() );
+               $this->assertTrue( $saved->hasRevision() );
+               $this->assertTrue( $saved->hasAddress() );
+               $this->assertTrue( $saved->hasContentId() );
+               $this->assertSame( 'theNewAddress', $saved->getAddress() );
+               $this->assertSame( 20, $saved->getContentId() );
+               $this->assertSame( 'A', $saved->getContent()->getNativeData() );
+               $this->assertSame( 10, $saved->getRevision() );
+               $this->assertSame( 10, $saved->getOrigin() );
+
+               // make sure we didn't mess with the internal state of $unsaved
+               $this->assertFalse( $unsaved->hasAddress() );
+               $this->assertFalse( $unsaved->hasContentId() );
+               $this->assertFalse( $unsaved->hasRevision() );
+       }
+
+       public function provideNewSaved_LogicException() {
+               $freshRow = $this->makeRow( [
+                       'content_id' => 10,
+                       'content_address' => 'address:1',
+                       'slot_origin' => 1,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
+               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
+               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
+               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
+
+               $inheritedRow = $this->makeRow( [
+                       'content_id' => null,
+                       'content_address' => null,
+                       'slot_origin' => 0,
+                       'slot_revision_id' => 1,
+               ] );
+
+               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
+               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_LogicException
+        */
+       public function testNewSaved_LogicException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( LogicException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideNewSaved_InvalidArgumentException() {
+               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
+               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
+               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
+       }
+
+       /**
+        * @dataProvider provideNewSaved_InvalidArgumentException
+        */
+       public function testNewSaved_InvalidArgumentException(
+               $revisionId,
+               $contentId,
+               $contentAddress,
+               SlotRecord $protoSlot
+       ) {
+               $this->setExpectedException( InvalidArgumentException::class );
+               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+       }
+
+       public function provideHasSameContent() {
+               $fail = function () {
+                       self::fail( 'There should be no need to actually load the content.' );
+               };
+
+               $a100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a1b = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100null = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => null,
+                               ]
+                       ),
+                       $fail
+               );
+               $a100a2 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $b100a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'B',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a1',
+                               ]
+                       ),
+                       $fail
+               );
+               $a200a1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 200,
+                                       'content_sha1' => 'hash-a',
+                                       'content_address' => 'xxx:a2',
+                               ]
+                       ),
+                       $fail
+               );
+               $a100x1 = new SlotRecord(
+                       $this->makeRow(
+                               [
+                                       'model_name' => 'A',
+                                       'content_size' => 100,
+                                       'content_sha1' => 'hash-x',
+                                       'content_address' => 'xxx:x1',
+                               ]
+                       ),
+                       $fail
+               );
+
+               yield 'same instance' => [ $a100a1, $a100a1, true ];
+               yield 'no address' => [ $a100a1, $a100null, true ];
+               yield 'same address' => [ $a100a1, $a100a1b, true ];
+               yield 'different address' => [ $a100a1, $a100a2, true ];
+               yield 'different model' => [ $a100a1, $b100a1, false ];
+               yield 'different size' => [ $a100a1, $a200a1, false ];
+               yield 'different hash' => [ $a100a1, $a100x1, false ];
+       }
+
+       /**
+        * @dataProvider provideHasSameContent
+        */
+       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
+               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
+               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/Revision/create-pre-mcr-fields.sql b/tests/phpunit/includes/Revision/create-pre-mcr-fields.sql
new file mode 100644 (file)
index 0000000..09deb4f
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TABLE /*_*/revision ADD rev_text_id INTEGER DEFAULT 0;
+ALTER TABLE /*_*/revision  ADD rev_content_model VARBINARY(32) DEFAULT NULL;
+ALTER TABLE /*_*/revision ADD rev_content_format VARBINARY(64) DEFAULT NULL;
diff --git a/tests/phpunit/includes/Revision/drop-mcr-tables.sql b/tests/phpunit/includes/Revision/drop-mcr-tables.sql
new file mode 100644 (file)
index 0000000..bc89edc
--- /dev/null
@@ -0,0 +1,4 @@
+DROP TABLE /*_*/slots;
+DROP TABLE /*_*/content;
+DROP TABLE /*_*/content_models;
+DROP TABLE /*_*/slot_roles;
diff --git a/tests/phpunit/includes/Revision/drop-pre-mcr-fields.sql b/tests/phpunit/includes/Revision/drop-pre-mcr-fields.sql
new file mode 100644 (file)
index 0000000..ddfe756
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TABLE /*_*/revision DROP COLUMN rev_text_id;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_model;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_format;
diff --git a/tests/phpunit/includes/Revision/drop-pre-mcr-fields.sqlite.sql b/tests/phpunit/includes/Revision/drop-pre-mcr-fields.sqlite.sql
new file mode 100644 (file)
index 0000000..ce7a618
--- /dev/null
@@ -0,0 +1,15 @@
+DROP TABLE /*_*/revision;
+
+CREATE TABLE /*_*/revision (
+  rev_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  rev_page INTEGER NOT NULL,
+  rev_comment BLOB NOT NULL,
+  rev_user INTEGER NOT NULL default 0,
+  rev_user_text varchar(255) NOT NULL default '',
+  rev_timestamp blob(14) NOT NULL default '',
+  rev_minor_edit INTEGER NOT NULL default 0,
+  rev_deleted INTEGER NOT NULL default 0,
+  rev_len INTEGER unsigned,
+  rev_parent_id INTEGER default NULL,
+  rev_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
index 28e6e12..cc166a3 100644 (file)
@@ -1,10 +1,10 @@
 <?php
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\IncompleteRevisionException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 
 /**
  * RevisionDbTestBase contains test cases for the Revision class that have Database interactions.
@@ -91,7 +91,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
                        'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
                        'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                       'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+                       'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                ] );
 
                $this->overrideMwServices();
index f5bc4fa..d6ac35b 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Tests\Storage\McrSchemaOverride;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Tests\Revision\McrSchemaOverride;
 
 /**
  * Tests Revision against the MCR DB schema after schema migration.
index 7218466..df54f56 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Tests\Storage\McrReadNewSchemaOverride;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Tests\Revision\McrReadNewSchemaOverride;
 
 /**
  * Tests Revision against the intermediate MCR DB schema for use during schema migration.
index 826b83d..bb62b2f 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-use MediaWiki\Tests\Storage\McrWriteBothSchemaOverride;
+use MediaWiki\Tests\Revision\McrWriteBothSchemaOverride;
 
 /**
  * Tests Revision against the intermediate MCR DB schema for use during schema migration.
index f07d169..19eeab3 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
 
 /**
  * Tests Revision against the pre-MCR, pre ContentHandler DB schema.
index 9a62881..e520f2d 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
 
 /**
  * Tests Revision against the pre-MCR DB schema.
index c470787..5868b8d 100644 (file)
@@ -1,12 +1,12 @@
 <?php
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
 use MediaWiki\Storage\SqlBlobStore;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
@@ -71,7 +71,7 @@ class RevisionTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideConstructFromArray
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
         */
        public function testConstructFromArray( $rowArray ) {
                $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
@@ -82,7 +82,7 @@ class RevisionTest extends MediaWikiTestCase {
 
        /**
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
         */
        public function testConstructFromEmptyArray() {
                $rev = new Revision( [], 0, $this->getMockTitle() );
@@ -91,7 +91,7 @@ class RevisionTest extends MediaWikiTestCase {
 
        /**
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
         */
        public function testConstructFromArrayWithBadPageId() {
                Wikimedia\suppressWarnings();
@@ -131,7 +131,7 @@ class RevisionTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideConstructFromArray_userSetAsExpected
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
         *
         * @param array $rowArray
         * @param mixed $expectedUserId null to expect the current wgUser ID
@@ -184,7 +184,7 @@ class RevisionTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideConstructFromArrayThrowsExceptions
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
         */
        public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) {
                $this->setExpectedException(
@@ -197,7 +197,7 @@ class RevisionTest extends MediaWikiTestCase {
 
        /**
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
         */
        public function testConstructFromNothing() {
                $this->setExpectedException(
@@ -268,7 +268,7 @@ class RevisionTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideConstructFromRow
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
         */
        public function testConstructFromRow( array $arrayData, callable $assertions ) {
                $row = (object)$arrayData;
@@ -278,7 +278,7 @@ class RevisionTest extends MediaWikiTestCase {
 
        /**
         * @covers Revision::__construct
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+        * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
         */
        public function testConstructFromRowWithBadPageId() {
                $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
@@ -594,7 +594,7 @@ class RevisionTest extends MediaWikiTestCase {
         */
        public function testLoadFromTitle() {
                $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD );
                $this->overrideMwServices();
                $title = $this->getMockTitle();
 
@@ -622,10 +622,11 @@ class RevisionTest extends MediaWikiTestCase {
                        'rev_content_model' => 'GOATMODEL',
                ];
 
+               $domain = MediaWikiServices::getInstance()->getDBLoadBalancer()->getLocalDomainID();
                $db = $this->getMock( IDatabase::class );
                $db->expects( $this->any() )
                        ->method( 'getDomainId' )
-                       ->will( $this->returnValue( wfWikiID() ) );
+                       ->will( $this->returnValue( $domain ) );
                $db->expects( $this->once() )
                        ->method( 'selectRow' )
                        ->with(
index 18f039d..c175e2f 100644 (file)
@@ -7,12 +7,12 @@ use Content;
 use ContentHandler;
 use LinksUpdate;
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\DerivedPageDataUpdater;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionRecord;
 use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\SlotRecord;
 use MediaWikiTestCase;
 use MWCallableUpdate;
 use MWTimestamp;
diff --git a/tests/phpunit/includes/Storage/McrReadNewRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/McrReadNewRevisionStoreDbTest.php
deleted file mode 100644 (file)
index 3b3b344..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use TextContent;
-use Title;
-use WikitextContent;
-
-/**
- * Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class McrReadNewRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
-       use McrReadNewSchemaOverride;
-
-       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
-               $numberOfSlots = count( $rev->getSlotRoles() );
-
-               // new schema is written
-               $this->assertSelect(
-                       'slots',
-                       [ 'count(*)' ],
-                       [ 'slot_revision_id' => $rev->getId() ],
-                       [ [ (string)$numberOfSlots ] ]
-               );
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
-
-               $this->assertSelect(
-                       $revQuery['tables'],
-                       [ 'count(*)' ],
-                       [
-                               'slot_revision_id' => $rev->getId(),
-                       ],
-                       [ [ (string)$numberOfSlots ] ],
-                       [],
-                       $revQuery['joins']
-               );
-
-               // Legacy schema is still being written
-               $this->assertSelect(
-                       [ 'revision', 'text' ],
-                       [ 'count(*)' ],
-                       [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
-                       [ [ 1 ] ],
-                       [],
-                       [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
-               );
-
-               parent::assertRevisionExistsInDatabase( $rev );
-       }
-
-       /**
-        * @param SlotRecord $a
-        * @param SlotRecord $b
-        */
-       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
-               parent::assertSameSlotContent( $a, $b );
-
-               // Assert that the same content ID has been used
-               $this->assertSame( $a->getContentId(), $b->getContentId() );
-       }
-
-       public function provideInsertRevisionOn_successes() {
-               foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
-                       yield $case;
-               }
-
-               yield 'Multi-slot revision insertion' => [
-                       [
-                               'content' => [
-                                       'main' => new WikitextContent( 'Chicken' ),
-                                       'aux' => new TextContent( 'Egg' ),
-                               ],
-                               'page' => true,
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'timestamp' => '20171117010101',
-                               'user' => true,
-                       ],
-               ];
-       }
-
-       public function provideNewNullRevision() {
-               foreach ( parent::provideNewNullRevision() as $case ) {
-                       yield $case;
-               }
-
-               yield [
-                       Title::newFromText( 'UTPage_notAutoCreated' ),
-                       [
-                               'content' => [
-                                       'main' => new WikitextContent( 'Chicken' ),
-                                       'aux' => new WikitextContent( 'Omelet' ),
-                               ],
-                       ],
-                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
-               ];
-       }
-
-       public function testGetQueryInfo_NoSlotDataJoin() {
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $queryInfo = $store->getQueryInfo();
-
-               // with the new schema enabled, query info should not join the main slot info
-               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
-               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
-       }
-
-       public function provideNewMutableRevisionFromArray() {
-               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
-                       yield $case;
-               }
-
-               yield 'Basic array, multiple roles' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 29,
-                               'parent_id' => 1,
-                               'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
-                               'comment' => 'Goat Comment!',
-                               'content' => [
-                                       'main' => new WikitextContent( 'Söme Cöntent' ),
-                                       'aux' => new TextContent( 'Öther Cöntent' ),
-                               ]
-                       ]
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/McrReadNewSchemaOverride.php b/tests/phpunit/includes/Storage/McrReadNewSchemaOverride.php
deleted file mode 100644 (file)
index 76bd59a..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IMaintainableDatabase;
-use MediaWiki\DB\PatchFileLocation;
-
-/**
- * Trait providing schema overrides that allow tests to run against the intermediate MCR database
- * schema for use during schema migration.
- */
-trait McrReadNewSchemaOverride {
-
-       use PatchFileLocation;
-       use McrSchemaDetection;
-
-       /**
-        * @return int
-        */
-       protected function getMcrMigrationStage() {
-               return SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW;
-       }
-
-       /**
-        * @return string[]
-        */
-       protected function getMcrTablesToReset() {
-               return [ 'content', 'content_models', 'slots', 'slot_roles' ];
-       }
-
-       /**
-        * @return array[]
-        */
-       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
-               $overrides = [
-                       'scripts' => [],
-                       'drop' => [],
-                       'create' => [],
-                       'alter' => [],
-               ];
-
-               if ( !$this->hasMcrTables( $db ) ) {
-                       $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' );
-               }
-
-               if ( !$this->hasPreMcrFields( $db ) ) {
-                       $overrides['alter'][] = 'revision';
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'create-pre-mcr-fields', __DIR__ );
-               }
-
-               return $overrides;
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php
deleted file mode 100644 (file)
index f4fcfb4..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use TextContent;
-use Title;
-use WikitextContent;
-
-/**
- * Tests RevisionStore against the post-migration MCR DB schema.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
-       use McrSchemaOverride;
-
-       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
-               $numberOfSlots = count( $rev->getSlotRoles() );
-
-               // new schema is written
-               $this->assertSelect(
-                       'slots',
-                       [ 'count(*)' ],
-                       [ 'slot_revision_id' => $rev->getId() ],
-                       [ [ (string)$numberOfSlots ] ]
-               );
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
-
-               $this->assertSelect(
-                       $revQuery['tables'],
-                       [ 'count(*)' ],
-                       [
-                               'slot_revision_id' => $rev->getId(),
-                       ],
-                       [ [ (string)$numberOfSlots ] ],
-                       [],
-                       $revQuery['joins']
-               );
-
-               parent::assertRevisionExistsInDatabase( $rev );
-       }
-
-       /**
-        * @param SlotRecord $a
-        * @param SlotRecord $b
-        */
-       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
-               parent::assertSameSlotContent( $a, $b );
-
-               // Assert that the same content ID has been used
-               $this->assertSame( $a->getContentId(), $b->getContentId() );
-       }
-
-       public function provideInsertRevisionOn_successes() {
-               foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
-                       yield $case;
-               }
-
-               yield 'Multi-slot revision insertion' => [
-                       [
-                               'content' => [
-                                       'main' => new WikitextContent( 'Chicken' ),
-                                       'aux' => new TextContent( 'Egg' ),
-                               ],
-                               'page' => true,
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'timestamp' => '20171117010101',
-                               'user' => true,
-                       ],
-               ];
-       }
-
-       public function provideNewNullRevision() {
-               foreach ( parent::provideNewNullRevision() as $case ) {
-                       yield $case;
-               }
-
-               yield [
-                       Title::newFromText( 'UTPage_notAutoCreated' ),
-                       [
-                               'content' => [
-                                       'main' => new WikitextContent( 'Chicken' ),
-                                       'aux' => new WikitextContent( 'Omelet' ),
-                               ],
-                       ],
-                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
-               ];
-       }
-
-       public function provideNewMutableRevisionFromArray() {
-               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
-                       yield $case;
-               }
-
-               yield 'Basic array, multiple roles' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 29,
-                               'parent_id' => 1,
-                               'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
-                               'comment' => 'Goat Comment!',
-                               'content' => [
-                                       'main' => new WikitextContent( 'Söme Cöntent' ),
-                                       'aux' => new TextContent( 'Öther Cöntent' ),
-                               ]
-                       ]
-               ];
-       }
-
-       public function testGetQueryInfo_NoSlotDataJoin() {
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $queryInfo = $store->getQueryInfo();
-
-               // with the new schema enabled, query info should not join the main slot info
-               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
-               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
-        * @covers \MediaWiki\Storage\RevisionStore::insertSlotRowOn
-        * @covers \MediaWiki\Storage\RevisionStore::insertContentRowOn
-        */
-       public function testInsertRevisionOn_T202032() {
-               // This test only makes sense for MySQL
-               if ( $this->db->getType() !== 'mysql' ) {
-                       $this->assertTrue( true );
-                       return;
-               }
-
-               // NOTE: must be done before checking MAX(rev_id)
-               $page = $this->getTestPage();
-
-               $maxRevId = $this->db->selectField( 'revision', 'MAX(rev_id)' );
-
-               // Construct a slot row that will conflict with the insertion of the next revision ID,
-               // to emulate the failure mode described in T202032. Nothing will ever read this row,
-               // we just need it to trigger a primary key conflict.
-               $this->db->insert( 'slots', [
-                       'slot_revision_id' => $maxRevId + 1,
-                       'slot_role_id' => 1,
-                       'slot_content_id' => 0,
-                       'slot_origin' => 0
-               ], __METHOD__ );
-
-               $rev = new MutableRevisionRecord( $page->getTitle() );
-               $rev->setTimestamp( '20180101000000' );
-               $rev->setComment( CommentStoreComment::newUnsavedComment( 'test' ) );
-               $rev->setUser( $this->getTestUser()->getUser() );
-               $rev->setContent( 'main', new WikitextContent( 'Text' ) );
-               $rev->setPageId( $page->getId() );
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $return = $store->insertRevisionOn( $rev, $this->db );
-
-               $this->assertSame( $maxRevId + 2, $return->getId() );
-
-               // is the new revision correct?
-               $this->assertRevisionCompleteness( $return );
-               $this->assertRevisionRecordsEqual( $rev, $return );
-
-               // can we find it directly in the database?
-               $this->assertRevisionExistsInDatabase( $return );
-
-               // can we load it from the store?
-               $loaded = $store->getRevisionById( $return->getId() );
-               $this->assertRevisionCompleteness( $loaded );
-               $this->assertRevisionRecordsEqual( $return, $loaded );
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/McrSchemaDetection.php b/tests/phpunit/includes/Storage/McrSchemaDetection.php
deleted file mode 100644 (file)
index c90d428..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IDatabase;
-
-/**
- * Trait providing methods for detecting which MCR schema migration phase the current schema
- * is compatible with.
- */
-trait McrSchemaDetection {
-
-       /**
-        * Returns true if MCR-related tables exist in the database.
-        * If yes, the database is compatible with with MIGRATION_NEW.
-        * If hasPreMcrFields() also returns true, the database supports MIGRATION_WRITE_BOTH mode.
-        *
-        * @param IDatabase $db
-        * @return bool
-        */
-       protected function hasMcrTables( IDatabase $db ) {
-               return $db->tableExists( 'slots', __METHOD__ );
-       }
-
-       /**
-        * Returns true if pre-MCR fields still exist in the database.
-        * If yes, the database is compatible with with MIGRATION_OLD mode.
-        * If hasMcrTables() also returns true, the database supports MIGRATION_WRITE_BOTH mode.
-        *
-        * Note that if the database has been updated in MIGRATION_NEW mode,
-        * the rev_text_id field will be 0 for new revisions. This means that
-        * in MIGRATION_OLD mode, reading such revisions will fail, even though
-        * all the necessary fields exist.
-        * This is not relevant for unit tests, since unit tests reset the database content anyway.
-        *
-        * @param IDatabase $db
-        * @return bool
-        */
-       protected function hasPreMcrFields( IDatabase $db ) {
-               return $db->fieldExists( 'revision', 'rev_content_model', __METHOD__ );
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/McrSchemaOverride.php b/tests/phpunit/includes/Storage/McrSchemaOverride.php
deleted file mode 100644 (file)
index d2f58bf..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IMaintainableDatabase;
-use MediaWiki\DB\PatchFileLocation;
-
-/**
- * Trait providing schema overrides that allow tests to run against the post-migration
- * MCR database schema.
- */
-trait McrSchemaOverride {
-
-       use PatchFileLocation;
-       use McrSchemaDetection;
-
-       /**
-        * @return int
-        */
-       protected function getMcrMigrationStage() {
-               return MIGRATION_NEW;
-       }
-
-       /**
-        * @return string[]
-        */
-       protected function getMcrTablesToReset() {
-               return [
-                       'content',
-                       'content_models',
-                       'slots',
-                       'slot_roles',
-               ];
-       }
-
-       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
-               $overrides = [
-                       'scripts' => [],
-                       'drop' => [],
-                       'create' => [],
-                       'alter' => [],
-               ];
-
-               if ( !$this->hasMcrTables( $db ) ) {
-                       $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots.sql' );
-               }
-
-               if ( !$this->hasPreMcrFields( $db ) ) {
-                       $overrides['alter'][] = 'revision';
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'drop-pre-mcr-fields', __DIR__ );
-               }
-
-               return $overrides;
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php
deleted file mode 100644 (file)
index 10c20b9..0000000
+++ /dev/null
@@ -1,185 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use InvalidArgumentException;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use Revision;
-use WikitextContent;
-
-/**
- * Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
-       use McrWriteBothSchemaOverride;
-
-       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
-               $row = parent::revisionToRow( $rev, $options );
-
-               $row->rev_text_id = (string)$rev->getTextId();
-               $row->rev_content_format = (string)$rev->getContentFormat();
-               $row->rev_content_model = (string)$rev->getContentModel();
-
-               return $row;
-       }
-
-       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
-               // New schema is being written
-               $this->assertSelect(
-                       'slots',
-                       [ 'count(*)' ],
-                       [ 'slot_revision_id' => $rev->getId() ],
-                       [ [ '1' ] ]
-               );
-
-               $this->assertSelect(
-                       'content',
-                       [ 'count(*)' ],
-                       [ 'content_address' => $rev->getSlot( 'main' )->getAddress() ],
-                       [ [ '1' ] ]
-               );
-
-               // Legacy schema is still being written
-               $this->assertSelect(
-                       [ 'revision', 'text' ],
-                       [ 'count(*)' ],
-                       [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
-                       [ [ 1 ] ],
-                       [],
-                       [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
-               );
-
-               parent::assertRevisionExistsInDatabase( $rev );
-       }
-
-       /**
-        * @param SlotRecord $a
-        * @param SlotRecord $b
-        */
-       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
-               parent::assertSameSlotContent( $a, $b );
-
-               // Assert that the same content ID has been used
-               if ( $a->hasContentId() && $b->hasContentId() ) {
-                       $this->assertSame( $a->getContentId(), $b->getContentId() );
-               }
-       }
-
-       public function provideInsertRevisionOn_failures() {
-               foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
-                       yield $case;
-               }
-
-               yield 'slot that is not main slot' => [
-                       [
-                               'content' => [
-                                       'main' => new WikitextContent( 'Chicken' ),
-                                       'lalala' => new WikitextContent( 'Duck' ),
-                               ],
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'timestamp' => '20171117010101',
-                               'user' => true,
-                       ],
-                       new InvalidArgumentException( 'Only the main slot is supported' )
-               ];
-       }
-
-       public function provideNewMutableRevisionFromArray() {
-               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
-                       yield $case;
-               }
-
-               yield 'Basic array, with page & id' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'text_id' => 2,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 46,
-                               'parent_id' => 1,
-                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'comment' => 'Goat Comment!',
-                               'content_format' => 'text/x-wiki',
-                               'content_model' => 'wikitext',
-                       ]
-               ];
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
-        * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
-        */
-       public function testInsertRevisionFromArchiveRow_unmigratedArchiveRow() {
-               // The main purpose of this test is to assert that after reading an archive
-               // row using the old schema it can be inserted into the revision table,
-               // and a slot row is created based on slot emulated from the old-style archive row,
-               // when none such slot row exists yet.
-
-               $title = $this->getTestPage()->getTitle();
-
-               $this->db->insert(
-                       'text',
-                       [ 'old_text' => 'Just a test', 'old_flags' => 'utf-8' ],
-                       __METHOD__
-               );
-
-               $textId = $this->db->insertId();
-
-               $row = (object)[
-                       'ar_minor_edit' => '0',
-                       'ar_user' => '0',
-                       'ar_user_text' => '127.0.0.1',
-                       'ar_actor' => null,
-                       'ar_len' => '11',
-                       'ar_deleted' => '0',
-                       'ar_rev_id' => 112277,
-                       'ar_timestamp' => $this->db->timestamp( '20180101000000' ),
-                       'ar_sha1' => 'deadbeef',
-                       'ar_page_id' => $title->getArticleID(),
-                       'ar_comment_text' => 'just a test',
-                       'ar_comment_data' => null,
-                       'ar_comment_cid' => null,
-                       'ar_content_format' => null,
-                       'ar_content_model' => null,
-                       'ts_tags' => null,
-                       'ar_id' => 17,
-                       'ar_namespace' => $title->getNamespace(),
-                       'ar_title' => $title->getDBkey(),
-                       'ar_text_id' => $textId,
-                       'ar_parent_id' => 112211,
-               ];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $rev = $store->newRevisionFromArchiveRow( $row );
-
-               // re-insert archived revision
-               $return = $store->insertRevisionOn( $rev, $this->db );
-
-               // is the new revision correct?
-               $this->assertRevisionCompleteness( $return );
-               $this->assertRevisionRecordsEqual( $rev, $return );
-
-               // can we load it from the store?
-               $loaded = $store->getRevisionById( $return->getId() );
-               $this->assertNotNull( $loaded );
-               $this->assertRevisionCompleteness( $loaded );
-               $this->assertRevisionRecordsEqual( $return, $loaded );
-
-               // can we find it directly in the database?
-               $this->assertRevisionExistsInDatabase( $return );
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/McrWriteBothSchemaOverride.php b/tests/phpunit/includes/Storage/McrWriteBothSchemaOverride.php
deleted file mode 100644 (file)
index cdcba4f..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IMaintainableDatabase;
-use MediaWiki\DB\PatchFileLocation;
-
-/**
- * Trait providing schema overrides that allow tests to run against the intermediate MCR database
- * schema for use during schema migration.
- */
-trait McrWriteBothSchemaOverride {
-
-       use PatchFileLocation;
-       use McrSchemaDetection;
-
-       /**
-        * @return int
-        */
-       protected function getMcrMigrationStage() {
-               return SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD;
-       }
-
-       /**
-        * @return string[]
-        */
-       protected function getMcrTablesToReset() {
-               return [ 'content', 'content_models', 'slots', 'slot_roles' ];
-       }
-
-       /**
-        * @return array[]
-        */
-       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
-               $overrides = [
-                       'scripts' => [],
-                       'drop' => [],
-                       'create' => [],
-                       'alter' => [],
-               ];
-
-               if ( !$this->hasMcrTables( $db ) ) {
-                       $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' );
-               }
-
-               if ( !$this->hasPreMcrFields( $db ) ) {
-                       $overrides['alter'][] = 'revision';
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'create-pre-mcr-fields', __DIR__ );
-               }
-
-               return $overrides;
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php b/tests/phpunit/includes/Storage/MutableRevisionRecordTest.php
deleted file mode 100644 (file)
index 3e91df4..0000000
+++ /dev/null
@@ -1,347 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use InvalidArgumentException;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiTestCase;
-use TextContent;
-use Title;
-use User;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Storage\MutableRevisionRecord
- * @covers \MediaWiki\Storage\RevisionRecord
- */
-class MutableRevisionRecordTest extends MediaWikiTestCase {
-
-       use RevisionRecordTests;
-
-       /**
-        * @param array $rowOverrides
-        *
-        * @return MutableRevisionRecord
-        */
-       protected function newRevision( array $rowOverrides = [] ) {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
-
-               $user = new UserIdentityValue( 11, 'Tester', 0 );
-               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
-               $record = new MutableRevisionRecord( $title );
-
-               if ( isset( $rowOverrides['rev_deleted'] ) ) {
-                       $record->setVisibility( $rowOverrides['rev_deleted'] );
-               }
-
-               if ( isset( $rowOverrides['rev_id'] ) ) {
-                       $record->setId( $rowOverrides['rev_id'] );
-               }
-
-               if ( isset( $rowOverrides['rev_page'] ) ) {
-                       $record->setPageId( $rowOverrides['rev_page'] );
-               }
-
-               $record->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
-               $record->setComment( $comment );
-               $record->setUser( $user );
-               $record->setTimestamp( '20101010000000' );
-
-               return $record;
-       }
-
-       public function provideConstructor() {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
-
-               yield [
-                       $title,
-                       'acmewiki'
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructor
-        *
-        * @param Title $title
-        * @param bool $wikiId
-        */
-       public function testConstructorAndGetters(
-               Title $title,
-               $wikiId = false
-       ) {
-               $rec = new MutableRevisionRecord( $title, $wikiId );
-
-               $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
-               $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
-       }
-
-       public function provideConstructorFailure() {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
-
-               yield 'not a wiki id' => [
-                       $title,
-                       null
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructorFailure
-        *
-        * @param Title $title
-        * @param bool $wikiId
-        */
-       public function testConstructorFailure(
-               Title $title,
-               $wikiId = false
-       ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new MutableRevisionRecord( $title, $wikiId );
-       }
-
-       public function testSetGetId() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertNull( $record->getId() );
-               $record->setId( 888 );
-               $this->assertSame( 888, $record->getId() );
-       }
-
-       public function testSetGetUser() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $user = $this->getTestSysop()->getUser();
-               $this->assertNull( $record->getUser() );
-               $record->setUser( $user );
-               $this->assertSame( $user, $record->getUser() );
-       }
-
-       public function testSetGetPageId() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertSame( 0, $record->getPageId() );
-               $record->setPageId( 999 );
-               $this->assertSame( 999, $record->getPageId() );
-       }
-
-       public function testSetGetParentId() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertNull( $record->getParentId() );
-               $record->setParentId( 100 );
-               $this->assertSame( 100, $record->getParentId() );
-       }
-
-       public function testGetMainContentWhenEmpty() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->setExpectedException( RevisionAccessException::class );
-               $this->assertNull( $record->getContent( SlotRecord::MAIN ) );
-       }
-
-       public function testSetGetMainContent() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $content = new WikitextContent( 'Badger' );
-               $record->setContent( SlotRecord::MAIN, $content );
-               $this->assertSame( $content, $record->getContent( SlotRecord::MAIN ) );
-       }
-
-       public function testGetSlotWhenEmpty() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertFalse( $record->hasSlot( SlotRecord::MAIN ) );
-
-               $this->setExpectedException( RevisionAccessException::class );
-               $record->getSlot( SlotRecord::MAIN );
-       }
-
-       public function testSetGetSlot() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $slot = SlotRecord::newUnsaved(
-                       SlotRecord::MAIN,
-                       new WikitextContent( 'x' )
-               );
-               $record->setSlot( $slot );
-               $this->assertTrue( $record->hasSlot( SlotRecord::MAIN ) );
-               $this->assertSame( $slot, $record->getSlot( SlotRecord::MAIN ) );
-       }
-
-       public function testSetGetMinor() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertFalse( $record->isMinor() );
-               $record->setMinorEdit( true );
-               $this->assertSame( true, $record->isMinor() );
-       }
-
-       public function testSetGetTimestamp() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertNull( $record->getTimestamp() );
-               $record->setTimestamp( '20180101010101' );
-               $this->assertSame( '20180101010101', $record->getTimestamp() );
-       }
-
-       public function testSetGetVisibility() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertSame( 0, $record->getVisibility() );
-               $record->setVisibility( RevisionRecord::DELETED_USER );
-               $this->assertSame( RevisionRecord::DELETED_USER, $record->getVisibility() );
-       }
-
-       public function testSetGetSha1() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertSame( 'phoiac9h4m842xq45sp7s6u21eteeq1', $record->getSha1() );
-               $record->setSha1( 'someHash' );
-               $this->assertSame( 'someHash', $record->getSha1() );
-       }
-
-       public function testGetSlots() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertInstanceOf( MutableRevisionSlots::class, $record->getSlots() );
-       }
-
-       public function testSetGetSize() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $this->assertSame( 0, $record->getSize() );
-               $record->setSize( 775 );
-               $this->assertSame( 775, $record->getSize() );
-       }
-
-       public function testSetGetComment() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $comment = new CommentStoreComment( 1, 'foo' );
-               $this->assertNull( $record->getComment() );
-               $record->setComment( $comment );
-               $this->assertSame( $comment, $record->getComment() );
-       }
-
-       public function testSimpleGetOriginalAndInheritedSlots() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $mainSlot = new SlotRecord(
-                       (object)[
-                               'slot_id' => 1,
-                               'slot_revision_id' => null, // unsaved
-                               'slot_content_id' => 1,
-                               'content_address' => null, // touched
-                               'model_name' => 'x',
-                               'role_name' => 'main',
-                               'slot_origin' => null // touched
-                       ],
-                       new WikitextContent( 'main' )
-               );
-               $auxSlot = new SlotRecord(
-                       (object)[
-                               'slot_id' => 2,
-                               'slot_revision_id' => null, // unsaved
-                               'slot_content_id' => 1,
-                               'content_address' => 'foo', // inherited
-                               'model_name' => 'x',
-                               'role_name' => 'aux',
-                               'slot_origin' => 1 // inherited
-                       ],
-                       new WikitextContent( 'aux' )
-               );
-
-               $record->setSlot( $mainSlot );
-               $record->setSlot( $auxSlot );
-
-               $this->assertSame( [ 'main' ], $record->getOriginalSlots()->getSlotRoles() );
-               $this->assertSame( $mainSlot, $record->getOriginalSlots()->getSlot( SlotRecord::MAIN ) );
-
-               $this->assertSame( [ 'aux' ], $record->getInheritedSlots()->getSlotRoles() );
-               $this->assertSame( $auxSlot, $record->getInheritedSlots()->getSlot( 'aux' ) );
-       }
-
-       public function testSimpleremoveSlot() {
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-
-               $a = new WikitextContent( 'a' );
-               $b = new WikitextContent( 'b' );
-
-               $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
-               $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
-
-               $record->removeSlot( 'b' );
-
-               $this->assertTrue( $record->hasSlot( 'a' ) );
-               $this->assertFalse( $record->hasSlot( 'b' ) );
-       }
-
-       public function testApplyUpdate() {
-               $update = new RevisionSlotsUpdate();
-
-               $a = new WikitextContent( 'a' );
-               $b = new WikitextContent( 'b' );
-               $c = new WikitextContent( 'c' );
-               $x = new WikitextContent( 'x' );
-
-               $update->modifyContent( 'b', $x );
-               $update->modifyContent( 'c', $x );
-               $update->removeSlot( 'c' );
-               $update->removeSlot( 'd' );
-
-               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-               $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
-               $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
-               $record->inheritSlot( SlotRecord::newSaved( 7, 5, 'c', SlotRecord::newUnsaved( 'c', $c ) ) );
-
-               $record->applyUpdate( $update );
-
-               $this->assertEquals( [ 'b' ], array_keys( $record->getOriginalSlots()->getSlots() ) );
-               $this->assertEquals( $a, $record->getSlot( 'a' )->getContent() );
-               $this->assertEquals( $x, $record->getSlot( 'b' )->getContent() );
-               $this->assertFalse( $record->hasSlot( 'c' ) );
-       }
-
-       public function provideNotReadyForInsertion() {
-               /** @var Title $title */
-               $title = $this->getMock( Title::class );
-
-               /** @var User $user */
-               $user = $this->getMock( User::class );
-
-               /** @var CommentStoreComment $comment */
-               $comment = $this->getMockBuilder( CommentStoreComment::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $content = new TextContent( 'Test' );
-
-               $rev = new MutableRevisionRecord( $title );
-               yield 'empty' => [ $rev ];
-
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setContent( SlotRecord::MAIN, $content );
-               $rev->setUser( $user );
-               $rev->setComment( $comment );
-               yield 'no timestamp' => [ $rev ];
-
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setUser( $user );
-               $rev->setComment( $comment );
-               $rev->setTimestamp( '20101010000000' );
-               yield 'no content' => [ $rev ];
-
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setContent( SlotRecord::MAIN, $content );
-               $rev->setComment( $comment );
-               $rev->setTimestamp( '20101010000000' );
-               yield 'no user' => [ $rev ];
-
-               $rev = new MutableRevisionRecord( $title );
-               $rev->setUser( $user );
-               $rev->setContent( SlotRecord::MAIN, $content );
-               $rev->setTimestamp( '20101010000000' );
-               yield 'no comment' => [ $rev ];
-       }
-
-       /**
-        * @dataProvider provideNotReadyForInsertion
-        */
-       public function testNotReadyForInsertion( $rev ) {
-               $this->assertFalse( $rev->isReadyForInsertion() );
-       }
-}
diff --git a/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php b/tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php
deleted file mode 100644 (file)
index 1ef0121..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use Content;
-use InvalidArgumentException;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\SlotRecord;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Storage\MutableRevisionSlots
- */
-class MutableRevisionSlotsTest extends RevisionSlotsTest {
-
-       /**
-        * @param SlotRecord[] $slots
-        * @return RevisionSlots
-        */
-       protected function newRevisionSlots( $slots = [] ) {
-               return new MutableRevisionSlots( $slots );
-       }
-
-       public function provideConstructorFailue() {
-               yield 'array or the wrong thing' => [
-                       [ 1, 2, 3 ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructorFailue
-        * @param $slots
-        *
-        * @covers \MediaWiki\Storage\RevisionSlots::__construct
-        * @covers \MediaWiki\Storage\RevisionSlots::setSlotsInternal
-        */
-       public function testConstructorFailue( $slots ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-
-               new MutableRevisionSlots( $slots );
-       }
-
-       public function testSetMultipleSlots() {
-               $slots = new MutableRevisionSlots();
-
-               $this->assertSame( [], $slots->getSlots() );
-
-               $slotA = SlotRecord::newUnsaved( 'some', new WikitextContent( 'A' ) );
-               $slots->setSlot( $slotA );
-               $this->assertTrue( $slots->hasSlot( 'some' ) );
-               $this->assertSame( $slotA, $slots->getSlot( 'some' ) );
-               $this->assertSame( [ 'some' => $slotA ], $slots->getSlots() );
-
-               $slotB = SlotRecord::newUnsaved( 'other', new WikitextContent( 'B' ) );
-               $slots->setSlot( $slotB );
-               $this->assertTrue( $slots->hasSlot( 'other' ) );
-               $this->assertSame( $slotB, $slots->getSlot( 'other' ) );
-               $this->assertSame( [ 'some' => $slotA, 'other' => $slotB ], $slots->getSlots() );
-       }
-
-       public function testSetExistingSlotOverwritesSlot() {
-               $slots = new MutableRevisionSlots();
-
-               $this->assertSame( [], $slots->getSlots() );
-
-               $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $slots->setSlot( $slotA );
-               $this->assertSame( $slotA, $slots->getSlot( SlotRecord::MAIN ) );
-               $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
-
-               $slotB = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'B' ) );
-               $slots->setSlot( $slotB );
-               $this->assertSame( $slotB, $slots->getSlot( SlotRecord::MAIN ) );
-               $this->assertSame( [ 'main' => $slotB ], $slots->getSlots() );
-       }
-
-       /**
-        * @param string $role
-        * @param Content $content
-        * @return SlotRecord
-        */
-       private function newSavedSlot( $role, Content $content ) {
-               return SlotRecord::newSaved( 7, 7, 'xyz', SlotRecord::newUnsaved( $role, $content ) );
-       }
-
-       public function testInheritSlotOverwritesSlot() {
-               $slots = new MutableRevisionSlots();
-               $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $slots->setSlot( $slotA );
-               $slotB = $this->newSavedSlot( SlotRecord::MAIN, new WikitextContent( 'B' ) );
-               $slotC = $this->newSavedSlot( 'foo', new WikitextContent( 'C' ) );
-               $slots->inheritSlot( $slotB );
-               $slots->inheritSlot( $slotC );
-               $this->assertSame( [ 'main', 'foo' ], $slots->getSlotRoles() );
-               $this->assertNotSame( $slotB, $slots->getSlot( SlotRecord::MAIN ) );
-               $this->assertNotSame( $slotC, $slots->getSlot( 'foo' ) );
-               $this->assertTrue( $slots->getSlot( SlotRecord::MAIN )->isInherited() );
-               $this->assertTrue( $slots->getSlot( 'foo' )->isInherited() );
-               $this->assertSame( $slotB->getContent(), $slots->getSlot( SlotRecord::MAIN )->getContent() );
-               $this->assertSame( $slotC->getContent(), $slots->getSlot( 'foo' )->getContent() );
-       }
-
-       public function testSetContentOfExistingSlotOverwritesContent() {
-               $slots = new MutableRevisionSlots();
-
-               $this->assertSame( [], $slots->getSlots() );
-
-               $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $slots->setSlot( $slotA );
-               $this->assertSame( $slotA, $slots->getSlot( SlotRecord::MAIN ) );
-               $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
-
-               $newContent = new WikitextContent( 'B' );
-               $slots->setContent( SlotRecord::MAIN, $newContent );
-               $this->assertSame( $newContent, $slots->getContent( SlotRecord::MAIN ) );
-       }
-
-       public function testRemoveExistingSlot() {
-               $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $slots = new MutableRevisionSlots( [ $slotA ] );
-
-               $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
-
-               $slots->removeSlot( SlotRecord::MAIN );
-               $this->assertSame( [], $slots->getSlots() );
-               $this->setExpectedException( RevisionAccessException::class );
-               $slots->getSlot( SlotRecord::MAIN );
-       }
-
-       public function testNewFromParentRevisionSlots() {
-               /** @var SlotRecord[] $parentSlots */
-               $parentSlots = [
-                       'some' => $this->newSavedSlot( 'some', new WikitextContent( 'X' ) ),
-                       'other' => $this->newSavedSlot( 'other', new WikitextContent( 'Y' ) ),
-               ];
-               $slots = MutableRevisionSlots::newFromParentRevisionSlots( $parentSlots );
-               $this->assertSame( [ 'some', 'other' ], $slots->getSlotRoles() );
-               $this->assertNotSame( $parentSlots['some'], $slots->getSlot( 'some' ) );
-               $this->assertNotSame( $parentSlots['other'], $slots->getSlot( 'other' ) );
-               $this->assertTrue( $slots->getSlot( 'some' )->isInherited() );
-               $this->assertTrue( $slots->getSlot( 'other' )->isInherited() );
-               $this->assertSame( $parentSlots['some']->getContent(), $slots->getContent( 'some' ) );
-               $this->assertSame( $parentSlots['other']->getContent(), $slots->getContent( 'other' ) );
-       }
-
-}
index f377993..ef7f2f5 100644 (file)
@@ -18,9 +18,15 @@ class NameTableStoreFactoryTest extends MediaWikiTestCase {
        /**
         * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
         */
-       private function getMockLoadBalancer() {
-               return $this->getMockBuilder( ILoadBalancer::class )
+       private function getMockLoadBalancer( $localDomain ) {
+               $mock = $this->getMockBuilder( ILoadBalancer::class )
                        ->disableOriginalConstructor()->getMock();
+
+               $mock->expects( $this->any() )
+                       ->method( 'getLocalDomainID' )
+                       ->willReturn( $localDomain );
+
+               return $mock;
        }
 
        /**
@@ -30,11 +36,16 @@ class NameTableStoreFactoryTest extends MediaWikiTestCase {
                $mock = $this->getMockBuilder( ILBFactory::class )
                        ->disableOriginalConstructor()->getMock();
 
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $localDomain = $lbFactory->getLocalDomainID();
+
+               $mock->expects( $this->any() )->method( 'getLocalDomainID' )->willReturn( $localDomain );
+
                $mock->expects( $this->once() )
                        ->method( 'getMainLB' )
                        ->with( $this->equalTo( $expectedWiki ) )
-                       ->willReturnCallback( function ( $domain ) use ( $expectedWiki ) {
-                               return $this->getMockLoadBalancer();
+                       ->willReturnCallback( function ( $domain ) use ( $localDomain ) {
+                               return $this->getMockLoadBalancer( $localDomain );
                        } );
 
                return $mock;
@@ -68,12 +79,9 @@ class NameTableStoreFactoryTest extends MediaWikiTestCase {
        /** @dataProvider provideTestGet */
        public function testGet( $tableName, $wiki, $expectedWiki ) {
                $services = MediaWikiServices::getInstance();
-               $db = wfGetDB( DB_MASTER );
-               if ( $wiki === false ) {
-                       $wiki2 = $db->getWikiID();
-               } else {
-                       $wiki2 = $wiki;
-               }
+               $wiki2 = ( $wiki === false )
+                       ? $services->getDBLoadBalancerFactory()->getLocalDomainID()
+                       : $wiki;
                $names = new NameTableStoreFactory(
                        $this->getMockLoadBalancerFactory( $expectedWiki ),
                        $services->getMainWANObjectCache(),
diff --git a/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php
deleted file mode 100644 (file)
index 7e1e1ee..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Revision;
-
-/**
- * Tests RevisionStore against the pre-MCR, pre-ContentHandler DB schema.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
-       use PreMcrSchemaOverride;
-
-       protected function getContentHandlerUseDB() {
-               return false;
-       }
-
-       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
-               $row = parent::revisionToRow( $rev, $options );
-
-               $row->rev_text_id = (string)$rev->getTextId();
-
-               return $row;
-       }
-
-       public function provideGetArchiveQueryInfo() {
-               yield [
-                       [
-                               'tables' => [ 'archive' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultArchiveFields(),
-                                       [
-                                               'ar_comment_text' => 'ar_comment',
-                                               'ar_comment_data' => 'NULL',
-                                               'ar_comment_cid' => 'NULL',
-                                               'ar_user_text' => 'ar_user_text',
-                                               'ar_user' => 'ar_user',
-                                               'ar_actor' => 'NULL',
-                                       ]
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-       }
-
-       public function provideGetQueryInfo() {
-               yield [
-                       [],
-                       [
-                               'tables' => [ 'revision' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields()
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-               yield [
-                       [ 'page' ],
-                       [
-                               'tables' => [ 'revision', 'page' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields(),
-                                       [
-                                               'page_namespace',
-                                               'page_title',
-                                               'page_id',
-                                               'page_latest',
-                                               'page_is_redirect',
-                                               'page_len',
-                                       ]
-                               ),
-                               'joins' => [
-                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
-                               ],
-                       ]
-               ];
-               yield [
-                       [ 'user' ],
-                       [
-                               'tables' => [ 'revision', 'user' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields(),
-                                       [
-                                               'user_name',
-                                       ]
-                               ),
-                               'joins' => [
-                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
-                               ],
-                       ]
-               ];
-               yield [
-                       [ 'text' ],
-                       [
-                               'tables' => [ 'revision', 'text' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields(),
-                                       [
-                                               'old_text',
-                                               'old_flags',
-                                       ]
-                               ),
-                               'joins' => [
-                                       'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
-                               ],
-                       ]
-               ];
-       }
-
-       public function provideGetSlotsQueryInfo() {
-               $db = wfGetDB( DB_REPLICA );
-
-               yield [
-                       [],
-                       [
-                               'tables' => [
-                                       'slots' => 'revision',
-                               ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id' => 'slots.rev_id',
-                                               'slot_content_id' => 'NULL',
-                                               'slot_origin' => 'slots.rev_id',
-                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
-                                       ]
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-               yield [
-                       [ 'content' ],
-                       [
-                               'tables' => [
-                                       'slots' => 'revision',
-                               ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id' => 'slots.rev_id',
-                                               'slot_content_id' => 'NULL',
-                                               'slot_origin' => 'slots.rev_id',
-                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
-                                               'content_size' => 'slots.rev_len',
-                                               'content_sha1' => 'slots.rev_sha1',
-                                               'content_address' =>
-                                                       $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
-                                               'model_name' => 'NULL',
-                                       ]
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-       }
-
-       public function provideNewMutableRevisionFromArray() {
-               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
-                       yield $case;
-               }
-
-               yield 'Basic array, with page & id' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'text_id' => 2,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 46,
-                               'parent_id' => 1,
-                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'comment' => 'Goat Comment!',
-                       ]
-               ];
-       }
-
-}
index 3933986..3ba9032 100644 (file)
@@ -5,8 +5,8 @@ namespace MediaWiki\Tests\Storage;
 use CommentStoreComment;
 use Content;
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 use MediaWikiTestCase;
 use ParserOptions;
 use RecentChange;
diff --git a/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php
deleted file mode 100644 (file)
index 687ad4f..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use InvalidArgumentException;
-use MediaWiki\Storage\RevisionRecord;
-use Revision;
-use WikitextContent;
-
-/**
- * Tests RevisionStore against the pre-MCR DB schema.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
-       use PreMcrSchemaOverride;
-
-       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
-               $row = parent::revisionToRow( $rev, $options );
-
-               $row->rev_text_id = (string)$rev->getTextId();
-               $row->rev_content_format = (string)$rev->getContentFormat();
-               $row->rev_content_model = (string)$rev->getContentModel();
-
-               return $row;
-       }
-
-       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
-               // Legacy schema is still being written
-               $this->assertSelect(
-                       [ 'revision', 'text' ],
-                       [ 'count(*)' ],
-                       [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
-                       [ [ 1 ] ],
-                       [],
-                       [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
-               );
-
-               parent::assertRevisionExistsInDatabase( $rev );
-       }
-
-       public function provideInsertRevisionOn_failures() {
-               foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
-                       yield $case;
-               }
-
-               yield 'slot that is not main slot' => [
-                       [
-                               'content' => [
-                                       'main' => new WikitextContent( 'Chicken' ),
-                                       'lalala' => new WikitextContent( 'Duck' ),
-                               ],
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'timestamp' => '20171117010101',
-                               'user' => true,
-                       ],
-                       new InvalidArgumentException( 'Only the main slot is supported' )
-               ];
-       }
-
-       public function provideNewMutableRevisionFromArray() {
-               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
-                       yield $case;
-               }
-
-               yield 'Basic array, with page & id' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'text_id' => 2,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 46,
-                               'parent_id' => 1,
-                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'comment' => 'Goat Comment!',
-                               'content_format' => 'text/x-wiki',
-                               'content_model' => 'wikitext',
-                       ]
-               ];
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/PreMcrSchemaOverride.php b/tests/phpunit/includes/Storage/PreMcrSchemaOverride.php
deleted file mode 100644 (file)
index bb72a57..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IMaintainableDatabase;
-use MediaWiki\DB\PatchFileLocation;
-
-/**
- * Trait providing schema overrides that allow tests to run against the pre-MCR database schema.
- */
-trait PreMcrSchemaOverride {
-
-       use PatchFileLocation;
-       use McrSchemaDetection;
-
-       /**
-        * @return int
-        */
-       protected function getMcrMigrationStage() {
-               return MIGRATION_OLD;
-       }
-
-       /**
-        * @return string[]
-        */
-       protected function getMcrTablesToReset() {
-               return [];
-       }
-
-       /**
-        * @return array[]
-        */
-       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
-               $overrides = [
-                       'scripts' => [],
-                       'drop' => [],
-                       'create' => [],
-                       'alter' => [],
-               ];
-
-               if ( $this->hasMcrTables( $db ) ) {
-                       $overrides['drop'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/drop-mcr-tables', __DIR__ );
-               }
-
-               if ( !$this->hasPreMcrFields( $db ) ) {
-                       $overrides['alter'][] = 'revision';
-                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/create-pre-mcr-fields', __DIR__ );
-               }
-
-               return $overrides;
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/RevisionArchiveRecordTest.php b/tests/phpunit/includes/Storage/RevisionArchiveRecordTest.php
deleted file mode 100644 (file)
index fad6228..0000000
+++ /dev/null
@@ -1,272 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use InvalidArgumentException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\RevisionArchiveRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\User\UserIdentity;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiTestCase;
-use TextContent;
-use Title;
-
-/**
- * @covers \MediaWiki\Storage\RevisionArchiveRecord
- * @covers \MediaWiki\Storage\RevisionRecord
- */
-class RevisionArchiveRecordTest extends MediaWikiTestCase {
-
-       use RevisionRecordTests;
-
-       /**
-        * @param array $rowOverrides
-        *
-        * @return RevisionArchiveRecord
-        */
-       protected function newRevision( array $rowOverrides = [] ) {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
-
-               $user = new UserIdentityValue( 11, 'Tester', 0 );
-               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
-               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
-               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
-               $slots = new RevisionSlots( [ $main, $aux ] );
-
-               $row = [
-                       'ar_id' => '5',
-                       'ar_rev_id' => '7',
-                       'ar_page_id' => strval( $title->getArticleID() ),
-                       'ar_timestamp' => '20200101000000',
-                       'ar_deleted' => 0,
-                       'ar_minor_edit' => 0,
-                       'ar_parent_id' => '5',
-                       'ar_len' => $slots->computeSize(),
-                       'ar_sha1' => $slots->computeSha1(),
-               ];
-
-               foreach ( $rowOverrides as $field => $value ) {
-                       $field = preg_replace( '/^rev_/', 'ar_', $field );
-                       $row[$field] = $value;
-               }
-
-               return new RevisionArchiveRecord( $title, $user, $comment, (object)$row, $slots );
-       }
-
-       public function provideConstructor() {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
-
-               $user = new UserIdentityValue( 11, 'Tester', 0 );
-               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
-               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
-               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
-               $slots = new RevisionSlots( [ $main, $aux ] );
-
-               $protoRow = [
-                       'ar_id' => '5',
-                       'ar_rev_id' => '7',
-                       'ar_page_id' => strval( $title->getArticleID() ),
-                       'ar_timestamp' => '20200101000000',
-                       'ar_deleted' => 0,
-                       'ar_minor_edit' => 0,
-                       'ar_parent_id' => '5',
-                       'ar_len' => $slots->computeSize(),
-                       'ar_sha1' => $slots->computeSha1(),
-               ];
-
-               $row = $protoRow;
-               yield 'all info' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots,
-                       'acmewiki'
-               ];
-
-               $row = $protoRow;
-               $row['ar_minor_edit'] = '1';
-               $row['ar_deleted'] = strval( RevisionRecord::DELETED_USER );
-
-               yield 'minor deleted' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-               unset( $row['ar_parent'] );
-
-               yield 'no parent' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-               $row['ar_len'] = null;
-               $row['ar_sha1'] = '';
-
-               yield 'ar_len is null, ar_sha1 is ""' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-               yield 'no length, no hash' => [
-                       Title::newFromText( 'DummyDoesNotExist' ),
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructor
-        *
-        * @param Title $title
-        * @param UserIdentity $user
-        * @param CommentStoreComment $comment
-        * @param object $row
-        * @param RevisionSlots $slots
-        * @param bool $wikiId
-        */
-       public function testConstructorAndGetters(
-               Title $title,
-               UserIdentity $user,
-               CommentStoreComment $comment,
-               $row,
-               RevisionSlots $slots,
-               $wikiId = false
-       ) {
-               $rec = new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId );
-
-               $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
-               $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
-               $this->assertSame( $comment, $rec->getComment(), 'getComment' );
-
-               $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
-               $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
-
-               $this->assertSame( (int)$row->ar_id, $rec->getArchiveId(), 'getArchiveId' );
-               $this->assertSame( (int)$row->ar_rev_id, $rec->getId(), 'getId' );
-               $this->assertSame( (int)$row->ar_page_id, $rec->getPageId(), 'getId' );
-               $this->assertSame( $row->ar_timestamp, $rec->getTimestamp(), 'getTimestamp' );
-               $this->assertSame( (int)$row->ar_deleted, $rec->getVisibility(), 'getVisibility' );
-               $this->assertSame( (bool)$row->ar_minor_edit, $rec->isMinor(), 'getIsMinor' );
-
-               if ( isset( $row->ar_parent_id ) ) {
-                       $this->assertSame( (int)$row->ar_parent_id, $rec->getParentId(), 'getParentId' );
-               } else {
-                       $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
-               }
-
-               if ( isset( $row->ar_len ) ) {
-                       $this->assertSame( (int)$row->ar_len, $rec->getSize(), 'getSize' );
-               } else {
-                       $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
-               }
-
-               if ( !empty( $row->ar_sha1 ) ) {
-                       $this->assertSame( $row->ar_sha1, $rec->getSha1(), 'getSha1' );
-               } else {
-                       $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
-               }
-       }
-
-       public function provideConstructorFailure() {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
-
-               $user = new UserIdentityValue( 11, 'Tester', 0 );
-
-               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
-               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
-               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
-               $slots = new RevisionSlots( [ $main, $aux ] );
-
-               $protoRow = [
-                       'ar_id' => '5',
-                       'ar_rev_id' => '7',
-                       'ar_page_id' => strval( $title->getArticleID() ),
-                       'ar_timestamp' => '20200101000000',
-                       'ar_deleted' => 0,
-                       'ar_minor_edit' => 0,
-                       'ar_parent_id' => '5',
-                       'ar_len' => $slots->computeSize(),
-                       'ar_sha1' => $slots->computeSha1(),
-               ];
-
-               yield 'not a row' => [
-                       $title,
-                       $user,
-                       $comment,
-                       'not a row',
-                       $slots,
-                       'acmewiki'
-               ];
-
-               $row = $protoRow;
-               $row['ar_timestamp'] = 'kittens';
-
-               yield 'bad timestamp' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-
-               yield 'bad wiki' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots,
-                       12345
-               ];
-
-               // NOTE: $title->getArticleID does *not* have to match ar_page_id in all cases!
-       }
-
-       /**
-        * @dataProvider provideConstructorFailure
-        *
-        * @param Title $title
-        * @param UserIdentity $user
-        * @param CommentStoreComment $comment
-        * @param object $row
-        * @param RevisionSlots $slots
-        * @param bool $wikiId
-        */
-       public function testConstructorFailure(
-               Title $title,
-               UserIdentity $user,
-               CommentStoreComment $comment,
-               $row,
-               RevisionSlots $slots,
-               $wikiId = false
-       ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId );
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/RevisionQueryInfoTest.php b/tests/phpunit/includes/Storage/RevisionQueryInfoTest.php
deleted file mode 100644 (file)
index 165c27b..0000000
+++ /dev/null
@@ -1,1178 +0,0 @@
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\SlotRecord;
-use MediaWikiTestCase;
-use Revision;
-
-/**
- * Tests RevisionStore against the post-migration MCR DB schema.
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- */
-class RevisionQueryInfoTest extends MediaWikiTestCase {
-
-       protected function getRevisionQueryFields( $returnTextIdField = true ) {
-               $fields = [
-                       'rev_id',
-                       'rev_page',
-                       'rev_timestamp',
-                       'rev_minor_edit',
-                       'rev_deleted',
-                       'rev_len',
-                       'rev_parent_id',
-                       'rev_sha1',
-               ];
-               if ( $returnTextIdField ) {
-                       $fields[] = 'rev_text_id';
-               }
-               return $fields;
-       }
-
-       protected function getArchiveQueryFields( $returnTextFields = true ) {
-               $fields = [
-                       'ar_id',
-                       'ar_page_id',
-                       'ar_namespace',
-                       'ar_title',
-                       'ar_rev_id',
-                       'ar_timestamp',
-                       'ar_minor_edit',
-                       'ar_deleted',
-                       'ar_len',
-                       'ar_parent_id',
-                       'ar_sha1',
-               ];
-               if ( $returnTextFields ) {
-                       $fields[] = 'ar_text_id';
-               }
-               return $fields;
-       }
-
-       protected function getOldCommentQueryFields( $prefix ) {
-               return [
-                       "{$prefix}_comment_text" => "{$prefix}_comment",
-                       "{$prefix}_comment_data" => 'NULL',
-                       "{$prefix}_comment_cid" => 'NULL',
-               ];
-       }
-
-       protected function getCompatCommentQueryFields( $prefix ) {
-               return [
-                       "{$prefix}_comment_text"
-                               => "COALESCE( comment_{$prefix}_comment.comment_text, {$prefix}_comment )",
-                       "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
-                       "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id",
-               ];
-       }
-
-       protected function getNewCommentQueryFields( $prefix ) {
-               return [
-                       "{$prefix}_comment_text" => "comment_{$prefix}_comment.comment_text",
-                       "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
-                       "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id",
-               ];
-       }
-
-       protected function getOldActorQueryFields( $prefix ) {
-               return [
-                       "{$prefix}_user" => "{$prefix}_user",
-                       "{$prefix}_user_text" => "{$prefix}_user_text",
-                       "{$prefix}_actor" => 'NULL',
-               ];
-       }
-
-       protected function getNewActorQueryFields( $prefix, $tmp = false ) {
-               return [
-                       "{$prefix}_user" => "actor_{$prefix}_user.actor_user",
-                       "{$prefix}_user_text" => "actor_{$prefix}_user.actor_name",
-                       "{$prefix}_actor" => $tmp ?: "{$prefix}_actor",
-               ];
-       }
-
-       protected function getCompatActorQueryFields( $prefix, $tmp = false ) {
-               return [
-                       "{$prefix}_user" => "COALESCE( actor_{$prefix}_user.actor_user, {$prefix}_user )",
-                       "{$prefix}_user_text" => "COALESCE( actor_{$prefix}_user.actor_name, {$prefix}_user_text )",
-                       "{$prefix}_actor" => $tmp ?: "{$prefix}_actor",
-               ];
-       }
-
-       protected function getCompatActorJoins( $prefix ) {
-               return [
-                       "temp_{$prefix}_user" => [
-                               "LEFT JOIN",
-                               "temp_{$prefix}_user.revactor_{$prefix} = {$prefix}_id",
-                       ],
-                       "actor_{$prefix}_user" => [
-                               "LEFT JOIN",
-                               "actor_{$prefix}_user.actor_id = temp_{$prefix}_user.revactor_actor",
-                       ],
-               ];
-       }
-
-       protected function getCompatCommentJoins( $prefix ) {
-               return [
-                       "temp_{$prefix}_comment" => [
-                               "LEFT JOIN",
-                               "temp_{$prefix}_comment.revcomment_{$prefix} = {$prefix}_id",
-                       ],
-                       "comment_{$prefix}_comment" => [
-                               "LEFT JOIN",
-                               "comment_{$prefix}_comment.comment_id = temp_{$prefix}_comment.revcomment_comment_id",
-                       ],
-               ];
-       }
-
-       protected function getTextQueryFields() {
-               return [
-                       'old_text',
-                       'old_flags',
-               ];
-       }
-
-       protected function getPageQueryFields() {
-               return [
-                       'page_namespace',
-                       'page_title',
-                       'page_id',
-                       'page_latest',
-                       'page_is_redirect',
-                       'page_len',
-               ];
-       }
-
-       protected function getUserQueryFields() {
-               return [
-                       'user_name',
-               ];
-       }
-
-       protected function getContentHandlerQueryFields( $prefix ) {
-               return [
-                       "{$prefix}_content_format",
-                       "{$prefix}_content_model",
-               ];
-       }
-
-       public function provideArchiveQueryInfo() {
-               yield 'MCR, comment, actor' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_NEW,
-                       ],
-                       [
-                               'tables' => [
-                                       'archive',
-                                       'actor_ar_user' => 'actor',
-                                       'comment_ar_comment' => 'comment',
-                               ],
-                               'fields' => array_merge(
-                                       $this->getArchiveQueryFields( false ),
-                                       $this->getNewActorQueryFields( 'ar' ),
-                                       $this->getNewCommentQueryFields( 'ar' )
-                               ),
-                               'joins' => [
-                                       'comment_ar_comment'
-                                               => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
-                                       'actor_ar_user' => [ 'JOIN', 'actor_ar_user.actor_id = ar_actor' ],
-                               ],
-                       ]
-               ];
-               yield 'read-new MCR, comment, actor' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
-                       ],
-                       [
-                               'tables' => [
-                                       'archive',
-                                       'actor_ar_user' => 'actor',
-                                       'comment_ar_comment' => 'comment',
-                               ],
-                               'fields' => array_merge(
-                                       $this->getArchiveQueryFields( false ),
-                                       $this->getCompatActorQueryFields( 'ar' ),
-                                       $this->getCompatCommentQueryFields( 'ar' )
-                               ),
-                               'joins' => [
-                                       'comment_ar_comment'
-                                               => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
-                                       'actor_ar_user' => [ 'LEFT JOIN', 'actor_ar_user.actor_id = ar_actor' ],
-                               ],
-                       ]
-               ];
-               yield 'MCR write-both/read-old' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                       ],
-                       [
-                               'tables' => [
-                                       'archive',
-                                       'actor_ar_user' => 'actor',
-                                       'comment_ar_comment' => 'comment',
-                               ],
-                               'fields' => array_merge(
-                                       $this->getArchiveQueryFields( true ),
-                                       $this->getContentHandlerQueryFields( 'ar' ),
-                                       $this->getCompatActorQueryFields( 'ar' ),
-                                       $this->getCompatCommentQueryFields( 'ar' )
-                               ),
-                               'joins' => [
-                                       'comment_ar_comment'
-                                               => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
-                                       'actor_ar_user' => [ 'LEFT JOIN', 'actor_ar_user.actor_id = ar_actor' ],
-                               ],
-                       ]
-               ];
-               yield 'pre-MCR, no model' => [
-                       [
-                               'wgContentHandlerUseDB' => false,
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       [
-                               'tables' => [
-                                       'archive',
-                               ],
-                               'fields' => array_merge(
-                                       $this->getArchiveQueryFields( true ),
-                                       $this->getOldActorQueryFields( 'ar' ),
-                                       $this->getOldCommentQueryFields( 'ar' )
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-       }
-
-       public function provideQueryInfo() {
-               // TODO: more option variations
-               yield 'MCR, page, user, comment, actor' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_NEW,
-                       ],
-                       [ 'page', 'user' ],
-                       [
-                               'tables' => [
-                                       'revision',
-                                       'page',
-                                       'user',
-                                       'temp_rev_user' => 'revision_actor_temp',
-                                       'temp_rev_comment' => 'revision_comment_temp',
-                                       'actor_rev_user' => 'actor',
-                                       'comment_rev_comment' => 'comment',
-                               ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( false ),
-                                       $this->getPageQueryFields(),
-                                       $this->getUserQueryFields(),
-                                       $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
-                                       $this->getNewCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => [
-                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
-                                       'user' => [
-                                               'LEFT JOIN',
-                                               [ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ],
-                                       ],
-                                       'comment_rev_comment' => [
-                                               'JOIN',
-                                               'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id',
-                                       ],
-                                       'actor_rev_user' => [
-                                               'JOIN',
-                                               'actor_rev_user.actor_id = temp_rev_user.revactor_actor',
-                                       ],
-                                       'temp_rev_user' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
-                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
-                               ],
-                       ]
-               ];
-               yield 'MCR read-new, page, user, comment, actor' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
-                       ],
-                       [ 'page', 'user' ],
-                       [
-                               'tables' => [
-                                       'revision',
-                                       'page',
-                                       'user',
-                                       'temp_rev_user' => 'revision_actor_temp',
-                                       'temp_rev_comment' => 'revision_comment_temp',
-                                       'actor_rev_user' => 'actor',
-                                       'comment_rev_comment' => 'comment',
-                               ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( false ),
-                                       $this->getPageQueryFields(),
-                                       $this->getUserQueryFields(),
-                                       $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
-                                       $this->getCompatCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => array_merge(
-                                       [
-                                               'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
-                                               'user' => [
-                                                       'LEFT JOIN',
-                                                       [
-                                                               'COALESCE( actor_rev_user.actor_user, rev_user ) != 0',
-                                                               'user_id = COALESCE( actor_rev_user.actor_user, rev_user )'
-                                                       ]
-                                               ],
-                                       ],
-                                       $this->getCompatActorJoins( 'rev' ),
-                                       $this->getCompatCommentJoins( 'rev' )
-                               ),
-                       ]
-               ];
-               yield 'MCR read-new' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
-                       ],
-                       [ 'page', 'user' ],
-                       [
-                               'tables' => [
-                                       'revision',
-                                       'page',
-                                       'user',
-                                       'temp_rev_user' => 'revision_actor_temp',
-                                       'temp_rev_comment' => 'revision_comment_temp',
-                                       'actor_rev_user' => 'actor',
-                                       'comment_rev_comment' => 'comment',
-                               ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( false ),
-                                       $this->getPageQueryFields(),
-                                       $this->getUserQueryFields(),
-                                       $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
-                                       $this->getCompatCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => array_merge(
-                                       [
-                                               'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
-                                               'user' => [
-                                                       'LEFT JOIN',
-                                                       [
-                                                               'COALESCE( actor_rev_user.actor_user, rev_user ) != 0',
-                                                               'user_id = COALESCE( actor_rev_user.actor_user, rev_user )'
-                                                       ]
-                                               ],
-                                       ],
-                                       $this->getCompatActorJoins( 'rev' ),
-                                       $this->getCompatCommentJoins( 'rev' )
-                               ),
-                       ]
-               ];
-               yield 'MCR write-both/read-old' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                       ],
-                       [],
-                       [
-                               'tables' => [
-                                       'revision',
-                                       'temp_rev_user' => 'revision_actor_temp',
-                                       'temp_rev_comment' => 'revision_comment_temp',
-                                       'actor_rev_user' => 'actor',
-                                       'comment_rev_comment' => 'comment',
-                               ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( true ),
-                                       $this->getContentHandlerQueryFields( 'rev' ),
-                                       $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
-                                       $this->getCompatCommentQueryFields( 'rev' )
-                               ),
-                       'joins' => array_merge(
-                               $this->getCompatActorJoins( 'rev' ),
-                               $this->getCompatCommentJoins( 'rev' )
-                       ),
-                       ]
-               ];
-               yield 'MCR write-both/read-old, page, user' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                       ],
-                       [ 'page', 'user' ],
-                       [
-                               'tables' => [
-                                       'revision',
-                                       'page',
-                                       'user',
-                                       'temp_rev_user' => 'revision_actor_temp',
-                                       'temp_rev_comment' => 'revision_comment_temp',
-                                       'actor_rev_user' => 'actor',
-                                       'comment_rev_comment' => 'comment',
-                               ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( true ),
-                                       $this->getContentHandlerQueryFields( 'rev' ),
-                                       $this->getUserQueryFields(),
-                                       $this->getPageQueryFields(),
-                                       $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
-                                       $this->getCompatCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => array_merge(
-                                       [
-                                               'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
-                                               'user' => [
-                                                       'LEFT JOIN',
-                                                       [
-                                                               'COALESCE( actor_rev_user.actor_user, rev_user ) != 0',
-                                                               'user_id = COALESCE( actor_rev_user.actor_user, rev_user )'
-                                                       ]
-                                               ],
-                                       ],
-                                       $this->getCompatActorJoins( 'rev' ),
-                                       $this->getCompatCommentJoins( 'rev' )
-                               ),
-                       ]
-               ];
-               yield 'pre-MCR' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       [],
-                       [
-                               'tables' => [ 'revision' ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( true ),
-                                       $this->getContentHandlerQueryFields( 'rev' ),
-                                       $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-               yield 'pre-MCR, page, user' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       [ 'page', 'user' ],
-                       [
-                               'tables' => [ 'revision', 'page', 'user' ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( true ),
-                                       $this->getContentHandlerQueryFields( 'rev' ),
-                                       $this->getPageQueryFields(),
-                                       $this->getUserQueryFields(),
-                                       $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => [
-                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
-                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
-                               ],
-                       ]
-               ];
-               yield 'pre-MCR, no model' => [
-                       [
-                               'wgContentHandlerUseDB' => false,
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       [],
-                       [
-                               'tables' => [ 'revision' ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( true ),
-                                       $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => [],
-                       ],
-               ];
-               yield 'pre-MCR, no model, page' => [
-                       [
-                               'wgContentHandlerUseDB' => false,
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       [ 'page' ],
-                       [
-                               'tables' => [ 'revision', 'page' ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( true ),
-                                       $this->getPageQueryFields(),
-                                       $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => [
-                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ], ],
-                               ],
-                       ],
-               ];
-               yield 'pre-MCR, no model, user' => [
-                       [
-                               'wgContentHandlerUseDB' => false,
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       [ 'user' ],
-                       [
-                               'tables' => [ 'revision', 'user' ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( true ),
-                                       $this->getUserQueryFields(),
-                                       $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => [
-                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
-                               ],
-                       ],
-               ];
-               yield 'pre-MCR, no model, text' => [
-                       [
-                               'wgContentHandlerUseDB' => false,
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       [ 'text' ],
-                       [
-                               'tables' => [ 'revision', 'text' ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( true ),
-                                       $this->getTextQueryFields(),
-                                       $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => [
-                                       'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
-                               ],
-                       ],
-               ];
-               yield 'pre-MCR, no model, text, page, user' => [
-                       [
-                               'wgContentHandlerUseDB' => false,
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       [ 'text', 'page', 'user' ],
-                       [
-                               'tables' => [
-                                       'revision', 'page', 'user', 'text'
-                               ],
-                               'fields' => array_merge(
-                                       $this->getRevisionQueryFields( true ),
-                                       $this->getPageQueryFields(),
-                                       $this->getUserQueryFields(),
-                                       $this->getTextQueryFields(),
-                                       $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
-                               ),
-                               'joins' => [
-                                       'page' => [
-                                               'INNER JOIN',
-                                               [ 'page_id = rev_page' ],
-                                       ],
-                                       'user' => [
-                                               'LEFT JOIN',
-                                               [
-                                                       'rev_user != 0',
-                                                       'user_id = rev_user',
-                                               ],
-                                       ],
-                                       'text' => [
-                                               'INNER JOIN',
-                                               [ 'rev_text_id=old_id' ],
-                                       ],
-                               ],
-                       ],
-               ];
-       }
-
-       public function provideSlotsQueryInfo() {
-               yield 'MCR, no options' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
-                       ],
-                       [],
-                       [
-                               'tables' => [
-                                       'slots'
-                               ],
-                               'fields' => [
-                                       'slot_revision_id',
-                                       'slot_content_id',
-                                       'slot_origin',
-                                       'slot_role_id',
-                               ],
-                               'joins' => [],
-                       ]
-               ];
-               yield 'MCR, role option' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
-                       ],
-                       [ 'role' ],
-                       [
-                               'tables' => [
-                                       'slots',
-                                       'slot_roles',
-                               ],
-                               'fields' => [
-                                       'slot_revision_id',
-                                       'slot_content_id',
-                                       'slot_origin',
-                                       'slot_role_id',
-                                       'role_name',
-                               ],
-                               'joins' => [
-                                       'slot_roles' => [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ],
-                               ],
-                       ]
-               ];
-               yield 'MCR read-new, content option' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
-                       ],
-                       [ 'content' ],
-                       [
-                               'tables' => [
-                                       'slots',
-                                       'content',
-                               ],
-                               'fields' => [
-                                       'slot_revision_id',
-                                       'slot_content_id',
-                                       'slot_origin',
-                                       'slot_role_id',
-                                       'content_size',
-                                       'content_sha1',
-                                       'content_address',
-                                       'content_model',
-                               ],
-                               'joins' => [
-                                       'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
-                               ],
-                       ]
-               ];
-               yield 'MCR read-new, content and model options' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
-                       ],
-                       [ 'content', 'model' ],
-                       [
-                               'tables' => [
-                                       'slots',
-                                       'content',
-                                       'content_models',
-                               ],
-                               'fields' => [
-                                       'slot_revision_id',
-                                       'slot_content_id',
-                                       'slot_origin',
-                                       'slot_role_id',
-                                       'content_size',
-                                       'content_sha1',
-                                       'content_address',
-                                       'content_model',
-                                       'model_name',
-                               ],
-                               'joins' => [
-                                       'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
-                                       'content_models' => [ 'LEFT JOIN', [ 'content_model = model_id' ] ],
-                               ],
-                       ]
-               ];
-
-               $db = wfGetDB( DB_REPLICA );
-
-               yield 'MCR write-both/read-old' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
-                       ],
-                       [],
-                       [
-                               'tables' => [
-                                       'slots' => 'revision',
-                               ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id' => 'slots.rev_id',
-                                               'slot_content_id' => 'NULL',
-                                               'slot_origin' => 'slots.rev_id',
-                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
-                                       ]
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-               yield 'MCR write-both/read-old, content' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
-                       ],
-                       [ 'content' ],
-                       [
-                               'tables' => [
-                                       'slots' => 'revision',
-                               ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id' => 'slots.rev_id',
-                                               'slot_content_id' => 'NULL',
-                                               'slot_origin' => 'slots.rev_id',
-                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
-                                               'content_size' => 'slots.rev_len',
-                                               'content_sha1' => 'slots.rev_sha1',
-                                               'content_address' => $db->buildConcat( [
-                                                       $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
-                                               'model_name' => 'slots.rev_content_model',
-                                       ]
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-               yield 'MCR write-both/read-old, content, model, role' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
-                       ],
-                       [ 'content', 'model', 'role' ],
-                       [
-                               'tables' => [
-                                       'slots' => 'revision',
-                               ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id' => 'slots.rev_id',
-                                               'slot_content_id' => 'NULL',
-                                               'slot_origin' => 'slots.rev_id',
-                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
-                                               'content_size' => 'slots.rev_len',
-                                               'content_sha1' => 'slots.rev_sha1',
-                                               'content_address' => $db->buildConcat( [
-                                                       $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
-                                               'model_name' => 'slots.rev_content_model',
-                                       ]
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-               yield 'pre-MCR' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_OLD,
-                       ],
-                       [],
-                       [
-                               'tables' => [
-                                       'slots' => 'revision',
-                               ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id' => 'slots.rev_id',
-                                               'slot_content_id' => 'NULL',
-                                               'slot_origin' => 'slots.rev_id',
-                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
-                                       ]
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-               yield 'pre-MCR, content' => [
-                       [
-                               'wgMultiContentRevisionSchemaMigrationStage'
-                                       => SCHEMA_COMPAT_OLD,
-                       ],
-                       [ 'content' ],
-                       [
-                               'tables' => [
-                                       'slots' => 'revision',
-                               ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id' => 'slots.rev_id',
-                                               'slot_content_id' => 'NULL',
-                                               'slot_origin' => 'slots.rev_id',
-                                               'role_name' => $db->addQuotes( SlotRecord::MAIN ),
-                                               'content_size' => 'slots.rev_len',
-                                               'content_sha1' => 'slots.rev_sha1',
-                                               'content_address' =>
-                                                       $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
-                                               'model_name' => 'slots.rev_content_model',
-                                       ]
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-       }
-
-       public function provideSelectFields() {
-               yield 'with model, comment, and actor' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                       ],
-                       'fields' => array_merge(
-                               [
-                                       'rev_id',
-                                       'rev_page',
-                                       'rev_text_id',
-                                       'rev_timestamp',
-                                       'rev_user_text',
-                                       'rev_user',
-                                       'rev_actor' => 'NULL',
-                                       'rev_minor_edit',
-                                       'rev_deleted',
-                                       'rev_len',
-                                       'rev_parent_id',
-                                       'rev_sha1',
-                               ],
-                               $this->getContentHandlerQueryFields( 'rev' ),
-                               [
-                                       'rev_comment_old' => 'rev_comment',
-                                       'rev_comment_pk' => 'rev_id',
-                               ]
-                       ),
-               ];
-               yield 'no mode, no comment, no actor' => [
-                       [
-                               'wgContentHandlerUseDB' => false,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       'fields' => array_merge(
-                               [
-                                       'rev_id',
-                                       'rev_page',
-                                       'rev_text_id',
-                                       'rev_timestamp',
-                                       'rev_user_text',
-                                       'rev_user',
-                                       'rev_actor' => 'NULL',
-                                       'rev_minor_edit',
-                                       'rev_deleted',
-                                       'rev_len',
-                                       'rev_parent_id',
-                                       'rev_sha1',
-                               ],
-                               $this->getOldCommentQueryFields( 'rev' )
-                       ),
-               ];
-       }
-
-       public function provideSelectArchiveFields() {
-               yield 'with model, comment, and actor' => [
-                       [
-                               'wgContentHandlerUseDB' => true,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
-                       ],
-                       'fields' => array_merge(
-                               [
-                                       'ar_id',
-                                       'ar_page_id',
-                                       'ar_rev_id',
-                                       'ar_text_id',
-                                       'ar_timestamp',
-                                       'ar_user_text',
-                                       'ar_user',
-                                       'ar_actor' => 'NULL',
-                                       'ar_minor_edit',
-                                       'ar_deleted',
-                                       'ar_len',
-                                       'ar_parent_id',
-                                       'ar_sha1',
-                               ],
-                               $this->getContentHandlerQueryFields( 'ar' ),
-                               [
-                                       'ar_comment_old' => 'ar_comment',
-                                       'ar_comment_id' => 'ar_comment_id',
-                               ]
-                       ),
-               ];
-               yield 'no mode, no comment, no actor' => [
-                       [
-                               'wgContentHandlerUseDB' => false,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                               'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-                       ],
-                       'fields' => array_merge(
-                               [
-                                       'ar_id',
-                                       'ar_page_id',
-                                       'ar_rev_id',
-                                       'ar_text_id',
-                                       'ar_timestamp',
-                                       'ar_user_text',
-                                       'ar_user',
-                                       'ar_actor' => 'NULL',
-                                       'ar_minor_edit',
-                                       'ar_deleted',
-                                       'ar_len',
-                                       'ar_parent_id',
-                                       'ar_sha1',
-                               ],
-                               $this->getOldCommentQueryFields( 'ar' )
-                       ),
-               ];
-       }
-
-       /**
-        * @dataProvider provideSelectFields
-        * @covers Revision::selectFields
-        */
-       public function testRevisionSelectFields( $migrationStageSettings, $expected ) {
-               $this->setMwGlobals( $migrationStageSettings );
-               $this->overrideMwServices();
-
-               $this->hideDeprecated( 'Revision::selectFields' );
-               $this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectFields() );
-       }
-
-       /**
-        * @dataProvider provideSelectArchiveFields
-        * @covers Revision::selectArchiveFields
-        */
-       public function testRevisionSelectArchiveFields( $migrationStageSettings, $expected ) {
-               $this->setMwGlobals( $migrationStageSettings );
-               $this->overrideMwServices();
-
-               $this->hideDeprecated( 'Revision::selectArchiveFields' );
-               $this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectArchiveFields() );
-       }
-
-       /**
-        * @covers Revision::userJoinCond
-        */
-       public function testRevisionUserJoinCond() {
-               $this->hideDeprecated( 'Revision::userJoinCond' );
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
-               $this->overrideMwServices();
-               $this->assertEquals(
-                       [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
-                       Revision::userJoinCond()
-               );
-       }
-
-       /**
-        * @covers Revision::pageJoinCond
-        */
-       public function testRevisionPageJoinCond() {
-               $this->hideDeprecated( 'Revision::pageJoinCond' );
-               $this->assertEquals(
-                       [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
-                       Revision::pageJoinCond()
-               );
-       }
-
-       /**
-        * @covers Revision::selectTextFields
-        */
-       public function testRevisionSelectTextFields() {
-               $this->hideDeprecated( 'Revision::selectTextFields' );
-               $this->assertEquals(
-                       $this->getTextQueryFields(),
-                       Revision::selectTextFields()
-               );
-       }
-
-       /**
-        * @covers Revision::selectPageFields
-        */
-       public function testRevisionSelectPageFields() {
-               $this->hideDeprecated( 'Revision::selectPageFields' );
-               $this->assertEquals(
-                       $this->getPageQueryFields(),
-                       Revision::selectPageFields()
-               );
-       }
-
-       /**
-        * @covers Revision::selectUserFields
-        */
-       public function testRevisionSelectUserFields() {
-               $this->hideDeprecated( 'Revision::selectUserFields' );
-               $this->assertEquals(
-                       $this->getUserQueryFields(),
-                       Revision::selectUserFields()
-               );
-       }
-
-       /**
-        * @covers Revision::getArchiveQueryInfo
-        * @dataProvider provideArchiveQueryInfo
-        */
-       public function testRevisionGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
-               $this->setMwGlobals( $migrationStageSettings );
-               $this->overrideMwServices();
-
-               $queryInfo = Revision::getArchiveQueryInfo();
-               $this->assertQueryInfoEquals( $expected, $queryInfo );
-       }
-
-       /**
-        * @covers Revision::getQueryInfo
-        * @dataProvider provideQueryInfo
-        */
-       public function testRevisionGetQueryInfo( $migrationStageSettings, $options, $expected ) {
-               $this->setMwGlobals( $migrationStageSettings );
-               $this->overrideMwServices();
-
-               $queryInfo = Revision::getQueryInfo( $options );
-               $this->assertQueryInfoEquals( $expected, $queryInfo );
-       }
-
-       /**
-        * @dataProvider provideQueryInfo
-        * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
-        */
-       public function testRevisionStoreGetQueryInfo( $migrationStageSettings, $options, $expected ) {
-               $this->setMwGlobals( $migrationStageSettings );
-               $this->overrideMwServices();
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-
-               $queryInfo = $store->getQueryInfo( $options );
-               $this->assertQueryInfoEquals( $expected, $queryInfo );
-       }
-
-       /**
-        * @dataProvider provideSlotsQueryInfo
-        * @covers \MediaWiki\Storage\RevisionStore::getSlotsQueryInfo
-        */
-       public function testRevisionStoreGetSlotsQueryInfo(
-               $migrationStageSettings,
-               $options,
-               $expected
-       ) {
-               $this->setMwGlobals( $migrationStageSettings );
-               $this->overrideMwServices();
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-
-               $queryInfo = $store->getSlotsQueryInfo( $options );
-               $this->assertQueryInfoEquals( $expected, $queryInfo );
-       }
-
-       /**
-        * @dataProvider provideArchiveQueryInfo
-        * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
-        */
-       public function testRevisionStoreGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
-               $this->setMwGlobals( $migrationStageSettings );
-               $this->overrideMwServices();
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-
-               $queryInfo = $store->getArchiveQueryInfo();
-               $this->assertQueryInfoEquals( $expected, $queryInfo );
-       }
-
-       private function assertQueryInfoEquals( $expected, $queryInfo ) {
-               $this->assertArrayEqualsIgnoringIntKeyOrder(
-                       $expected['tables'],
-                       $queryInfo['tables'],
-                       'tables'
-               );
-               $this->assertArrayEqualsIgnoringIntKeyOrder(
-                       $expected['fields'],
-                       $queryInfo['fields'],
-                       'fields'
-               );
-               $this->assertArrayEqualsIgnoringIntKeyOrder(
-                       $expected['joins'],
-                       $queryInfo['joins'],
-                       'joins'
-               );
-       }
-
-       /**
-        * Assert that the two arrays passed are equal, ignoring the order of the values that integer
-        * keys.
-        *
-        * Note: Failures of this assertion can be slightly confusing as the arrays are actually
-        * split into a string key array and an int key array before assertions occur.
-        *
-        * @param array $expected
-        * @param array $actual
-        */
-       private function assertArrayEqualsIgnoringIntKeyOrder(
-               array $expected,
-               array $actual,
-               $message = null
-       ) {
-               $this->objectAssociativeSort( $expected );
-               $this->objectAssociativeSort( $actual );
-
-               // Separate the int key values from the string key values so that assertion failures are
-               // easier to understand.
-               $expectedIntKeyValues = [];
-               $actualIntKeyValues = [];
-
-               // Remove all int keys and re add them at the end after sorting by value
-               // This will result in all int keys being in the same order with same ints at the end of
-               // the array
-               foreach ( $expected as $key => $value ) {
-                       if ( is_int( $key ) ) {
-                               unset( $expected[$key] );
-                               $expectedIntKeyValues[] = $value;
-                       }
-               }
-               foreach ( $actual as $key => $value ) {
-                       if ( is_int( $key ) ) {
-                               unset( $actual[$key] );
-                               $actualIntKeyValues[] = $value;
-                       }
-               }
-
-               $this->objectAssociativeSort( $expected );
-               $this->objectAssociativeSort( $actual );
-
-               $this->objectAssociativeSort( $expectedIntKeyValues );
-               $this->objectAssociativeSort( $actualIntKeyValues );
-
-               $this->assertEquals( $expected, $actual, $message );
-               $this->assertEquals( $expectedIntKeyValues, $actualIntKeyValues, $message );
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/RevisionRecordTests.php b/tests/phpunit/includes/Storage/RevisionRecordTests.php
deleted file mode 100644 (file)
index 901b800..0000000
+++ /dev/null
@@ -1,528 +0,0 @@
-<?php
-
-// phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotClassTrait
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use LogicException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SuppressedDataException;
-use MediaWiki\User\UserIdentityValue;
-use TextContent;
-use Title;
-
-/**
- * @covers \MediaWiki\Storage\RevisionRecord
- *
- * @note Expects to be used in classes that extend MediaWikiTestCase.
- */
-trait RevisionRecordTests {
-
-       /**
-        * @param array $rowOverrides
-        *
-        * @return RevisionRecord
-        */
-       protected abstract function newRevision( array $rowOverrides = [] );
-
-       private function provideAudienceCheckData( $field ) {
-               yield 'field accessible for oversighter (ALL)' => [
-                       RevisionRecord::SUPPRESSED_ALL,
-                       [ 'oversight' ],
-                       true,
-                       false
-               ];
-
-               yield 'field accessible for oversighter' => [
-                       RevisionRecord::DELETED_RESTRICTED | $field,
-                       [ 'oversight' ],
-                       true,
-                       false
-               ];
-
-               yield 'field not accessible for sysops (ALL)' => [
-                       RevisionRecord::SUPPRESSED_ALL,
-                       [ 'sysop' ],
-                       false,
-                       false
-               ];
-
-               yield 'field not accessible for sysops' => [
-                       RevisionRecord::DELETED_RESTRICTED | $field,
-                       [ 'sysop' ],
-                       false,
-                       false
-               ];
-
-               yield 'field accessible for sysops' => [
-                       $field,
-                       [ 'sysop' ],
-                       true,
-                       false
-               ];
-
-               yield 'field suppressed for logged in users' => [
-                       $field,
-                       [ 'user' ],
-                       false,
-                       false
-               ];
-
-               yield 'unrelated field suppressed' => [
-                       $field === RevisionRecord::DELETED_COMMENT
-                               ? RevisionRecord::DELETED_USER
-                               : RevisionRecord::DELETED_COMMENT,
-                       [ 'user' ],
-                       true,
-                       true
-               ];
-
-               yield 'nothing suppressed' => [
-                       0,
-                       [ 'user' ],
-                       true,
-                       true
-               ];
-       }
-
-       public function testSerialization_fails() {
-               $this->setExpectedException( LogicException::class );
-               $rev = $this->newRevision();
-               serialize( $rev );
-       }
-
-       public function provideGetComment_audience() {
-               return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT );
-       }
-
-       private function forceStandardPermissions() {
-               $this->setMwGlobals(
-                       'wgGroupPermissions',
-                       [
-                               'user' => [
-                                       'viewsuppressed' => false,
-                                       'suppressrevision' => false,
-                                       'deletedtext' => false,
-                                       'deletedhistory' => false,
-                               ],
-                               'sysop' => [
-                                       'viewsuppressed' => false,
-                                       'suppressrevision' => false,
-                                       'deletedtext' => true,
-                                       'deletedhistory' => true,
-                               ],
-                               'oversight' => [
-                                       'deletedtext' => true,
-                                       'deletedhistory' => true,
-                                       'viewsuppressed' => true,
-                                       'suppressrevision' => true,
-                               ],
-                       ]
-               );
-       }
-
-       /**
-        * @dataProvider provideGetComment_audience
-        */
-       public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
-               $this->forceStandardPermissions();
-
-               $user = $this->getTestUser( $groups )->getUser();
-               $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
-
-               $this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' );
-
-               $this->assertSame(
-                       $publicCan,
-                       $rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null,
-                       'public can'
-               );
-               $this->assertSame(
-                       $userCan,
-                       $rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null,
-                       'user can'
-               );
-       }
-
-       public function provideGetUser_audience() {
-               return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER );
-       }
-
-       /**
-        * @dataProvider provideGetUser_audience
-        */
-       public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
-               $this->forceStandardPermissions();
-
-               $user = $this->getTestUser( $groups )->getUser();
-               $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
-
-               $this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' );
-
-               $this->assertSame(
-                       $publicCan,
-                       $rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null,
-                       'public can'
-               );
-               $this->assertSame(
-                       $userCan,
-                       $rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null,
-                       'user can'
-               );
-       }
-
-       public function provideGetSlot_audience() {
-               return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
-       }
-
-       /**
-        * @dataProvider provideGetSlot_audience
-        */
-       public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
-               $this->forceStandardPermissions();
-
-               $user = $this->getTestUser( $groups )->getUser();
-               $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
-
-               // NOTE: slot meta-data is never suppressed, just the content is!
-               $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ), 'hasSlot is never suppressed' );
-               $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw meta' );
-               $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ),
-                       'public meta' );
-
-               $this->assertNotNull(
-                       $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ),
-                       'user can'
-               );
-
-               try {
-                       $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC )->getContent();
-                       $exception = null;
-               } catch ( SuppressedDataException $ex ) {
-                       $exception = $ex;
-               }
-
-               $this->assertSame(
-                       $publicCan,
-                       $exception === null,
-                       'public can'
-               );
-
-               try {
-                       $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )->getContent();
-                       $exception = null;
-               } catch ( SuppressedDataException $ex ) {
-                       $exception = $ex;
-               }
-
-               $this->assertSame(
-                       $userCan,
-                       $exception === null,
-                       'user can'
-               );
-       }
-
-       /**
-        * @dataProvider provideGetSlot_audience
-        */
-       public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
-               $this->forceStandardPermissions();
-
-               $user = $this->getTestUser( $groups )->getUser();
-               $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
-
-               $this->assertNotNull( $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw can' );
-
-               $this->assertSame(
-                       $publicCan,
-                       $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ) !== null,
-                       'public can'
-               );
-               $this->assertSame(
-                       $userCan,
-                       $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ) !== null,
-                       'user can'
-               );
-       }
-
-       public function testGetSlot() {
-               $rev = $this->newRevision();
-
-               $slot = $rev->getSlot( SlotRecord::MAIN );
-               $this->assertNotNull( $slot, 'getSlot()' );
-               $this->assertSame( 'main', $slot->getRole(), 'getRole()' );
-       }
-
-       public function testHasSlot() {
-               $rev = $this->newRevision();
-
-               $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ) );
-               $this->assertFalse( $rev->hasSlot( 'xyz' ) );
-       }
-
-       public function testGetContent() {
-               $rev = $this->newRevision();
-
-               $content = $rev->getSlot( SlotRecord::MAIN );
-               $this->assertNotNull( $content, 'getContent()' );
-               $this->assertSame( CONTENT_MODEL_TEXT, $content->getModel(), 'getModel()' );
-       }
-
-       public function provideUserCanBitfield() {
-               yield [ 0, 0, [], null, true ];
-               // Bitfields match, user has no permissions
-               yield [
-                       RevisionRecord::DELETED_TEXT,
-                       RevisionRecord::DELETED_TEXT,
-                       [],
-                       null,
-                       false
-               ];
-               yield [
-                       RevisionRecord::DELETED_COMMENT,
-                       RevisionRecord::DELETED_COMMENT,
-                       [],
-                       null,
-                       false,
-               ];
-               yield [
-                       RevisionRecord::DELETED_USER,
-                       RevisionRecord::DELETED_USER,
-                       [],
-                       null,
-                       false
-               ];
-               yield [
-                       RevisionRecord::DELETED_RESTRICTED,
-                       RevisionRecord::DELETED_RESTRICTED,
-                       [],
-                       null,
-                       false,
-               ];
-               // Bitfields match, user (admin) does have permissions
-               yield [
-                       RevisionRecord::DELETED_TEXT,
-                       RevisionRecord::DELETED_TEXT,
-                       [ 'sysop' ],
-                       null,
-                       true,
-               ];
-               yield [
-                       RevisionRecord::DELETED_COMMENT,
-                       RevisionRecord::DELETED_COMMENT,
-                       [ 'sysop' ],
-                       null,
-                       true,
-               ];
-               yield [
-                       RevisionRecord::DELETED_USER,
-                       RevisionRecord::DELETED_USER,
-                       [ 'sysop' ],
-                       null,
-                       true,
-               ];
-               // Bitfields match, user (admin) does not have permissions
-               yield [
-                       RevisionRecord::DELETED_RESTRICTED,
-                       RevisionRecord::DELETED_RESTRICTED,
-                       [ 'sysop' ],
-                       null,
-                       false,
-               ];
-               // Bitfields match, user (oversight) does have permissions
-               yield [
-                       RevisionRecord::DELETED_RESTRICTED,
-                       RevisionRecord::DELETED_RESTRICTED,
-                       [ 'oversight' ],
-                       null,
-                       true,
-               ];
-               // Check permissions using the title
-               yield [
-                       RevisionRecord::DELETED_TEXT,
-                       RevisionRecord::DELETED_TEXT,
-                       [ 'sysop' ],
-                       __METHOD__,
-                       true,
-               ];
-               yield [
-                       RevisionRecord::DELETED_TEXT,
-                       RevisionRecord::DELETED_TEXT,
-                       [],
-                       __METHOD__,
-                       false,
-               ];
-       }
-
-       /**
-        * @dataProvider provideUserCanBitfield
-        * @covers \MediaWiki\Storage\RevisionRecord::userCanBitfield
-        */
-       public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
-               if ( is_string( $title ) ) {
-                       // NOTE: Data providers cannot instantiate Title objects! See T202641.
-                       $title = Title::newFromText( $title );
-               }
-
-               $this->forceStandardPermissions();
-
-               $user = $this->getTestUser( $userGroups )->getUser();
-
-               $this->assertSame(
-                       $expected,
-                       RevisionRecord::userCanBitfield( $bitField, $field, $user, $title )
-               );
-       }
-
-       public function provideHasSameContent() {
-               // Create some slots with content
-               $mainA = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'A' ) );
-               $mainB = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'B' ) );
-               $auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
-               $auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
-
-               $initialRecordSpec = [ [ $mainA ], 12 ];
-
-               return [
-                       'same record object' => [
-                               true,
-                               $initialRecordSpec,
-                               $initialRecordSpec,
-                       ],
-                       'same record content, different object' => [
-                               true,
-                               [ [ $mainA ], 12 ],
-                               [ [ $mainA ], 13 ],
-                       ],
-                       'same record content, aux slot, different object' => [
-                               true,
-                               [ [ $auxA ], 12 ],
-                               [ [ $auxB ], 13 ],
-                       ],
-                       'different content' => [
-                               false,
-                               [ [ $mainA ], 12 ],
-                               [ [ $mainB ], 13 ],
-                       ],
-                       'different content and number of slots' => [
-                               false,
-                               [ [ $mainA ], 12 ],
-                               [ [ $mainA, $mainB ], 13 ],
-                       ],
-               ];
-       }
-
-       /**
-        * @note Do not call directly from a data provider! Data providers cannot instantiate
-        * Title objects! See T202641.
-        *
-        * @param SlotRecord[] $slots
-        * @param int $revId
-        * @return RevisionStoreRecord
-        */
-       private function makeHasSameContentTestRecord( array $slots, $revId ) {
-               $title = Title::newFromText( 'provideHasSameContent' );
-               $title->resetArticleID( 19 );
-               $slots = new RevisionSlots( $slots );
-
-               return new RevisionStoreRecord(
-                       $title,
-                       new UserIdentityValue( 11, __METHOD__, 0 ),
-                       CommentStoreComment::newUnsavedComment( __METHOD__ ),
-                       (object)[
-                               'rev_id' => strval( $revId ),
-                               'rev_page' => strval( $title->getArticleID() ),
-                               'rev_timestamp' => '20200101000000',
-                               'rev_deleted' => 0,
-                               'rev_minor_edit' => 0,
-                               'rev_parent_id' => '5',
-                               'rev_len' => $slots->computeSize(),
-                               'rev_sha1' => $slots->computeSha1(),
-                               'page_latest' => '18',
-                       ],
-                       $slots
-               );
-       }
-
-       /**
-        * @dataProvider provideHasSameContent
-        * @covers \MediaWiki\Storage\RevisionRecord::hasSameContent
-        * @group Database
-        */
-       public function testHasSameContent(
-               $expected,
-               $recordSpec1,
-               $recordSpec2
-       ) {
-               $record1 = $this->makeHasSameContentTestRecord( ...$recordSpec1 );
-               $record2 = $this->makeHasSameContentTestRecord( ...$recordSpec2 );
-
-               $this->assertSame(
-                       $expected,
-                       $record1->hasSameContent( $record2 )
-               );
-       }
-
-       public function provideIsDeleted() {
-               yield 'no deletion' => [
-                       0,
-                       [
-                               RevisionRecord::DELETED_TEXT => false,
-                               RevisionRecord::DELETED_COMMENT => false,
-                               RevisionRecord::DELETED_USER => false,
-                               RevisionRecord::DELETED_RESTRICTED => false,
-                       ]
-               ];
-               yield 'text deleted' => [
-                       RevisionRecord::DELETED_TEXT,
-                       [
-                               RevisionRecord::DELETED_TEXT => true,
-                               RevisionRecord::DELETED_COMMENT => false,
-                               RevisionRecord::DELETED_USER => false,
-                               RevisionRecord::DELETED_RESTRICTED => false,
-                       ]
-               ];
-               yield 'text and comment deleted' => [
-                       RevisionRecord::DELETED_TEXT + RevisionRecord::DELETED_COMMENT,
-                       [
-                               RevisionRecord::DELETED_TEXT => true,
-                               RevisionRecord::DELETED_COMMENT => true,
-                               RevisionRecord::DELETED_USER => false,
-                               RevisionRecord::DELETED_RESTRICTED => false,
-                       ]
-               ];
-               yield 'all 4 deleted' => [
-                       RevisionRecord::DELETED_TEXT +
-                       RevisionRecord::DELETED_COMMENT +
-                       RevisionRecord::DELETED_RESTRICTED +
-                       RevisionRecord::DELETED_USER,
-                       [
-                               RevisionRecord::DELETED_TEXT => true,
-                               RevisionRecord::DELETED_COMMENT => true,
-                               RevisionRecord::DELETED_USER => true,
-                               RevisionRecord::DELETED_RESTRICTED => true,
-                       ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideIsDeleted
-        * @covers \MediaWiki\Storage\RevisionRecord::isDeleted
-        */
-       public function testIsDeleted( $revDeleted, $assertionMap ) {
-               $rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] );
-               foreach ( $assertionMap as $deletionLevel => $expected ) {
-                       $this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) );
-               }
-       }
-
-       public function testIsReadyForInsertion() {
-               $rev = $this->newRevision();
-               $this->assertTrue( $rev->isReadyForInsertion() );
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/RevisionSlotsTest.php b/tests/phpunit/includes/Storage/RevisionSlotsTest.php
deleted file mode 100644 (file)
index 409e002..0000000
+++ /dev/null
@@ -1,257 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use InvalidArgumentException;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\SlotRecord;
-use MediaWikiTestCase;
-use TextContent;
-use WikitextContent;
-
-class RevisionSlotsTest extends MediaWikiTestCase {
-
-       /**
-        * @param SlotRecord[] $slots
-        * @return RevisionSlots
-        */
-       protected function newRevisionSlots( $slots = [] ) {
-               return new RevisionSlots( $slots );
-       }
-
-       public function provideConstructorFailue() {
-               yield 'not an array or callable' => [
-                       'foo'
-               ];
-               yield 'array of the wrong thing' => [
-                       [ 1, 2, 3 ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructorFailue
-        * @param $slots
-        *
-        * @covers \MediaWiki\Storage\RevisionSlots::__construct
-        * @covers \MediaWiki\Storage\RevisionSlots::setSlotsInternal
-        */
-       public function testConstructorFailue( $slots ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-
-               new RevisionSlots( $slots );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionSlots::getSlot
-        */
-       public function testGetSlot() {
-               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
-               $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
-
-               $this->assertSame( $mainSlot, $slots->getSlot( SlotRecord::MAIN ) );
-               $this->assertSame( $auxSlot, $slots->getSlot( 'aux' ) );
-               $this->setExpectedException( RevisionAccessException::class );
-               $slots->getSlot( 'nothere' );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionSlots::hasSlot
-        */
-       public function testHasSlot() {
-               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
-               $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
-
-               $this->assertTrue( $slots->hasSlot( SlotRecord::MAIN ) );
-               $this->assertTrue( $slots->hasSlot( 'aux' ) );
-               $this->assertFalse( $slots->hasSlot( 'AUX' ) );
-               $this->assertFalse( $slots->hasSlot( 'xyz' ) );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionSlots::getContent
-        */
-       public function testGetContent() {
-               $mainContent = new WikitextContent( 'A' );
-               $auxContent = new WikitextContent( 'B' );
-               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, $mainContent );
-               $auxSlot = SlotRecord::newUnsaved( 'aux', $auxContent );
-               $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
-
-               $this->assertSame( $mainContent, $slots->getContent( SlotRecord::MAIN ) );
-               $this->assertSame( $auxContent, $slots->getContent( 'aux' ) );
-               $this->setExpectedException( RevisionAccessException::class );
-               $slots->getContent( 'nothere' );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionSlots::getSlotRoles
-        */
-       public function testGetSlotRoles_someSlots() {
-               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
-               $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
-
-               $this->assertSame( [ 'main', 'aux' ], $slots->getSlotRoles() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionSlots::getSlotRoles
-        */
-       public function testGetSlotRoles_noSlots() {
-               $slots = $this->newRevisionSlots( [] );
-
-               $this->assertSame( [], $slots->getSlotRoles() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionSlots::getSlots
-        */
-       public function testGetSlots() {
-               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
-               $slotsArray = [ $mainSlot, $auxSlot ];
-               $slots = $this->newRevisionSlots( $slotsArray );
-
-               $this->assertEquals( [ 'main' => $mainSlot, 'aux' => $auxSlot ], $slots->getSlots() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionSlots::getInheritedSlots
-        */
-       public function testGetInheritedSlots() {
-               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $auxSlot = SlotRecord::newInherited(
-                       SlotRecord::newSaved(
-                               7, 7, 'foo',
-                               SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
-                       )
-               );
-               $slotsArray = [ $mainSlot, $auxSlot ];
-               $slots = $this->newRevisionSlots( $slotsArray );
-
-               $this->assertEquals( [ 'aux' => $auxSlot ], $slots->getInheritedSlots() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionSlots::getOriginalSlots
-        */
-       public function testGetOriginalSlots() {
-               $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $auxSlot = SlotRecord::newInherited(
-                       SlotRecord::newSaved(
-                               7, 7, 'foo',
-                               SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
-                       )
-               );
-               $slotsArray = [ $mainSlot, $auxSlot ];
-               $slots = $this->newRevisionSlots( $slotsArray );
-
-               $this->assertEquals( [ 'main' => $mainSlot ], $slots->getOriginalSlots() );
-       }
-
-       public function provideComputeSize() {
-               yield [ 1, [ 'A' ] ];
-               yield [ 2, [ 'AA' ] ];
-               yield [ 4, [ 'AA', 'X', 'H' ] ];
-       }
-
-       /**
-        * @dataProvider provideComputeSize
-        * @covers \MediaWiki\Storage\RevisionSlots::computeSize
-        */
-       public function testComputeSize( $expected, $contentStrings ) {
-               $slotsArray = [];
-               foreach ( $contentStrings as $key => $contentString ) {
-                       $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) );
-               }
-               $slots = $this->newRevisionSlots( $slotsArray );
-
-               $this->assertSame( $expected, $slots->computeSize() );
-       }
-
-       public function provideComputeSha1() {
-               yield [ 'ctqm7794fr2dp1taki8a88ovwnvmnmj', [ 'A' ] ];
-               yield [ 'eyq8wiwlcofnaiy4eid97gyfy60uw51', [ 'AA' ] ];
-               yield [ 'lavctqfpxartyjr31f853drgfl4kj1g', [ 'AA', 'X', 'H' ] ];
-       }
-
-       /**
-        * @dataProvider provideComputeSha1
-        * @covers \MediaWiki\Storage\RevisionSlots::computeSha1
-        * @note this test is a bit brittle as the hashes are hardcoded, perhaps just check that strings
-        *       are returned and different Slots objects return different strings?
-        */
-       public function testComputeSha1( $expected, $contentStrings ) {
-               $slotsArray = [];
-               foreach ( $contentStrings as $key => $contentString ) {
-                       $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) );
-               }
-               $slots = $this->newRevisionSlots( $slotsArray );
-
-               $this->assertSame( $expected, $slots->computeSha1() );
-       }
-
-       public function provideHasSameContent() {
-               $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) );
-               $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) );
-               $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) );
-               $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ );
-               $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) );
-
-               $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
-               $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
-               $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] );
-               $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] );
-               $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] );
-
-               yield 'same instance' => [ $a, $a, true ];
-               yield 'same slots' => [ $a, $a2, true ];
-               yield 'same content' => [ $a, $a3, true ];
-
-               yield 'different roles' => [ $a, $b, false ];
-               yield 'different content' => [ $a, $c, false ];
-       }
-
-       /**
-        * @dataProvider provideHasSameContent
-        * @covers \MediaWiki\Storage\RevisionSlots::hasSameContent
-        */
-       public function testHasSameContent( RevisionSlots $a, RevisionSlots $b, $same ) {
-               $this->assertSame( $same, $a->hasSameContent( $b ) );
-               $this->assertSame( $same, $b->hasSameContent( $a ) );
-       }
-
-       public function provideGetRolesWithDifferentContent() {
-               $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) );
-               $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) );
-               $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) );
-               $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ );
-               $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) );
-
-               $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
-               $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
-               $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] );
-               $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] );
-               $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] );
-
-               yield 'same instance' => [ $a, $a, [] ];
-               yield 'same slots' => [ $a, $a2, [] ];
-               yield 'same content' => [ $a, $a3, [] ];
-
-               yield 'different roles' => [ $a, $b, [ 'x', 'y' ] ];
-               yield 'different content' => [ $a, $c, [ 'z' ] ];
-       }
-
-       /**
-        * @dataProvider provideGetRolesWithDifferentContent
-        * @covers \MediaWiki\Storage\RevisionSlots::getRolesWithDifferentContent
-        */
-       public function testGetRolesWithDifferentContent( RevisionSlots $a, RevisionSlots $b, $roles ) {
-               $this->assertArrayEquals( $roles, $a->getRolesWithDifferentContent( $b ) );
-               $this->assertArrayEquals( $roles, $b->getRolesWithDifferentContent( $a ) );
-       }
-
-}
index 75a4718..442f4d2 100644 (file)
@@ -3,11 +3,11 @@
 namespace MediaWiki\Tests\Storage;
 
 use Content;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionSlots;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\SlotRecord;
 use MediaWikiTestCase;
 use WikitextContent;
 
diff --git a/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php
deleted file mode 100644 (file)
index 04b6aa8..0000000
+++ /dev/null
@@ -1,1662 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use Content;
-use Exception;
-use HashBagOStuff;
-use InvalidArgumentException;
-use Language;
-use MediaWiki\Linker\LinkTarget;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\IncompleteRevisionException;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiTestCase;
-use PHPUnit_Framework_MockObject_MockObject;
-use Revision;
-use TestUserRegistry;
-use Title;
-use WANObjectCache;
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\DatabaseSqlite;
-use Wikimedia\Rdbms\FakeResultWrapper;
-use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\Rdbms\TransactionProfiler;
-use WikiPage;
-use WikitextContent;
-
-/**
- * @group Database
- * @group RevisionStore
- */
-abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
-
-       /**
-        * @var Title
-        */
-       private $testPageTitle;
-
-       /**
-        * @var WikiPage
-        */
-       private $testPage;
-
-       /**
-        * @return int
-        */
-       abstract protected function getMcrMigrationStage();
-
-       /**
-        * @return bool
-        */
-       protected function getContentHandlerUseDB() {
-               return true;
-       }
-
-       /**
-        * @return string[]
-        */
-       abstract protected function getMcrTablesToReset();
-
-       public function setUp() {
-               parent::setUp();
-               $this->tablesUsed[] = 'archive';
-               $this->tablesUsed[] = 'page';
-               $this->tablesUsed[] = 'revision';
-               $this->tablesUsed[] = 'comment';
-
-               $this->tablesUsed += $this->getMcrTablesToReset();
-
-               $this->setMwGlobals( [
-                       'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
-                       'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
-                       'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
-                       'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
-               ] );
-
-               $this->overrideMwServices();
-       }
-
-       protected function addCoreDBData() {
-               // Blank out. This would fail with a modified schema, and we don't need it.
-       }
-
-       /**
-        * @return Title
-        */
-       protected function getTestPageTitle() {
-               if ( $this->testPageTitle ) {
-                       return $this->testPageTitle;
-               }
-
-               $this->testPageTitle = Title::newFromText( 'UTPage-' . __CLASS__ );
-               return $this->testPageTitle;
-       }
-       /**
-        * @return WikiPage
-        */
-       protected function getTestPage() {
-               if ( $this->testPage ) {
-                       return $this->testPage;
-               }
-
-               $title = $this->getTestPageTitle();
-               $this->testPage = WikiPage::factory( $title );
-
-               if ( !$this->testPage->exists() ) {
-                       // Make sure we don't write to the live db.
-                       $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
-
-                       $user = static::getTestSysop()->getUser();
-
-                       $this->testPage->doEditContent(
-                               new WikitextContent( 'UTContent-' . __CLASS__ ),
-                               'UTPageSummary-' . __CLASS__,
-                               EDIT_NEW | EDIT_SUPPRESS_RC,
-                               false,
-                               $user
-                       );
-               }
-
-               return $this->testPage;
-       }
-
-       /**
-        * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
-        */
-       private function getLoadBalancerMock( array $server ) {
-               $lb = $this->getMockBuilder( LoadBalancer::class )
-                       ->setMethods( [ 'reallyOpenConnection' ] )
-                       ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] )
-                       ->getMock();
-
-               $lb->method( 'reallyOpenConnection' )->willReturnCallback(
-                       function ( array $server, $dbNameOverride ) {
-                               return $this->getDatabaseMock( $server );
-                       }
-               );
-
-               return $lb;
-       }
-
-       /**
-        * @return Database|PHPUnit_Framework_MockObject_MockObject
-        */
-       private function getDatabaseMock( array $params ) {
-               $db = $this->getMockBuilder( DatabaseSqlite::class )
-                       ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] )
-                       ->setConstructorArgs( [ $params ] )
-                       ->getMock();
-
-               $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) );
-               $db->method( 'isOpen' )->willReturn( true );
-
-               return $db;
-       }
-
-       public function provideDomainCheck() {
-               yield [ false, 'test', '' ];
-               yield [ 'test', 'test', '' ];
-
-               yield [ false, 'test', 'foo_' ];
-               yield [ 'test-foo_', 'test', 'foo_' ];
-
-               yield [ false, 'dash-test', '' ];
-               yield [ 'dash-test', 'dash-test', '' ];
-
-               yield [ false, 'underscore_test', 'foo_' ];
-               yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ];
-       }
-
-       /**
-        * @dataProvider provideDomainCheck
-        * @covers \MediaWiki\Storage\RevisionStore::checkDatabaseWikiId
-        */
-       public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) {
-               $this->setMwGlobals(
-                       [
-                               'wgDBname' => $dbName,
-                               'wgDBprefix' => $dbPrefix,
-                       ]
-               );
-
-               $loadBalancer = $this->getLoadBalancerMock(
-                       [
-                               'host' => '*dummy*',
-                               'dbDirectory' => '*dummy*',
-                               'user' => 'test',
-                               'password' => 'test',
-                               'flags' => 0,
-                               'variables' => [],
-                               'schema' => '',
-                               'cliMode' => true,
-                               'agent' => '',
-                               'load' => 100,
-                               'profiler' => null,
-                               'trxProfiler' => new TransactionProfiler(),
-                               'connLogger' => new \Psr\Log\NullLogger(),
-                               'queryLogger' => new \Psr\Log\NullLogger(),
-                               'errorLogger' => function () {
-                               },
-                               'deprecationLogger' => function () {
-                               },
-                               'type' => 'test',
-                               'dbname' => $dbName,
-                               'tablePrefix' => $dbPrefix,
-                       ]
-               );
-               $db = $loadBalancer->getConnection( DB_REPLICA );
-
-               /** @var SqlBlobStore $blobStore */
-               $blobStore = $this->getMockBuilder( SqlBlobStore::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $store = new RevisionStore(
-                       $loadBalancer,
-                       $blobStore,
-                       new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
-                       MediaWikiServices::getInstance()->getCommentStore(),
-                       MediaWikiServices::getInstance()->getContentModelStore(),
-                       MediaWikiServices::getInstance()->getSlotRoleStore(),
-                       $this->getMcrMigrationStage(),
-                       MediaWikiServices::getInstance()->getActorMigration(),
-                       $wikiId
-               );
-
-               $count = $store->countRevisionsByPageId( $db, 0 );
-
-               // Dummy check to make PhpUnit happy. We are really only interested in
-               // countRevisionsByPageId not failing due to the DB domain check.
-               $this->assertSame( 0, $count );
-       }
-
-       protected function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) {
-               $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() );
-               $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() );
-               $this->assertEquals( $l1->getFragment(), $l2->getFragment() );
-               $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() );
-       }
-
-       protected function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) {
-               $this->assertEquals(
-                       $r1->getPageAsLinkTarget()->getNamespace(),
-                       $r2->getPageAsLinkTarget()->getNamespace()
-               );
-
-               $this->assertEquals(
-                       $r1->getPageAsLinkTarget()->getText(),
-                       $r2->getPageAsLinkTarget()->getText()
-               );
-
-               if ( $r1->getParentId() ) {
-                       $this->assertEquals( $r1->getParentId(), $r2->getParentId() );
-               }
-
-               $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() );
-               $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() );
-               $this->assertEquals( $r1->getComment(), $r2->getComment() );
-               $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() );
-               $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() );
-               $this->assertEquals( $r1->getSha1(), $r2->getSha1() );
-               $this->assertEquals( $r1->getSize(), $r2->getSize() );
-               $this->assertEquals( $r1->getPageId(), $r2->getPageId() );
-               $this->assertArrayEquals( $r1->getSlotRoles(), $r2->getSlotRoles() );
-               $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() );
-               $this->assertEquals( $r1->isMinor(), $r2->isMinor() );
-               foreach ( $r1->getSlotRoles() as $role ) {
-                       $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) );
-                       $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) );
-               }
-               foreach ( [
-                       RevisionRecord::DELETED_TEXT,
-                       RevisionRecord::DELETED_COMMENT,
-                       RevisionRecord::DELETED_USER,
-                       RevisionRecord::DELETED_RESTRICTED,
-               ] as $field ) {
-                       $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) );
-               }
-       }
-
-       protected function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) {
-               $this->assertSame( $s1->getRole(), $s2->getRole() );
-               $this->assertSame( $s1->getModel(), $s2->getModel() );
-               $this->assertSame( $s1->getFormat(), $s2->getFormat() );
-               $this->assertSame( $s1->getSha1(), $s2->getSha1() );
-               $this->assertSame( $s1->getSize(), $s2->getSize() );
-               $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) );
-
-               $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null;
-               $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null;
-       }
-
-       protected function assertRevisionCompleteness( RevisionRecord $r ) {
-               $this->assertTrue( $r->hasSlot( SlotRecord::MAIN ) );
-               $this->assertInstanceOf( SlotRecord::class, $r->getSlot( SlotRecord::MAIN ) );
-               $this->assertInstanceOf( Content::class, $r->getContent( SlotRecord::MAIN ) );
-
-               foreach ( $r->getSlotRoles() as $role ) {
-                       $this->assertSlotCompleteness( $r, $r->getSlot( $role ) );
-               }
-       }
-
-       protected function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) {
-               $this->assertTrue( $slot->hasAddress() );
-               $this->assertSame( $r->getId(), $slot->getRevision() );
-
-               $this->assertInstanceOf( Content::class, $slot->getContent() );
-       }
-
-       /**
-        * @param mixed[] $details
-        *
-        * @return RevisionRecord
-        */
-       private function getRevisionRecordFromDetailsArray( $details = [] ) {
-               // Convert some values that can't be provided by dataProviders
-               if ( isset( $details['user'] ) && $details['user'] === true ) {
-                       $details['user'] = $this->getTestUser()->getUser();
-               }
-               if ( isset( $details['page'] ) && $details['page'] === true ) {
-                       $details['page'] = $this->getTestPage()->getId();
-               }
-               if ( isset( $details['parent'] ) && $details['parent'] === true ) {
-                       $details['parent'] = $this->getTestPage()->getLatest();
-               }
-
-               // Create the RevisionRecord with any available data
-               $rev = new MutableRevisionRecord( $this->getTestPageTitle() );
-               isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null;
-               isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null;
-               isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null;
-               isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null;
-               isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null;
-               isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null;
-               isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null;
-               isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null;
-               isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null;
-               isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null;
-               isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null;
-
-               if ( isset( $details['content'] ) ) {
-                       foreach ( $details['content'] as $role => $content ) {
-                               $rev->setContent( $role, $content );
-                       }
-               }
-
-               return $rev;
-       }
-
-       public function provideInsertRevisionOn_successes() {
-               yield 'Bare minimum revision insertion' => [
-                       [
-                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
-                               'page' => true,
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'timestamp' => '20171117010101',
-                               'user' => true,
-                       ],
-               ];
-               yield 'Detailed revision insertion' => [
-                       [
-                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
-                               'parent' => true,
-                               'page' => true,
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'timestamp' => '20171117010101',
-                               'user' => true,
-                               'minor' => true,
-                               'visibility' => RevisionRecord::DELETED_RESTRICTED,
-                       ],
-               ];
-       }
-
-       protected function getRandomCommentStoreComment() {
-               return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) );
-       }
-
-       /**
-        * @dataProvider provideInsertRevisionOn_successes
-        * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
-        * @covers \MediaWiki\Storage\RevisionStore::insertSlotRowOn
-        * @covers \MediaWiki\Storage\RevisionStore::insertContentRowOn
-        */
-       public function testInsertRevisionOn_successes(
-               array $revDetails = []
-       ) {
-               $title = $this->getTestPageTitle();
-               $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
-
-               $this->overrideMwServices();
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
-
-               // is the new revision correct?
-               $this->assertRevisionCompleteness( $return );
-               $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() );
-               $this->assertRevisionRecordsEqual( $rev, $return );
-
-               // can we load it from the store?
-               $loaded = $store->getRevisionById( $return->getId() );
-               $this->assertRevisionCompleteness( $loaded );
-               $this->assertRevisionRecordsEqual( $return, $loaded );
-
-               // can we find it directly in the database?
-               $this->assertRevisionExistsInDatabase( $return );
-       }
-
-       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
-               $row = $this->revisionToRow( new Revision( $rev ), [] );
-
-               // unset nulled fields
-               unset( $row->rev_content_model );
-               unset( $row->rev_content_format );
-
-               // unset fake fields
-               unset( $row->rev_comment_text );
-               unset( $row->rev_comment_data );
-               unset( $row->rev_comment_cid );
-               unset( $row->rev_comment_id );
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $queryInfo = $store->getQueryInfo( [ 'user' ] );
-
-               $row = get_object_vars( $row );
-               $this->assertSelect(
-                       $queryInfo['tables'],
-                       array_keys( $row ),
-                       [ 'rev_id' => $rev->getId() ],
-                       [ array_values( $row ) ],
-                       [],
-                       $queryInfo['joins']
-               );
-       }
-
-       /**
-        * @param SlotRecord $a
-        * @param SlotRecord $b
-        */
-       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
-               // Assert that the same blob address has been used.
-               $this->assertSame( $a->getAddress(), $b->getAddress() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
-        */
-       public function testInsertRevisionOn_blobAddressExists() {
-               $title = $this->getTestPageTitle();
-               $revDetails = [
-                       'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
-                       'parent' => true,
-                       'comment' => $this->getRandomCommentStoreComment(),
-                       'timestamp' => '20171117010101',
-                       'user' => true,
-               ];
-
-               $this->overrideMwServices();
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-
-               // Insert the first revision
-               $revOne = $this->getRevisionRecordFromDetailsArray( $revDetails );
-               $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) );
-               $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() );
-               $this->assertRevisionRecordsEqual( $revOne, $firstReturn );
-
-               // Insert a second revision inheriting the same blob address
-               $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( SlotRecord::MAIN ) );
-               $revTwo = $this->getRevisionRecordFromDetailsArray( $revDetails );
-               $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) );
-               $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() );
-               $this->assertRevisionRecordsEqual( $revTwo, $secondReturn );
-
-               $firstMainSlot = $firstReturn->getSlot( SlotRecord::MAIN );
-               $secondMainSlot = $secondReturn->getSlot( SlotRecord::MAIN );
-
-               $this->assertSameSlotContent( $firstMainSlot, $secondMainSlot );
-
-               // And that different revisions have been created.
-               $this->assertNotSame( $firstReturn->getId(), $secondReturn->getId() );
-
-               // Make sure the slot rows reference the correct revision
-               $this->assertSame( $firstReturn->getId(), $firstMainSlot->getRevision() );
-               $this->assertSame( $secondReturn->getId(), $secondMainSlot->getRevision() );
-       }
-
-       public function provideInsertRevisionOn_failures() {
-               yield 'no slot' => [
-                       [
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'timestamp' => '20171117010101',
-                               'user' => true,
-                       ],
-                       new InvalidArgumentException( 'main slot must be provided' )
-               ];
-               yield 'no main slot' => [
-                       [
-                               'slot' => SlotRecord::newUnsaved( 'aux', new WikitextContent( 'Turkey' ) ),
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'timestamp' => '20171117010101',
-                               'user' => true,
-                       ],
-                       new InvalidArgumentException( 'main slot must be provided' )
-               ];
-               yield 'no timestamp' => [
-                       [
-                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'user' => true,
-                       ],
-                       new IncompleteRevisionException( 'timestamp field must not be NULL!' )
-               ];
-               yield 'no comment' => [
-                       [
-                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
-                               'timestamp' => '20171117010101',
-                               'user' => true,
-                       ],
-                       new IncompleteRevisionException( 'comment must not be NULL!' )
-               ];
-               yield 'no user' => [
-                       [
-                               'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
-                               'comment' => $this->getRandomCommentStoreComment(),
-                               'timestamp' => '20171117010101',
-                       ],
-                       new IncompleteRevisionException( 'user must not be NULL!' )
-               ];
-       }
-
-       /**
-        * @dataProvider provideInsertRevisionOn_failures
-        * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
-        */
-       public function testInsertRevisionOn_failures(
-               array $revDetails = [],
-               Exception $exception
-       ) {
-               $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-
-               $this->setExpectedException(
-                       get_class( $exception ),
-                       $exception->getMessage(),
-                       $exception->getCode()
-               );
-               $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
-       }
-
-       public function provideNewNullRevision() {
-               yield [
-                       Title::newFromText( 'UTPage_notAutoCreated' ),
-                       [ 'content' => [ 'main' => new WikitextContent( 'Flubber1' ) ] ],
-                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ),
-                       true,
-               ];
-               yield [
-                       Title::newFromText( 'UTPage_notAutoCreated' ),
-                       [ 'content' => [ 'main' => new WikitextContent( 'Flubber2' ) ] ],
-                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ),
-                       false,
-               ];
-       }
-
-       /**
-        * @dataProvider provideNewNullRevision
-        * @covers \MediaWiki\Storage\RevisionStore::newNullRevision
-        * @covers \MediaWiki\Storage\RevisionStore::findSlotContentId
-        */
-       public function testNewNullRevision( Title $title, $revDetails, $comment, $minor = false ) {
-               $this->overrideMwServices();
-
-               $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
-               $page = WikiPage::factory( $title );
-
-               if ( !$page->exists() ) {
-                       $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__, EDIT_NEW );
-               }
-
-               $revDetails['page'] = $page->getId();
-               $revDetails['timestamp'] = wfTimestampNow();
-               $revDetails['comment'] = CommentStoreComment::newUnsavedComment( 'Base' );
-               $revDetails['user'] = $user;
-
-               $baseRev = $this->getRevisionRecordFromDetailsArray( $revDetails );
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-
-               $dbw = wfGetDB( DB_MASTER );
-               $baseRev = $store->insertRevisionOn( $baseRev, $dbw );
-               $page->updateRevisionOn( $dbw, new Revision( $baseRev ), $page->getLatest() );
-
-               $record = $store->newNullRevision(
-                       wfGetDB( DB_MASTER ),
-                       $title,
-                       $comment,
-                       $minor,
-                       $user
-               );
-
-               $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() );
-               $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() );
-               $this->assertEquals( $comment, $record->getComment() );
-               $this->assertEquals( $minor, $record->isMinor() );
-               $this->assertEquals( $user->getName(), $record->getUser()->getName() );
-               $this->assertEquals( $baseRev->getId(), $record->getParentId() );
-
-               $this->assertArrayEquals(
-                       $baseRev->getSlotRoles(),
-                       $record->getSlotRoles()
-               );
-
-               foreach ( $baseRev->getSlotRoles() as $role ) {
-                       $parentSlot = $baseRev->getSlot( $role );
-                       $slot = $record->getSlot( $role );
-
-                       $this->assertTrue( $slot->isInherited(), 'isInherited' );
-                       $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' );
-                       $this->assertSameSlotContent( $parentSlot, $slot );
-               }
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newNullRevision
-        */
-       public function testNewNullRevision_nonExistingTitle() {
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $record = $store->newNullRevision(
-                       wfGetDB( DB_MASTER ),
-                       Title::newFromText( __METHOD__ . '.iDontExist!' ),
-                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ),
-                       false,
-                       TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser()
-               );
-               $this->assertNull( $record );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled
-        */
-       public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() {
-               $page = $this->getTestPage();
-               $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               /** @var Revision $rev */
-               $rev = $status->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $revisionRecord = $store->getRevisionById( $rev->getId() );
-               $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
-
-               $this->assertGreaterThan( 0, $result );
-               $this->assertSame(
-                       $store->getRecentChange( $revisionRecord )->getAttribute( 'rc_id' ),
-                       $result
-               );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled
-        */
-       public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() {
-               // This assumes that sysops are auto patrolled
-               $sysop = $this->getTestSysop()->getUser();
-               $page = $this->getTestPage();
-               $status = $page->doEditContent(
-                       new WikitextContent( __METHOD__ ),
-                       __METHOD__,
-                       0,
-                       false,
-                       $sysop
-               );
-               /** @var Revision $rev */
-               $rev = $status->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $revisionRecord = $store->getRevisionById( $rev->getId() );
-               $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
-
-               $this->assertSame( 0, $result );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getRecentChange
-        */
-       public function testGetRecentChange() {
-               $page = $this->getTestPage();
-               $content = new WikitextContent( __METHOD__ );
-               $status = $page->doEditContent( $content, __METHOD__ );
-               /** @var Revision $rev */
-               $rev = $status->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $revRecord = $store->getRevisionById( $rev->getId() );
-               $recentChange = $store->getRecentChange( $revRecord );
-
-               $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) );
-               $this->assertEquals( $rev->getRecentChange(), $recentChange );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getRevisionById
-        */
-       public function testGetRevisionById() {
-               $page = $this->getTestPage();
-               $content = new WikitextContent( __METHOD__ );
-               $status = $page->doEditContent( $content, __METHOD__ );
-               /** @var Revision $rev */
-               $rev = $status->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $revRecord = $store->getRevisionById( $rev->getId() );
-
-               $this->assertSame( $rev->getId(), $revRecord->getId() );
-               $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
-               $this->assertSame( __METHOD__, $revRecord->getComment()->text );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle
-        */
-       public function testGetRevisionByTitle() {
-               $page = $this->getTestPage();
-               $content = new WikitextContent( __METHOD__ );
-               $status = $page->doEditContent( $content, __METHOD__ );
-               /** @var Revision $rev */
-               $rev = $status->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $revRecord = $store->getRevisionByTitle( $page->getTitle() );
-
-               $this->assertSame( $rev->getId(), $revRecord->getId() );
-               $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
-               $this->assertSame( __METHOD__, $revRecord->getComment()->text );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId
-        */
-       public function testGetRevisionByPageId() {
-               $page = $this->getTestPage();
-               $content = new WikitextContent( __METHOD__ );
-               $status = $page->doEditContent( $content, __METHOD__ );
-               /** @var Revision $rev */
-               $rev = $status->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $revRecord = $store->getRevisionByPageId( $page->getId() );
-
-               $this->assertSame( $rev->getId(), $revRecord->getId() );
-               $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
-               $this->assertSame( __METHOD__, $revRecord->getComment()->text );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTimestamp
-        */
-       public function testGetRevisionByTimestamp() {
-               // Make sure there is 1 second between the last revision and the rev we create...
-               // Otherwise we might not get the correct revision and the test may fail...
-               // :(
-               $page = $this->getTestPage();
-               sleep( 1 );
-               $content = new WikitextContent( __METHOD__ );
-               $status = $page->doEditContent( $content, __METHOD__ );
-               /** @var Revision $rev */
-               $rev = $status->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $revRecord = $store->getRevisionByTimestamp(
-                       $page->getTitle(),
-                       $rev->getTimestamp()
-               );
-
-               $this->assertSame( $rev->getId(), $revRecord->getId() );
-               $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
-               $this->assertSame( __METHOD__, $revRecord->getComment()->text );
-       }
-
-       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
-               // XXX: the WikiPage object loads another RevisionRecord from the database. Not great.
-               $page = WikiPage::factory( $rev->getTitle() );
-
-               $fields = [
-                       'rev_id' => (string)$rev->getId(),
-                       'rev_page' => (string)$rev->getPage(),
-                       'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ),
-                       'rev_user_text' => (string)$rev->getUserText(),
-                       'rev_user' => (string)$rev->getUser(),
-                       'rev_minor_edit' => $rev->isMinor() ? '1' : '0',
-                       'rev_deleted' => (string)$rev->getVisibility(),
-                       'rev_len' => (string)$rev->getSize(),
-                       'rev_parent_id' => (string)$rev->getParentId(),
-                       'rev_sha1' => (string)$rev->getSha1(),
-               ];
-
-               if ( in_array( 'page', $options ) ) {
-                       $fields += [
-                               'page_namespace' => (string)$page->getTitle()->getNamespace(),
-                               'page_title' => $page->getTitle()->getDBkey(),
-                               'page_id' => (string)$page->getId(),
-                               'page_latest' => (string)$page->getLatest(),
-                               'page_is_redirect' => $page->isRedirect() ? '1' : '0',
-                               'page_len' => (string)$page->getContent()->getSize(),
-                       ];
-               }
-
-               if ( in_array( 'user', $options ) ) {
-                       $fields += [
-                               'user_name' => (string)$rev->getUserText(),
-                       ];
-               }
-
-               if ( in_array( 'comment', $options ) ) {
-                       $fields += [
-                               'rev_comment_text' => $rev->getComment(),
-                               'rev_comment_data' => null,
-                               'rev_comment_cid' => null,
-                       ];
-               }
-
-               if ( $rev->getId() ) {
-                       $fields += [
-                               'rev_id' => (string)$rev->getId(),
-                       ];
-               }
-
-               return (object)$fields;
-       }
-
-       protected function assertRevisionRecordMatchesRevision(
-               Revision $rev,
-               RevisionRecord $record
-       ) {
-               $this->assertSame( $rev->getId(), $record->getId() );
-               $this->assertSame( $rev->getPage(), $record->getPageId() );
-               $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() );
-               $this->assertSame( $rev->getUserText(), $record->getUser()->getName() );
-               $this->assertSame( $rev->getUser(), $record->getUser()->getId() );
-               $this->assertSame( $rev->isMinor(), $record->isMinor() );
-               $this->assertSame( $rev->getVisibility(), $record->getVisibility() );
-               $this->assertSame( $rev->getSize(), $record->getSize() );
-               /**
-                * @note As of MW 1.31, the database schema allows the parent ID to be
-                * NULL to indicate that it is unknown.
-                */
-               $expectedParent = $rev->getParentId();
-               if ( $expectedParent === null ) {
-                       $expectedParent = 0;
-               }
-               $this->assertSame( $expectedParent, $record->getParentId() );
-               $this->assertSame( $rev->getSha1(), $record->getSha1() );
-               $this->assertSame( $rev->getComment(), $record->getComment()->text );
-               $this->assertSame( $rev->getContentFormat(),
-                       $record->getContent( SlotRecord::MAIN )->getDefaultFormat() );
-               $this->assertSame( $rev->getContentModel(), $record->getContent( SlotRecord::MAIN )->getModel() );
-               $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() );
-
-               $revRec = $rev->getRevisionRecord();
-               $revMain = $revRec->getSlot( SlotRecord::MAIN );
-               $recMain = $record->getSlot( SlotRecord::MAIN );
-
-               $this->assertSame( $revMain->hasOrigin(), $recMain->hasOrigin(), 'hasOrigin' );
-               $this->assertSame( $revMain->hasAddress(), $recMain->hasAddress(), 'hasAddress' );
-               $this->assertSame( $revMain->hasContentId(), $recMain->hasContentId(), 'hasContentId' );
-
-               if ( $revMain->hasOrigin() ) {
-                       $this->assertSame( $revMain->getOrigin(), $recMain->getOrigin(), 'getOrigin' );
-               }
-
-               if ( $revMain->hasAddress() ) {
-                       $this->assertSame( $revMain->getAddress(), $recMain->getAddress(), 'getAddress' );
-               }
-
-               if ( $revMain->hasContentId() ) {
-                       $this->assertSame( $revMain->getContentId(), $recMain->getContentId(), 'getContentId' );
-               }
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
-        */
-       public function testNewRevisionFromRow_getQueryInfo() {
-               $page = $this->getTestPage();
-               $text = __METHOD__ . 'a-ä';
-               /** @var Revision $rev */
-               $rev = $page->doEditContent(
-                       new WikitextContent( $text ),
-                       __METHOD__ . 'a'
-               )->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $info = $store->getQueryInfo();
-               $row = $this->db->selectRow(
-                       $info['tables'],
-                       $info['fields'],
-                       [ 'rev_id' => $rev->getId() ],
-                       __METHOD__,
-                       [],
-                       $info['joins']
-               );
-               $record = $store->newRevisionFromRow(
-                       $row,
-                       [],
-                       $page->getTitle()
-               );
-               $this->assertRevisionRecordMatchesRevision( $rev, $record );
-               $this->assertSame( $text, $rev->getContent()->serialize() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        */
-       public function testNewRevisionFromRow_anonEdit() {
-               $page = $this->getTestPage();
-               $text = __METHOD__ . 'a-ä';
-               /** @var Revision $rev */
-               $rev = $page->doEditContent(
-                       new WikitextContent( $text ),
-                       __METHOD__ . 'a'
-               )->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $record = $store->newRevisionFromRow(
-                       $this->revisionToRow( $rev ),
-                       [],
-                       $page->getTitle()
-               );
-               $this->assertRevisionRecordMatchesRevision( $rev, $record );
-               $this->assertSame( $text, $rev->getContent()->serialize() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        */
-       public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
-               $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
-               $this->overrideMwServices();
-               $page = $this->getTestPage();
-               $text = __METHOD__ . 'a-ä';
-               /** @var Revision $rev */
-               $rev = $page->doEditContent(
-                       new WikitextContent( $text ),
-                       __METHOD__ . 'a'
-               )->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $record = $store->newRevisionFromRow(
-                       $this->revisionToRow( $rev ),
-                       [],
-                       $page->getTitle()
-               );
-               $this->assertRevisionRecordMatchesRevision( $rev, $record );
-               $this->assertSame( $text, $rev->getContent()->serialize() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        */
-       public function testNewRevisionFromRow_userEdit() {
-               $page = $this->getTestPage();
-               $text = __METHOD__ . 'b-ä';
-               /** @var Revision $rev */
-               $rev = $page->doEditContent(
-                       new WikitextContent( $text ),
-                       __METHOD__ . 'b',
-                       0,
-                       false,
-                       $this->getTestUser()->getUser()
-               )->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $record = $store->newRevisionFromRow(
-                       $this->revisionToRow( $rev ),
-                       [],
-                       $page->getTitle()
-               );
-               $this->assertRevisionRecordMatchesRevision( $rev, $record );
-               $this->assertSame( $text, $rev->getContent()->serialize() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
-        * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
-        */
-       public function testNewRevisionFromArchiveRow_getArchiveQueryInfo() {
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $title = Title::newFromText( __METHOD__ );
-               $text = __METHOD__ . '-bä';
-               $page = WikiPage::factory( $title );
-               /** @var Revision $orig */
-               $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
-                       ->value['revision'];
-               $page->doDeleteArticle( __METHOD__ );
-
-               $db = wfGetDB( DB_MASTER );
-               $arQuery = $store->getArchiveQueryInfo();
-               $res = $db->select(
-                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
-                       __METHOD__, [], $arQuery['joins']
-               );
-               $this->assertTrue( is_object( $res ), 'query failed' );
-
-               $row = $res->fetchObject();
-               $res->free();
-               $record = $store->newRevisionFromArchiveRow( $row );
-
-               $this->assertRevisionRecordMatchesRevision( $orig, $record );
-               $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
-        */
-       public function testNewRevisionFromArchiveRow_legacyEncoding() {
-               $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
-               $this->overrideMwServices();
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $title = Title::newFromText( __METHOD__ );
-               $text = __METHOD__ . '-bä';
-               $page = WikiPage::factory( $title );
-               /** @var Revision $orig */
-               $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
-                       ->value['revision'];
-               $page->doDeleteArticle( __METHOD__ );
-
-               $db = wfGetDB( DB_MASTER );
-               $arQuery = $store->getArchiveQueryInfo();
-               $res = $db->select(
-                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
-                       __METHOD__, [], $arQuery['joins']
-               );
-               $this->assertTrue( is_object( $res ), 'query failed' );
-
-               $row = $res->fetchObject();
-               $res->free();
-               $record = $store->newRevisionFromArchiveRow( $row );
-
-               $this->assertRevisionRecordMatchesRevision( $orig, $record );
-               $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
-        */
-       public function testNewRevisionFromArchiveRow_no_user() {
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-
-               $row = (object)[
-                       'ar_id' => '1',
-                       'ar_page_id' => '2',
-                       'ar_namespace' => '0',
-                       'ar_title' => 'Something',
-                       'ar_rev_id' => '2',
-                       'ar_text_id' => '47',
-                       'ar_timestamp' => '20180528192356',
-                       'ar_minor_edit' => '0',
-                       'ar_deleted' => '0',
-                       'ar_len' => '78',
-                       'ar_parent_id' => '0',
-                       'ar_sha1' => 'deadbeef',
-                       'ar_comment_text' => 'whatever',
-                       'ar_comment_data' => null,
-                       'ar_comment_cid' => null,
-                       'ar_user' => '0',
-                       'ar_user_text' => '', // this is the important bit
-                       'ar_actor' => null,
-                       'ar_content_format' => null,
-                       'ar_content_model' => null,
-               ];
-
-               \Wikimedia\suppressWarnings();
-               $record = $store->newRevisionFromArchiveRow( $row );
-               \Wikimedia\suppressWarnings( true );
-
-               $this->assertInstanceOf( RevisionRecord::class, $record );
-               $this->assertInstanceOf( UserIdentityValue::class, $record->getUser() );
-               $this->assertSame( 'Unknown user', $record->getUser()->getName() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        */
-       public function testNewRevisionFromRow_no_user() {
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $title = Title::newFromText( __METHOD__ );
-
-               $row = (object)[
-                       'rev_id' => '2',
-                       'rev_page' => '2',
-                       'page_namespace' => '0',
-                       'page_title' => $title->getText(),
-                       'rev_text_id' => '47',
-                       'rev_timestamp' => '20180528192356',
-                       'rev_minor_edit' => '0',
-                       'rev_deleted' => '0',
-                       'rev_len' => '78',
-                       'rev_parent_id' => '0',
-                       'rev_sha1' => 'deadbeef',
-                       'rev_comment_text' => 'whatever',
-                       'rev_comment_data' => null,
-                       'rev_comment_cid' => null,
-                       'rev_user' => '0',
-                       'rev_user_text' => '', // this is the important bit
-                       'rev_actor' => null,
-                       'rev_content_format' => null,
-                       'rev_content_model' => null,
-               ];
-
-               \Wikimedia\suppressWarnings();
-               $record = $store->newRevisionFromRow( $row, 0, $title );
-               \Wikimedia\suppressWarnings( true );
-
-               $this->assertNotNull( $record );
-               $this->assertNotNull( $record->getUser() );
-               $this->assertNotEmpty( $record->getUser()->getName() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
-        */
-       public function testInsertRevisionOn_archive() {
-               // This is a round trip test for deletion and undeletion of a
-               // revision row via the archive table.
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $title = Title::newFromText( __METHOD__ );
-
-               $page = WikiPage::factory( $title );
-               /** @var Revision $origRev */
-               $page->doEditContent( new WikitextContent( "First" ), __METHOD__ . '-first' );
-               $origRev = $page->doEditContent( new WikitextContent( "Foo" ), __METHOD__ )
-                       ->value['revision'];
-               $orig = $origRev->getRevisionRecord();
-               $page->doDeleteArticle( __METHOD__ );
-
-               // re-create page, so we can later load revisions for it
-               $page->doEditContent( new WikitextContent( 'Two' ), __METHOD__ );
-
-               $db = wfGetDB( DB_MASTER );
-               $arQuery = $store->getArchiveQueryInfo();
-               $row = $db->selectRow(
-                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
-                       __METHOD__, [], $arQuery['joins']
-               );
-
-               $this->assertNotFalse( $row, 'query failed' );
-
-               $record = $store->newRevisionFromArchiveRow(
-                       $row,
-                       0,
-                       $title,
-                       [ 'page_id' => $title->getArticleID() ]
-               );
-
-               $restored = $store->insertRevisionOn( $record, $db );
-
-               // is the new revision correct?
-               $this->assertRevisionCompleteness( $restored );
-               $this->assertRevisionRecordsEqual( $record, $restored );
-
-               // does the new revision use the original slot?
-               $recMain = $record->getSlot( SlotRecord::MAIN );
-               $restMain = $restored->getSlot( SlotRecord::MAIN );
-               $this->assertSame( $recMain->getAddress(), $restMain->getAddress() );
-               $this->assertSame( $recMain->getContentId(), $restMain->getContentId() );
-               $this->assertSame( $recMain->getOrigin(), $restMain->getOrigin() );
-               $this->assertSame( 'Foo', $restMain->getContent()->serialize() );
-
-               // can we load it from the store?
-               $loaded = $store->getRevisionById( $restored->getId() );
-               $this->assertNotNull( $loaded );
-               $this->assertRevisionCompleteness( $loaded );
-               $this->assertRevisionRecordsEqual( $restored, $loaded );
-
-               // can we find it directly in the database?
-               $this->assertRevisionExistsInDatabase( $restored );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromId
-        */
-       public function testLoadRevisionFromId() {
-               $title = Title::newFromText( __METHOD__ );
-               $page = WikiPage::factory( $title );
-               /** @var Revision $rev */
-               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
-                       ->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() );
-               $this->assertRevisionRecordMatchesRevision( $rev, $result );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromPageId
-        */
-       public function testLoadRevisionFromPageId() {
-               $title = Title::newFromText( __METHOD__ );
-               $page = WikiPage::factory( $title );
-               /** @var Revision $rev */
-               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
-                       ->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() );
-               $this->assertRevisionRecordMatchesRevision( $rev, $result );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTitle
-        */
-       public function testLoadRevisionFromTitle() {
-               $title = Title::newFromText( __METHOD__ );
-               $page = WikiPage::factory( $title );
-               /** @var Revision $rev */
-               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
-                       ->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title );
-               $this->assertRevisionRecordMatchesRevision( $rev, $result );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTimestamp
-        */
-       public function testLoadRevisionFromTimestamp() {
-               $title = Title::newFromText( __METHOD__ );
-               $page = WikiPage::factory( $title );
-               /** @var Revision $revOne */
-               $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
-                       ->value['revision'];
-               // Sleep to ensure different timestamps... )(evil)
-               sleep( 1 );
-               /** @var Revision $revTwo */
-               $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' )
-                       ->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $this->assertNull(
-                       $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' )
-               );
-               $this->assertSame(
-                       $revOne->getId(),
-                       $store->loadRevisionFromTimestamp(
-                               wfGetDB( DB_MASTER ),
-                               $title,
-                               $revOne->getTimestamp()
-                       )->getId()
-               );
-               $this->assertSame(
-                       $revTwo->getId(),
-                       $store->loadRevisionFromTimestamp(
-                               wfGetDB( DB_MASTER ),
-                               $title,
-                               $revTwo->getTimestamp()
-                       )->getId()
-               );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::listRevisionSizes
-        */
-       public function testGetParentLengths() {
-               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
-               /** @var Revision $revOne */
-               $revOne = $page->doEditContent(
-                       new WikitextContent( __METHOD__ ), __METHOD__
-               )->value['revision'];
-               /** @var Revision $revTwo */
-               $revTwo = $page->doEditContent(
-                       new WikitextContent( __METHOD__ . '2' ), __METHOD__
-               )->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $this->assertSame(
-                       [
-                               $revOne->getId() => strlen( __METHOD__ ),
-                       ],
-                       $store->listRevisionSizes(
-                               wfGetDB( DB_MASTER ),
-                               [ $revOne->getId() ]
-                       )
-               );
-               $this->assertSame(
-                       [
-                               $revOne->getId() => strlen( __METHOD__ ),
-                               $revTwo->getId() => strlen( __METHOD__ ) + 1,
-                       ],
-                       $store->listRevisionSizes(
-                               wfGetDB( DB_MASTER ),
-                               [ $revOne->getId(), $revTwo->getId() ]
-                       )
-               );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getPreviousRevision
-        */
-       public function testGetPreviousRevision() {
-               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
-               /** @var Revision $revOne */
-               $revOne = $page->doEditContent(
-                       new WikitextContent( __METHOD__ ), __METHOD__
-               )->value['revision'];
-               /** @var Revision $revTwo */
-               $revTwo = $page->doEditContent(
-                       new WikitextContent( __METHOD__ . '2' ), __METHOD__
-               )->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $this->assertNull(
-                       $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) )
-               );
-               $this->assertSame(
-                       $revOne->getId(),
-                       $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId()
-               );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getNextRevision
-        */
-       public function testGetNextRevision() {
-               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
-               /** @var Revision $revOne */
-               $revOne = $page->doEditContent(
-                       new WikitextContent( __METHOD__ ), __METHOD__
-               )->value['revision'];
-               /** @var Revision $revTwo */
-               $revTwo = $page->doEditContent(
-                       new WikitextContent( __METHOD__ . '2' ), __METHOD__
-               )->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $this->assertSame(
-                       $revTwo->getId(),
-                       $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId()
-               );
-               $this->assertNull(
-                       $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) )
-               );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
-        */
-       public function testGetTimestampFromId_found() {
-               $page = $this->getTestPage();
-               /** @var Revision $rev */
-               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
-                       ->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $result = $store->getTimestampFromId(
-                       $page->getTitle(),
-                       $rev->getId()
-               );
-
-               $this->assertSame( $rev->getTimestamp(), $result );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
-        */
-       public function testGetTimestampFromId_notFound() {
-               $page = $this->getTestPage();
-               /** @var Revision $rev */
-               $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
-                       ->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $result = $store->getTimestampFromId(
-                       $page->getTitle(),
-                       $rev->getId() + 1
-               );
-
-               $this->assertFalse( $result );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByPageId
-        */
-       public function testCountRevisionsByPageId() {
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
-
-               $this->assertSame(
-                       0,
-                       $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
-               );
-               $page->doEditContent( new WikitextContent( 'a' ), 'a' );
-               $this->assertSame(
-                       1,
-                       $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
-               );
-               $page->doEditContent( new WikitextContent( 'b' ), 'b' );
-               $this->assertSame(
-                       2,
-                       $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
-               );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByTitle
-        */
-       public function testCountRevisionsByTitle() {
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
-
-               $this->assertSame(
-                       0,
-                       $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
-               );
-               $page->doEditContent( new WikitextContent( 'a' ), 'a' );
-               $this->assertSame(
-                       1,
-                       $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
-               );
-               $page->doEditContent( new WikitextContent( 'b' ), 'b' );
-               $this->assertSame(
-                       2,
-                       $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
-               );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit
-        */
-       public function testUserWasLastToEdit_false() {
-               $sysop = $this->getTestSysop()->getUser();
-               $page = $this->getTestPage();
-               $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $result = $store->userWasLastToEdit(
-                       wfGetDB( DB_MASTER ),
-                       $page->getId(),
-                       $sysop->getId(),
-                       '20160101010101'
-               );
-               $this->assertFalse( $result );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit
-        */
-       public function testUserWasLastToEdit_true() {
-               $startTime = wfTimestampNow();
-               $sysop = $this->getTestSysop()->getUser();
-               $page = $this->getTestPage();
-               $page->doEditContent(
-                       new WikitextContent( __METHOD__ ),
-                       __METHOD__,
-                       0,
-                       false,
-                       $sysop
-               );
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $result = $store->userWasLastToEdit(
-                       wfGetDB( DB_MASTER ),
-                       $page->getId(),
-                       $sysop->getId(),
-                       $startTime
-               );
-               $this->assertTrue( $result );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision
-        */
-       public function testGetKnownCurrentRevision() {
-               $page = $this->getTestPage();
-               /** @var Revision $rev */
-               $rev = $page->doEditContent(
-                       new WikitextContent( __METHOD__ . 'b' ),
-                       __METHOD__ . 'b',
-                       0,
-                       false,
-                       $this->getTestUser()->getUser()
-               )->value['revision'];
-
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $record = $store->getKnownCurrentRevision(
-                       $page->getTitle(),
-                       $rev->getId()
-               );
-
-               $this->assertRevisionRecordMatchesRevision( $rev, $record );
-       }
-
-       public function provideNewMutableRevisionFromArray() {
-               yield 'Basic array, content object' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 46,
-                               'parent_id' => 1,
-                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'comment' => 'Goat Comment!',
-                               'content' => new WikitextContent( 'Some Content' ),
-                       ]
-               ];
-               yield 'Basic array, serialized text' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 46,
-                               'parent_id' => 1,
-                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'comment' => 'Goat Comment!',
-                               'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
-                       ]
-               ];
-               yield 'Basic array, serialized text, utf-8 flags' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 46,
-                               'parent_id' => 1,
-                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'comment' => 'Goat Comment!',
-                               'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
-                               'flags' => 'utf-8',
-                       ]
-               ];
-               yield 'Basic array, with title' => [
-                       [
-                               'title' => Title::newFromText( 'SomeText' ),
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 46,
-                               'parent_id' => 1,
-                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'comment' => 'Goat Comment!',
-                               'content' => new WikitextContent( 'Some Content' ),
-                       ]
-               ];
-               yield 'Basic array, no user field' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.3',
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 46,
-                               'parent_id' => 1,
-                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'comment' => 'Goat Comment!',
-                               'content' => new WikitextContent( 'Some Content' ),
-                       ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideNewMutableRevisionFromArray
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
-        */
-       public function testNewMutableRevisionFromArray( array $array ) {
-               $store = MediaWikiServices::getInstance()->getRevisionStore();
-
-               // HACK: if $array['page'] is given, make sure that the page exists
-               if ( isset( $array['page'] ) ) {
-                       $t = Title::newFromID( $array['page'] );
-                       if ( !$t || !$t->exists() ) {
-                               $t = Title::makeTitle( NS_MAIN, __METHOD__ );
-                               $info = $this->insertPage( $t );
-                               $array['page'] = $info['id'];
-                       }
-               }
-
-               $result = $store->newMutableRevisionFromArray( $array );
-
-               if ( isset( $array['id'] ) ) {
-                       $this->assertSame( $array['id'], $result->getId() );
-               }
-               if ( isset( $array['page'] ) ) {
-                       $this->assertSame( $array['page'], $result->getPageId() );
-               }
-               $this->assertSame( $array['timestamp'], $result->getTimestamp() );
-               $this->assertSame( $array['user_text'], $result->getUser()->getName() );
-               if ( isset( $array['user'] ) ) {
-                       $this->assertSame( $array['user'], $result->getUser()->getId() );
-               }
-               $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() );
-               $this->assertSame( $array['deleted'], $result->getVisibility() );
-               $this->assertSame( $array['len'], $result->getSize() );
-               $this->assertSame( $array['parent_id'], $result->getParentId() );
-               $this->assertSame( $array['sha1'], $result->getSha1() );
-               $this->assertSame( $array['comment'], $result->getComment()->text );
-               if ( isset( $array['content'] ) ) {
-                       foreach ( $array['content'] as $role => $content ) {
-                               $this->assertTrue(
-                                       $result->getContent( $role )->equals( $content )
-                               );
-                       }
-               } elseif ( isset( $array['text'] ) ) {
-                       $this->assertSame( $array['text'],
-                               $result->getSlot( SlotRecord::MAIN )->getContent()->serialize() );
-               } elseif ( isset( $array['content_format'] ) ) {
-                       $this->assertSame(
-                               $array['content_format'],
-                               $result->getSlot( SlotRecord::MAIN )->getContent()->getDefaultFormat()
-                       );
-                       $this->assertSame( $array['content_model'], $result->getSlot( SlotRecord::MAIN )->getModel() );
-               }
-       }
-
-       /**
-        * @dataProvider provideNewMutableRevisionFromArray
-        * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
-        */
-       public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) {
-               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
-               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-               $blobStore = new SqlBlobStore( $lb, $cache );
-               $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
-
-               $factory = $this->getMockBuilder( BlobStoreFactory::class )
-                       ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $factory->expects( $this->any() )
-                       ->method( 'newBlobStore' )
-                       ->willReturn( $blobStore );
-               $factory->expects( $this->any() )
-                       ->method( 'newSqlBlobStore' )
-                       ->willReturn( $blobStore );
-
-               $this->setService( 'BlobStoreFactory', $factory );
-
-               $this->testNewMutableRevisionFromArray( $array );
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/RevisionStoreFactoryTest.php b/tests/phpunit/includes/Storage/RevisionStoreFactoryTest.php
deleted file mode 100644 (file)
index 1d8771b..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use ActorMigration;
-use CommentStore;
-use MediaWiki\Logger\Spi as LoggerSpi;
-use MediaWiki\Storage\BlobStore;
-use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\NameTableStore;
-use MediaWiki\Storage\NameTableStoreFactory;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreFactory;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWikiTestCase;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use WANObjectCache;
-use Wikimedia\Rdbms\ILBFactory;
-use Wikimedia\Rdbms\ILoadBalancer;
-use Wikimedia\TestingAccessWrapper;
-
-class RevisionStoreFactoryTest extends MediaWikiTestCase {
-
-       public function testValidConstruction_doesntCauseErrors() {
-               new RevisionStoreFactory(
-                       $this->getMockLoadBalancerFactory(),
-                       $this->getMockBlobStoreFactory(),
-                       $this->getNameTableStoreFactory(),
-                       $this->getHashWANObjectCache(),
-                       $this->getMockCommentStore(),
-                       ActorMigration::newMigration(),
-                       MIGRATION_OLD,
-                       $this->getMockLoggerSpi(),
-                       true
-               );
-               $this->assertTrue( true );
-       }
-
-       public function provideWikiIds() {
-               yield [ true ];
-               yield [ false ];
-               yield [ 'somewiki' ];
-               yield [ 'somewiki', MIGRATION_OLD , false ];
-               yield [ 'somewiki', MIGRATION_NEW , true ];
-       }
-
-       /**
-        * @dataProvider provideWikiIds
-        */
-       public function testGetRevisionStore(
-               $wikiId,
-               $mcrMigrationStage = MIGRATION_OLD,
-               $contentHandlerUseDb = true
-       ) {
-               $lbFactory = $this->getMockLoadBalancerFactory();
-               $blobStoreFactory = $this->getMockBlobStoreFactory();
-               $nameTableStoreFactory = $this->getNameTableStoreFactory();
-               $cache = $this->getHashWANObjectCache();
-               $commentStore = $this->getMockCommentStore();
-               $actorMigration = ActorMigration::newMigration();
-               $loggerProvider = $this->getMockLoggerSpi();
-
-               $factory = new RevisionStoreFactory(
-                       $lbFactory,
-                       $blobStoreFactory,
-                       $nameTableStoreFactory,
-                       $cache,
-                       $commentStore,
-                       $actorMigration,
-                       $mcrMigrationStage,
-                       $loggerProvider,
-                       $contentHandlerUseDb
-               );
-
-               $store = $factory->getRevisionStore( $wikiId );
-               $wrapper = TestingAccessWrapper::newFromObject( $store );
-
-               // ensure the correct object type is returned
-               $this->assertInstanceOf( RevisionStore::class, $store );
-
-               // ensure the RevisionStore is for the given wikiId
-               $this->assertSame( $wikiId, $wrapper->wikiId );
-
-               // ensure all other required services are correctly set
-               $this->assertSame( $cache, $wrapper->cache );
-               $this->assertSame( $commentStore, $wrapper->commentStore );
-               $this->assertSame( $mcrMigrationStage, $wrapper->mcrMigrationStage );
-               $this->assertSame( $actorMigration, $wrapper->actorMigration );
-               $this->assertSame( $contentHandlerUseDb, $store->getContentHandlerUseDB() );
-
-               $this->assertInstanceOf( ILoadBalancer::class, $wrapper->loadBalancer );
-               $this->assertInstanceOf( BlobStore::class, $wrapper->blobStore );
-               $this->assertInstanceOf( NameTableStore::class, $wrapper->contentModelStore );
-               $this->assertInstanceOf( NameTableStore::class, $wrapper->slotRoleStore );
-               $this->assertInstanceOf( LoggerInterface::class, $wrapper->logger );
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
-        */
-       private function getMockLoadBalancer() {
-               return $this->getMockBuilder( ILoadBalancer::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|ILBFactory
-        */
-       private function getMockLoadBalancerFactory() {
-               $mock = $this->getMockBuilder( ILBFactory::class )
-                       ->disableOriginalConstructor()->getMock();
-
-               $mock->method( 'getMainLB' )
-                       ->willReturnCallback( function () {
-                               return $this->getMockLoadBalancer();
-                       } );
-
-               return $mock;
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
-        */
-       private function getMockSqlBlobStore() {
-               return $this->getMockBuilder( SqlBlobStore::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|BlobStoreFactory
-        */
-       private function getMockBlobStoreFactory() {
-               $mock = $this->getMockBuilder( BlobStoreFactory::class )
-                       ->disableOriginalConstructor()->getMock();
-
-               $mock->method( 'newSqlBlobStore' )
-                       ->willReturnCallback( function () {
-                               return $this->getMockSqlBlobStore();
-                       } );
-
-               return $mock;
-       }
-
-       /**
-        * @return NameTableStoreFactory
-        */
-       private function getNameTableStoreFactory() {
-               return new NameTableStoreFactory(
-                       $this->getMockLoadBalancerFactory(),
-                       $this->getHashWANObjectCache(),
-                       new NullLogger() );
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
-        */
-       private function getMockCommentStore() {
-               return $this->getMockBuilder( CommentStore::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       private function getHashWANObjectCache() {
-               return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|LoggerSpi
-        */
-       private function getMockLoggerSpi() {
-               $mock = $this->getMock( LoggerSpi::class );
-
-               $mock->method( 'getLogger' )
-                       ->willReturn( new NullLogger() );
-
-               return $mock;
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php b/tests/phpunit/includes/Storage/RevisionStoreRecordTest.php
deleted file mode 100644 (file)
index 12d950c..0000000
+++ /dev/null
@@ -1,366 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use InvalidArgumentException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\User\UserIdentity;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiTestCase;
-use TextContent;
-use Title;
-
-/**
- * @covers \MediaWiki\Storage\RevisionStoreRecord
- * @covers \MediaWiki\Storage\RevisionRecord
- */
-class RevisionStoreRecordTest extends MediaWikiTestCase {
-
-       use RevisionRecordTests;
-
-       /**
-        * @param array $rowOverrides
-        *
-        * @return RevisionStoreRecord
-        */
-       protected function newRevision( array $rowOverrides = [] ) {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
-
-               $user = new UserIdentityValue( 11, 'Tester', 0 );
-               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
-               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
-               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
-               $slots = new RevisionSlots( [ $main, $aux ] );
-
-               $row = [
-                       'rev_id' => '7',
-                       'rev_page' => strval( $title->getArticleID() ),
-                       'rev_timestamp' => '20200101000000',
-                       'rev_deleted' => 0,
-                       'rev_minor_edit' => 0,
-                       'rev_parent_id' => '5',
-                       'rev_len' => $slots->computeSize(),
-                       'rev_sha1' => $slots->computeSha1(),
-                       'page_latest' => '18',
-               ];
-
-               $row = array_merge( $row, $rowOverrides );
-
-               return new RevisionStoreRecord( $title, $user, $comment, (object)$row, $slots );
-       }
-
-       public function provideConstructor() {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
-
-               $user = new UserIdentityValue( 11, 'Tester', 0 );
-               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
-               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
-               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
-               $slots = new RevisionSlots( [ $main, $aux ] );
-
-               $protoRow = [
-                       'rev_id' => '7',
-                       'rev_page' => strval( $title->getArticleID() ),
-                       'rev_timestamp' => '20200101000000',
-                       'rev_deleted' => 0,
-                       'rev_minor_edit' => 0,
-                       'rev_parent_id' => '5',
-                       'rev_len' => $slots->computeSize(),
-                       'rev_sha1' => $slots->computeSha1(),
-                       'page_latest' => '18',
-               ];
-
-               $row = $protoRow;
-               yield 'all info' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots,
-                       'acmewiki'
-               ];
-
-               $row = $protoRow;
-               $row['rev_minor_edit'] = '1';
-               $row['rev_deleted'] = strval( RevisionRecord::DELETED_USER );
-
-               yield 'minor deleted' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-               $row['page_latest'] = $row['rev_id'];
-
-               yield 'latest' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-               unset( $row['rev_parent'] );
-
-               yield 'no parent' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-               $row['rev_len'] = null;
-               $row['rev_sha1'] = '';
-
-               yield 'rev_len is null, rev_sha1 is ""' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-               yield 'no length, no hash' => [
-                       Title::newFromText( 'DummyDoesNotExist' ),
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructor
-        *
-        * @param Title $title
-        * @param UserIdentity $user
-        * @param CommentStoreComment $comment
-        * @param object $row
-        * @param RevisionSlots $slots
-        * @param bool $wikiId
-        */
-       public function testConstructorAndGetters(
-               Title $title,
-               UserIdentity $user,
-               CommentStoreComment $comment,
-               $row,
-               RevisionSlots $slots,
-               $wikiId = false
-       ) {
-               $rec = new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
-
-               $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
-               $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
-               $this->assertSame( $comment, $rec->getComment(), 'getComment' );
-
-               $this->assertSame( $slots, $rec->getSlots(), 'getSlots' );
-               $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
-               $this->assertSame( $slots->getSlots(), $rec->getSlots()->getSlots(), 'getSlots' );
-               $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
-
-               $this->assertSame( (int)$row->rev_id, $rec->getId(), 'getId' );
-               $this->assertSame( (int)$row->rev_page, $rec->getPageId(), 'getId' );
-               $this->assertSame( $row->rev_timestamp, $rec->getTimestamp(), 'getTimestamp' );
-               $this->assertSame( (int)$row->rev_deleted, $rec->getVisibility(), 'getVisibility' );
-               $this->assertSame( (bool)$row->rev_minor_edit, $rec->isMinor(), 'getIsMinor' );
-
-               if ( isset( $row->rev_parent_id ) ) {
-                       $this->assertSame( (int)$row->rev_parent_id, $rec->getParentId(), 'getParentId' );
-               } else {
-                       $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
-               }
-
-               if ( isset( $row->rev_len ) ) {
-                       $this->assertSame( (int)$row->rev_len, $rec->getSize(), 'getSize' );
-               } else {
-                       $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
-               }
-
-               if ( !empty( $row->rev_sha1 ) ) {
-                       $this->assertSame( $row->rev_sha1, $rec->getSha1(), 'getSha1' );
-               } else {
-                       $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
-               }
-
-               if ( isset( $row->page_latest ) ) {
-                       $this->assertSame(
-                               (int)$row->rev_id === (int)$row->page_latest,
-                               $rec->isCurrent(),
-                               'isCurrent'
-                       );
-               } else {
-                       $this->assertSame(
-                               false,
-                               $rec->isCurrent(),
-                               'isCurrent'
-                       );
-               }
-       }
-
-       public function provideConstructorFailure() {
-               $title = Title::newFromText( 'Dummy' );
-               $title->resetArticleID( 17 );
-
-               $user = new UserIdentityValue( 11, 'Tester', 0 );
-
-               $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
-               $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
-               $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
-               $slots = new RevisionSlots( [ $main, $aux ] );
-
-               $protoRow = [
-                       'rev_id' => '7',
-                       'rev_page' => strval( $title->getArticleID() ),
-                       'rev_timestamp' => '20200101000000',
-                       'rev_deleted' => 0,
-                       'rev_minor_edit' => 0,
-                       'rev_parent_id' => '5',
-                       'rev_len' => $slots->computeSize(),
-                       'rev_sha1' => $slots->computeSha1(),
-                       'page_latest' => '18',
-               ];
-
-               yield 'not a row' => [
-                       $title,
-                       $user,
-                       $comment,
-                       'not a row',
-                       $slots,
-                       'acmewiki'
-               ];
-
-               $row = $protoRow;
-               $row['rev_timestamp'] = 'kittens';
-
-               yield 'bad timestamp' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-               $row['rev_page'] = 99;
-
-               yield 'page ID mismatch' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots
-               ];
-
-               $row = $protoRow;
-
-               yield 'bad wiki' => [
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots,
-                       12345
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructorFailure
-        *
-        * @param Title $title
-        * @param UserIdentity $user
-        * @param CommentStoreComment $comment
-        * @param object $row
-        * @param RevisionSlots $slots
-        * @param bool $wikiId
-        */
-       public function testConstructorFailure(
-               Title $title,
-               UserIdentity $user,
-               CommentStoreComment $comment,
-               $row,
-               RevisionSlots $slots,
-               $wikiId = false
-       ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
-       }
-
-       public function provideIsCurrent() {
-               yield [
-                       [
-                               'rev_id' => 11,
-                               'page_latest' => 11,
-                       ],
-                       true,
-               ];
-               yield [
-                       [
-                               'rev_id' => 11,
-                               'page_latest' => 10,
-                       ],
-                       false,
-               ];
-       }
-
-       /**
-        * @dataProvider provideIsCurrent
-        */
-       public function testIsCurrent( $row, $current ) {
-               $rev = $this->newRevision( $row );
-
-               $this->assertSame( $current, $rev->isCurrent(), 'isCurrent()' );
-       }
-
-       public function provideGetSlot_audience_latest() {
-               return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
-       }
-
-       /**
-        * @dataProvider provideGetSlot_audience_latest
-        */
-       public function testGetSlot_audience_latest( $visibility, $groups, $userCan, $publicCan ) {
-               $this->forceStandardPermissions();
-
-               $user = $this->getTestUser( $groups )->getUser();
-               $rev = $this->newRevision(
-                       [
-                               'rev_deleted' => $visibility,
-                               'rev_id' => 11,
-                               'page_latest' => 11, // revision is current
-                       ]
-               );
-
-               // NOTE: slot meta-data is never suppressed, just the content is!
-               $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw can' );
-               $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ),
-                       'public can' );
-
-               $this->assertNotNull(
-                       $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ),
-                       'user can'
-               );
-
-               $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getContent();
-               // NOTE: the content of the current revision is never suppressed!
-               // Check that getContent() doesn't throw SuppressedDataException
-               $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC )->getContent();
-               $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )->getContent();
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/RevisionStoreTest.php b/tests/phpunit/includes/Storage/RevisionStoreTest.php
deleted file mode 100644 (file)
index 2ed6f28..0000000
+++ /dev/null
@@ -1,566 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStore;
-use HashBagOStuff;
-use InvalidArgumentException;
-use Language;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWikiTestCase;
-use MWException;
-use Title;
-use WANObjectCache;
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\TestingAccessWrapper;
-use WikitextContent;
-
-class RevisionStoreTest extends MediaWikiTestCase {
-
-       private function useTextId() {
-               global $wgMultiContentRevisionSchemaMigrationStage;
-
-               return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD );
-       }
-
-       /**
-        * @param LoadBalancer $loadBalancer
-        * @param SqlBlobStore $blobStore
-        * @param WANObjectCache $WANObjectCache
-        *
-        * @return RevisionStore
-        */
-       private function getRevisionStore(
-               $loadBalancer = null,
-               $blobStore = null,
-               $WANObjectCache = null
-       ) {
-               global $wgMultiContentRevisionSchemaMigrationStage;
-               // the migration stage should be irrelevant, since all the tests that interact with
-               // the database are in RevisionStoreDbTest, not here.
-
-               return new RevisionStore(
-                       $loadBalancer ?: $this->getMockLoadBalancer(),
-                       $blobStore ?: $this->getMockSqlBlobStore(),
-                       $WANObjectCache ?: $this->getHashWANObjectCache(),
-                       MediaWikiServices::getInstance()->getCommentStore(),
-                       MediaWikiServices::getInstance()->getContentModelStore(),
-                       MediaWikiServices::getInstance()->getSlotRoleStore(),
-                       $wgMultiContentRevisionSchemaMigrationStage,
-                       MediaWikiServices::getInstance()->getActorMigration()
-               );
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
-        */
-       private function getMockLoadBalancer() {
-               return $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|Database
-        */
-       private function getMockDatabase() {
-               return $this->getMockBuilder( Database::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
-        */
-       private function getMockSqlBlobStore() {
-               return $this->getMockBuilder( SqlBlobStore::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       /**
-        * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
-        */
-       private function getMockCommentStore() {
-               return $this->getMockBuilder( CommentStore::class )
-                       ->disableOriginalConstructor()->getMock();
-       }
-
-       private function getHashWANObjectCache() {
-               return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
-       }
-
-       public function provideSetContentHandlerUseDB() {
-               return [
-                       // ContentHandlerUseDB can be true of false pre migration.
-                       [ false, SCHEMA_COMPAT_OLD, false ],
-                       [ true, SCHEMA_COMPAT_OLD, false ],
-                       // During and after migration it can not be false...
-                       [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, true ],
-                       [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, true ],
-                       [ false, SCHEMA_COMPAT_NEW, true ],
-                       // ...but it can be true.
-                       [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
-                       [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
-                       [ true, SCHEMA_COMPAT_NEW, false ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideSetContentHandlerUseDB
-        * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
-        * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
-        */
-       public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
-               if ( $expectedFail ) {
-                       $this->setExpectedException( MWException::class );
-               }
-
-               $nameTables = MediaWikiServices::getInstance()->getNameTableStoreFactory();
-
-               $store = new RevisionStore(
-                       $this->getMockLoadBalancer(),
-                       $this->getMockSqlBlobStore(),
-                       $this->getHashWANObjectCache(),
-                       $this->getMockCommentStore(),
-                       $nameTables->getContentModels(),
-                       $nameTables->getSlotRoles(),
-                       $migrationMode,
-                       MediaWikiServices::getInstance()->getActorMigration()
-               );
-
-               $store->setContentHandlerUseDB( $contentHandlerDb );
-               $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
-       }
-
-       public function testGetTitle_successFromPageId() {
-               $mockLoadBalancer = $this->getMockLoadBalancer();
-               // Title calls wfGetDB() so we have to set the main service
-               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
-               $db = $this->getMockDatabase();
-               // Title calls wfGetDB() which uses a regular Connection
-               $mockLoadBalancer->expects( $this->atLeastOnce() )
-                       ->method( 'getConnection' )
-                       ->willReturn( $db );
-
-               // First call to Title::newFromID, faking no result (db lag?)
-               $db->expects( $this->at( 0 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'page',
-                               $this->anything(),
-                               [ 'page_id' => 1 ]
-                       )
-                       ->willReturn( (object)[
-                               'page_namespace' => '1',
-                               'page_title' => 'Food',
-                       ] );
-
-               $store = $this->getRevisionStore( $mockLoadBalancer );
-               $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
-
-               $this->assertSame( 1, $title->getNamespace() );
-               $this->assertSame( 'Food', $title->getDBkey() );
-       }
-
-       public function testGetTitle_successFromPageIdOnFallback() {
-               $mockLoadBalancer = $this->getMockLoadBalancer();
-               // Title calls wfGetDB() so we have to set the main service
-               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
-               $db = $this->getMockDatabase();
-               // Title calls wfGetDB() which uses a regular Connection
-               // Assert that the first call uses a REPLICA and the second falls back to master
-               $mockLoadBalancer->expects( $this->exactly( 2 ) )
-                       ->method( 'getConnection' )
-                       ->willReturn( $db );
-               // RevisionStore getTitle uses a ConnectionRef
-               $mockLoadBalancer->expects( $this->atLeastOnce() )
-                       ->method( 'getConnectionRef' )
-                       ->willReturn( $db );
-
-               // First call to Title::newFromID, faking no result (db lag?)
-               $db->expects( $this->at( 0 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'page',
-                               $this->anything(),
-                               [ 'page_id' => 1 ]
-                       )
-                       ->willReturn( false );
-
-               // First select using rev_id, faking no result (db lag?)
-               $db->expects( $this->at( 1 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               [ 'revision', 'page' ],
-                               $this->anything(),
-                               [ 'rev_id' => 2 ]
-                       )
-                       ->willReturn( false );
-
-               // Second call to Title::newFromID, no result
-               $db->expects( $this->at( 2 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'page',
-                               $this->anything(),
-                               [ 'page_id' => 1 ]
-                       )
-                       ->willReturn( (object)[
-                               'page_namespace' => '2',
-                               'page_title' => 'Foodey',
-                       ] );
-
-               $store = $this->getRevisionStore( $mockLoadBalancer );
-               $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
-
-               $this->assertSame( 2, $title->getNamespace() );
-               $this->assertSame( 'Foodey', $title->getDBkey() );
-       }
-
-       public function testGetTitle_successFromRevId() {
-               $mockLoadBalancer = $this->getMockLoadBalancer();
-               // Title calls wfGetDB() so we have to set the main service
-               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
-               $db = $this->getMockDatabase();
-               // Title calls wfGetDB() which uses a regular Connection
-               $mockLoadBalancer->expects( $this->atLeastOnce() )
-                       ->method( 'getConnection' )
-                       ->willReturn( $db );
-               // RevisionStore getTitle uses a ConnectionRef
-               $mockLoadBalancer->expects( $this->atLeastOnce() )
-                       ->method( 'getConnectionRef' )
-                       ->willReturn( $db );
-
-               // First call to Title::newFromID, faking no result (db lag?)
-               $db->expects( $this->at( 0 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'page',
-                               $this->anything(),
-                               [ 'page_id' => 1 ]
-                       )
-                       ->willReturn( false );
-
-               // First select using rev_id, faking no result (db lag?)
-               $db->expects( $this->at( 1 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               [ 'revision', 'page' ],
-                               $this->anything(),
-                               [ 'rev_id' => 2 ]
-                       )
-                       ->willReturn( (object)[
-                               'page_namespace' => '1',
-                               'page_title' => 'Food2',
-                       ] );
-
-               $store = $this->getRevisionStore( $mockLoadBalancer );
-               $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
-
-               $this->assertSame( 1, $title->getNamespace() );
-               $this->assertSame( 'Food2', $title->getDBkey() );
-       }
-
-       public function testGetTitle_successFromRevIdOnFallback() {
-               $mockLoadBalancer = $this->getMockLoadBalancer();
-               // Title calls wfGetDB() so we have to set the main service
-               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
-               $db = $this->getMockDatabase();
-               // Title calls wfGetDB() which uses a regular Connection
-               // Assert that the first call uses a REPLICA and the second falls back to master
-               $mockLoadBalancer->expects( $this->exactly( 2 ) )
-                       ->method( 'getConnection' )
-                       ->willReturn( $db );
-               // RevisionStore getTitle uses a ConnectionRef
-               $mockLoadBalancer->expects( $this->atLeastOnce() )
-                       ->method( 'getConnectionRef' )
-                       ->willReturn( $db );
-
-               // First call to Title::newFromID, faking no result (db lag?)
-               $db->expects( $this->at( 0 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'page',
-                               $this->anything(),
-                               [ 'page_id' => 1 ]
-                       )
-                       ->willReturn( false );
-
-               // First select using rev_id, faking no result (db lag?)
-               $db->expects( $this->at( 1 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               [ 'revision', 'page' ],
-                               $this->anything(),
-                               [ 'rev_id' => 2 ]
-                       )
-                       ->willReturn( false );
-
-               // Second call to Title::newFromID, no result
-               $db->expects( $this->at( 2 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'page',
-                               $this->anything(),
-                               [ 'page_id' => 1 ]
-                       )
-                       ->willReturn( false );
-
-               // Second select using rev_id, result
-               $db->expects( $this->at( 3 ) )
-                       ->method( 'selectRow' )
-                       ->with(
-                               [ 'revision', 'page' ],
-                               $this->anything(),
-                               [ 'rev_id' => 2 ]
-                       )
-                       ->willReturn( (object)[
-                               'page_namespace' => '2',
-                               'page_title' => 'Foodey',
-                       ] );
-
-               $store = $this->getRevisionStore( $mockLoadBalancer );
-               $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
-
-               $this->assertSame( 2, $title->getNamespace() );
-               $this->assertSame( 'Foodey', $title->getDBkey() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::getTitle
-        */
-       public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
-               $mockLoadBalancer = $this->getMockLoadBalancer();
-               // Title calls wfGetDB() so we have to set the main service
-               $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
-               $db = $this->getMockDatabase();
-               // Title calls wfGetDB() which uses a regular Connection
-               // Assert that the first call uses a REPLICA and the second falls back to master
-
-               // RevisionStore getTitle uses getConnectionRef
-               // Title::newFromID uses getConnection
-               foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
-                       $mockLoadBalancer->expects( $this->exactly( 2 ) )
-                               ->method( $method )
-                               ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
-                                       static $callCounter = 0;
-                                       $callCounter++;
-                                       // The first call should be to a REPLICA, and the second a MASTER.
-                                       if ( $callCounter === 1 ) {
-                                               $this->assertSame( DB_REPLICA, $masterOrReplica );
-                                       } elseif ( $callCounter === 2 ) {
-                                               $this->assertSame( DB_MASTER, $masterOrReplica );
-                                       }
-                                       return $db;
-                               } );
-               }
-               // First and third call to Title::newFromID, faking no result
-               foreach ( [ 0, 2 ] as $counter ) {
-                       $db->expects( $this->at( $counter ) )
-                               ->method( 'selectRow' )
-                               ->with(
-                                       'page',
-                                       $this->anything(),
-                                       [ 'page_id' => 1 ]
-                               )
-                               ->willReturn( false );
-               }
-
-               foreach ( [ 1, 3 ] as $counter ) {
-                       $db->expects( $this->at( $counter ) )
-                               ->method( 'selectRow' )
-                               ->with(
-                                       [ 'revision', 'page' ],
-                                       $this->anything(),
-                                       [ 'rev_id' => 2 ]
-                               )
-                               ->willReturn( false );
-               }
-
-               $store = $this->getRevisionStore( $mockLoadBalancer );
-
-               $this->setExpectedException( RevisionAccessException::class );
-               $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
-       }
-
-       public function provideNewRevisionFromRow_legacyEncoding_applied() {
-               yield 'windows-1252, old_flags is empty' => [
-                       'windows-1252',
-                       'en',
-                       [
-                               'old_flags' => '',
-                               'old_text' => "S\xF6me Content",
-                       ],
-                       'Söme Content'
-               ];
-
-               yield 'windows-1252, old_flags is null' => [
-                       'windows-1252',
-                       'en',
-                       [
-                               'old_flags' => null,
-                               'old_text' => "S\xF6me Content",
-                       ],
-                       'Söme Content'
-               ];
-       }
-
-       /**
-        * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
-        *
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        */
-       public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
-               if ( !$this->useTextId() ) {
-                       $this->markTestSkipped( 'No longer applicable with MCR schema' );
-               }
-
-               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
-               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-
-               $blobStore = new SqlBlobStore( $lb, $cache );
-               $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
-
-               $store = $this->getRevisionStore( $lb, $blobStore, $cache );
-
-               $record = $store->newRevisionFromRow(
-                       $this->makeRow( $row ),
-                       0,
-                       Title::newFromText( __METHOD__ . '-UTPage' )
-               );
-
-               $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        */
-       public function testNewRevisionFromRow_legacyEncoding_ignored() {
-               if ( !$this->useTextId() ) {
-                       $this->markTestSkipped( 'No longer applicable with MCR schema' );
-               }
-
-               $row = [
-                       'old_flags' => 'utf-8',
-                       'old_text' => 'Söme Content',
-               ];
-
-               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
-               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-
-               $blobStore = new SqlBlobStore( $lb, $cache );
-               $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
-
-               $store = $this->getRevisionStore( $lb, $blobStore, $cache );
-
-               $record = $store->newRevisionFromRow(
-                       $this->makeRow( $row ),
-                       0,
-                       Title::newFromText( __METHOD__ . '-UTPage' )
-               );
-               $this->assertSame( 'Söme Content', $record->getContent( SlotRecord::MAIN )->serialize() );
-       }
-
-       private function makeRow( array $array ) {
-               $row = $array + [
-                               'rev_id' => 7,
-                               'rev_page' => 5,
-                               'rev_timestamp' => '20110101000000',
-                               'rev_user_text' => 'Tester',
-                               'rev_user' => 17,
-                               'rev_minor_edit' => 0,
-                               'rev_deleted' => 0,
-                               'rev_len' => 100,
-                               'rev_parent_id' => 0,
-                               'rev_sha1' => 'deadbeef',
-                               'rev_comment_text' => 'Testing',
-                               'rev_comment_data' => '{}',
-                               'rev_comment_cid' => 111,
-                               'page_namespace' => 0,
-                               'page_title' => 'TEST',
-                               'page_id' => 5,
-                               'page_latest' => 7,
-                               'page_is_redirect' => 0,
-                               'page_len' => 100,
-                               'user_name' => 'Tester',
-                       ];
-
-               if ( $this->useTextId() ) {
-                       $row += [
-                               'rev_content_format' => CONTENT_FORMAT_TEXT,
-                               'rev_content_model' => CONTENT_MODEL_TEXT,
-                               'rev_text_id' => 11,
-                               'old_id' => 11,
-                               'old_text' => 'Hello World',
-                               'old_flags' => 'utf-8',
-                       ];
-               } else {
-                       if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
-                               $row['content'] = [
-                                       'main' => new WikitextContent( $array['old_text'] ),
-                               ];
-                       }
-               }
-
-               return (object)$row;
-       }
-
-       public function provideMigrationConstruction() {
-               return [
-                       [ SCHEMA_COMPAT_OLD, false ],
-                       [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
-                       [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
-                       [ SCHEMA_COMPAT_NEW, false ],
-                       [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH, true ],
-                       [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, true ],
-                       [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH, true ],
-               ];
-       }
-
-       /**
-        * @covers \MediaWiki\Storage\RevisionStore::__construct
-        * @dataProvider provideMigrationConstruction
-        */
-       public function testMigrationConstruction( $migration, $expectException ) {
-               if ( $expectException ) {
-                       $this->setExpectedException( InvalidArgumentException::class );
-               }
-               $loadBalancer = $this->getMockLoadBalancer();
-               $blobStore = $this->getMockSqlBlobStore();
-               $cache = $this->getHashWANObjectCache();
-               $commentStore = $this->getMockCommentStore();
-               $services = MediaWikiServices::getInstance();
-               $nameTables = $services->getNameTableStoreFactory();
-               $contentModelStore = $nameTables->getContentModels();
-               $slotRoleStore = $nameTables->getSlotRoles();
-               $store = new RevisionStore(
-                       $loadBalancer,
-                       $blobStore,
-                       $cache,
-                       $commentStore,
-                       $nameTables->getContentModels(),
-                       $nameTables->getSlotRoles(),
-                       $migration,
-                       $services->getActorMigration()
-               );
-               if ( !$expectException ) {
-                       $store = TestingAccessWrapper::newFromObject( $store );
-                       $this->assertSame( $loadBalancer, $store->loadBalancer );
-                       $this->assertSame( $blobStore, $store->blobStore );
-                       $this->assertSame( $cache, $store->cache );
-                       $this->assertSame( $commentStore, $store->commentStore );
-                       $this->assertSame( $contentModelStore, $store->contentModelStore );
-                       $this->assertSame( $slotRoleStore, $store->slotRoleStore );
-                       $this->assertSame( $migration, $store->mcrMigrationStage );
-               }
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/SlotRecordTest.php b/tests/phpunit/includes/Storage/SlotRecordTest.php
deleted file mode 100644 (file)
index 0db294e..0000000
+++ /dev/null
@@ -1,408 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use InvalidArgumentException;
-use LogicException;
-use MediaWiki\Storage\IncompleteRevisionException;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SuppressedDataException;
-use MediaWikiTestCase;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Storage\SlotRecord
- */
-class SlotRecordTest extends MediaWikiTestCase {
-
-       private function makeRow( $data = [] ) {
-               $data = $data + [
-                       'slot_id' => 1234,
-                       'slot_content_id' => 33,
-                       'content_size' => '5',
-                       'content_sha1' => 'someHash',
-                       'content_address' => 'tt:456',
-                       'model_name' => CONTENT_MODEL_WIKITEXT,
-                       'format_name' => CONTENT_FORMAT_WIKITEXT,
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '1',
-                       'role_name' => 'myRole',
-               ];
-               return (object)$data;
-       }
-
-       public function testCompleteConstruction() {
-               $row = $this->makeRow();
-               $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasContentId() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertTrue( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getNativeData() );
-               $this->assertSame( 5, $record->getSize() );
-               $this->assertSame( 'someHash', $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 1, $record->getOrigin() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( 33, $record->getContentId() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testConstructionDeferred() {
-               $row = $this->makeRow( [
-                       'content_size' => null, // to be computed
-                       'content_sha1' => null, // to be computed
-                       'format_name' => function () {
-                               return CONTENT_FORMAT_WIKITEXT;
-                       },
-                       'slot_revision_id' => '2',
-                       'slot_origin' => '2',
-                       'slot_content_id' => function () {
-                               return null;
-                       },
-               ] );
-
-               $content = function () {
-                       return new WikitextContent( 'A' );
-               };
-
-               $record = new SlotRecord( $row, $content );
-
-               $this->assertTrue( $record->hasAddress() );
-               $this->assertTrue( $record->hasRevision() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertSame( 'A', $record->getContent()->getNativeData() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotNull( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 2, $record->getRevision() );
-               $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function testNewUnsaved() {
-               $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
-
-               $this->assertFalse( $record->hasAddress() );
-               $this->assertFalse( $record->hasContentId() );
-               $this->assertFalse( $record->hasRevision() );
-               $this->assertFalse( $record->isInherited() );
-               $this->assertFalse( $record->hasOrigin() );
-               $this->assertSame( 'A', $record->getContent()->getNativeData() );
-               $this->assertSame( 1, $record->getSize() );
-               $this->assertNotNull( $record->getSha1() );
-               $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
-               $this->assertSame( 'myRole', $record->getRole() );
-       }
-
-       public function provideInvalidConstruction() {
-               yield 'both null' => [ null, null ];
-               yield 'null row' => [ null, new WikitextContent( 'A' ) ];
-               yield 'array row' => [ [], new WikitextContent( 'A' ) ];
-               yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
-               yield 'null content' => [ (object)[], null ];
-       }
-
-       /**
-        * @dataProvider provideInvalidConstruction
-        */
-       public function testInvalidConstruction( $row, $content ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               new SlotRecord( $row, $content );
-       }
-
-       public function testGetContentId_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getContentId();
-       }
-
-       public function testGetAddress_fails() {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getAddress();
-       }
-
-       public function provideIncomplete() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               yield 'unsaved' => [ $unsaved ];
-
-               $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $inherited = SlotRecord::newInherited( $parent );
-               yield 'inherited' => [ $inherited ];
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetRevision_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getRevision();
-       }
-
-       /**
-        * @dataProvider provideIncomplete
-        */
-       public function testGetOrigin_fails( SlotRecord $record ) {
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-               $this->setExpectedException( IncompleteRevisionException::class );
-
-               $record->getOrigin();
-       }
-
-       public function provideHashStability() {
-               yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
-               yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
-       }
-
-       /**
-        * @dataProvider provideHashStability
-        */
-       public function testHashStability( $text, $hash ) {
-               // Changing the output of the hash function will break things horribly!
-
-               $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
-
-               $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
-               $this->assertSame( $hash, $record->getSha1() );
-       }
-
-       public function testNewWithSuppressedContent() {
-               $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
-               $output = SlotRecord::newWithSuppressedContent( $input );
-
-               $this->setExpectedException( SuppressedDataException::class );
-               $output->getContent();
-       }
-
-       public function testNewInherited() {
-               $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
-               $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, before saving revision meta-data.
-               $inherited = SlotRecord::newInherited( $parent );
-
-               $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
-               $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
-               $this->assertSame( $parent->getContent(), $inherited->getContent() );
-               $this->assertTrue( $inherited->isInherited() );
-               $this->assertTrue( $inherited->hasOrigin() );
-               $this->assertFalse( $inherited->hasRevision() );
-
-               // make sure we didn't mess with the internal state of $parent
-               $this->assertFalse( $parent->isInherited() );
-               $this->assertSame( 7, $parent->getRevision() );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved(
-                       10,
-                       $inherited->getContentId(),
-                       $inherited->getAddress(),
-                       $inherited
-               );
-               $this->assertSame( $parent->getContentId(), $saved->getContentId() );
-               $this->assertSame( $parent->getAddress(), $saved->getAddress() );
-               $this->assertSame( $parent->getContent(), $saved->getContent() );
-               $this->assertTrue( $saved->isInherited() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertSame( 10, $saved->getRevision() );
-
-               // make sure we didn't mess with the internal state of $parent or $inherited
-               $this->assertSame( 7, $parent->getRevision() );
-               $this->assertFalse( $inherited->hasRevision() );
-       }
-
-       public function testNewSaved() {
-               // This would happen while doing an edit, before saving revision meta-data.
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               // This would happen while doing an edit, after saving the revision meta-data
-               // and content meta-data.
-               $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
-               $this->assertFalse( $saved->isInherited() );
-               $this->assertTrue( $saved->hasOrigin() );
-               $this->assertTrue( $saved->hasRevision() );
-               $this->assertTrue( $saved->hasAddress() );
-               $this->assertTrue( $saved->hasContentId() );
-               $this->assertSame( 'theNewAddress', $saved->getAddress() );
-               $this->assertSame( 20, $saved->getContentId() );
-               $this->assertSame( 'A', $saved->getContent()->getNativeData() );
-               $this->assertSame( 10, $saved->getRevision() );
-               $this->assertSame( 10, $saved->getOrigin() );
-
-               // make sure we didn't mess with the internal state of $unsaved
-               $this->assertFalse( $unsaved->hasAddress() );
-               $this->assertFalse( $unsaved->hasContentId() );
-               $this->assertFalse( $unsaved->hasRevision() );
-       }
-
-       public function provideNewSaved_LogicException() {
-               $freshRow = $this->makeRow( [
-                       'content_id' => 10,
-                       'content_address' => 'address:1',
-                       'slot_origin' => 1,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
-               yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
-               yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
-               yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
-
-               $inheritedRow = $this->makeRow( [
-                       'content_id' => null,
-                       'content_address' => null,
-                       'slot_origin' => 0,
-                       'slot_revision_id' => 1,
-               ] );
-
-               $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
-               yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_LogicException
-        */
-       public function testNewSaved_LogicException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( LogicException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideNewSaved_InvalidArgumentException() {
-               $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
-               yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
-               yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
-               yield 'bad content address' => [ 7, 5, 77, $unsaved ];
-       }
-
-       /**
-        * @dataProvider provideNewSaved_InvalidArgumentException
-        */
-       public function testNewSaved_InvalidArgumentException(
-               $revisionId,
-               $contentId,
-               $contentAddress,
-               SlotRecord $protoSlot
-       ) {
-               $this->setExpectedException( InvalidArgumentException::class );
-               SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
-       }
-
-       public function provideHasSameContent() {
-               $fail = function () {
-                       self::fail( 'There should be no need to actually load the content.' );
-               };
-
-               $a100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a1b = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100null = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => null,
-                               ]
-                       ),
-                       $fail
-               );
-               $a100a2 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $b100a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'B',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a1',
-                               ]
-                       ),
-                       $fail
-               );
-               $a200a1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 200,
-                                       'content_sha1' => 'hash-a',
-                                       'content_address' => 'xxx:a2',
-                               ]
-                       ),
-                       $fail
-               );
-               $a100x1 = new SlotRecord(
-                       $this->makeRow(
-                               [
-                                       'model_name' => 'A',
-                                       'content_size' => 100,
-                                       'content_sha1' => 'hash-x',
-                                       'content_address' => 'xxx:x1',
-                               ]
-                       ),
-                       $fail
-               );
-
-               yield 'same instance' => [ $a100a1, $a100a1, true ];
-               yield 'no address' => [ $a100a1, $a100null, true ];
-               yield 'same address' => [ $a100a1, $a100a1b, true ];
-               yield 'different address' => [ $a100a1, $a100a2, true ];
-               yield 'different model' => [ $a100a1, $b100a1, false ];
-               yield 'different size' => [ $a100a1, $a200a1, false ];
-               yield 'different hash' => [ $a100a1, $a100x1, false ];
-       }
-
-       /**
-        * @dataProvider provideHasSameContent
-        */
-       public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
-               $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
-               $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/Storage/create-pre-mcr-fields.sql b/tests/phpunit/includes/Storage/create-pre-mcr-fields.sql
deleted file mode 100644 (file)
index 09deb4f..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE /*_*/revision ADD rev_text_id INTEGER DEFAULT 0;
-ALTER TABLE /*_*/revision  ADD rev_content_model VARBINARY(32) DEFAULT NULL;
-ALTER TABLE /*_*/revision ADD rev_content_format VARBINARY(64) DEFAULT NULL;
diff --git a/tests/phpunit/includes/Storage/drop-mcr-tables.sql b/tests/phpunit/includes/Storage/drop-mcr-tables.sql
deleted file mode 100644 (file)
index bc89edc..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-DROP TABLE /*_*/slots;
-DROP TABLE /*_*/content;
-DROP TABLE /*_*/content_models;
-DROP TABLE /*_*/slot_roles;
diff --git a/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql b/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql
deleted file mode 100644 (file)
index ddfe756..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE /*_*/revision DROP COLUMN rev_text_id;
-ALTER TABLE /*_*/revision DROP COLUMN rev_content_model;
-ALTER TABLE /*_*/revision DROP COLUMN rev_content_format;
diff --git a/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql b/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql
deleted file mode 100644 (file)
index ce7a618..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-DROP TABLE /*_*/revision;
-
-CREATE TABLE /*_*/revision (
-  rev_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-  rev_page INTEGER NOT NULL,
-  rev_comment BLOB NOT NULL,
-  rev_user INTEGER NOT NULL default 0,
-  rev_user_text varchar(255) NOT NULL default '',
-  rev_timestamp blob(14) NOT NULL default '',
-  rev_minor_edit INTEGER NOT NULL default 0,
-  rev_deleted INTEGER NOT NULL default 0,
-  rev_len INTEGER unsigned,
-  rev_parent_id INTEGER default NULL,
-  rev_sha1 varbinary(32) NOT NULL default ''
-) /*$wgDBTableOptions*/;
index baf8243..5aa24e5 100644 (file)
@@ -454,8 +454,6 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * @covers Title::checkUserConfigPermissions
         */
        public function testJsConfigEditPermissions() {
-               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
-                       getFormattedNsText( NS_PROJECT );
                $this->setUser( $this->userName );
 
                $this->setTitle( NS_USER, $this->userName . '/test.js' );
@@ -469,7 +467,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
                        [ [ 'badaccess-group0' ], [ 'mycustomjsprotected', 'bogus' ] ],
                        [ [ 'badaccess-group0' ] ],
-                       [ [ 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ] ]
+                       [ [ 'badaccess-groups' ] ]
                );
        }
 
@@ -494,7 +492,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
                        [ [ 'badaccess-group0' ] ],
                        [ [ 'badaccess-group0' ], [ 'mycustomjsonprotected', 'bogus' ] ],
-                       [ [ 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ] ]
+                       [ [ 'badaccess-groups' ] ]
                );
        }
 
@@ -504,8 +502,6 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * @covers Title::checkUserConfigPermissions
         */
        public function testCssConfigEditPermissions() {
-               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
-                       getFormattedNsText( NS_PROJECT );
                $this->setUser( $this->userName );
 
                $this->setTitle( NS_USER, $this->userName . '/test.css' );
@@ -519,7 +515,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        [ [ 'badaccess-group0' ] ],
                        [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
                        [ [ 'badaccess-group0' ], [ 'mycustomcssprotected', 'bogus' ] ],
-                       [ [ 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ] ]
+                       [ [ 'badaccess-groups' ] ]
                );
        }
 
@@ -529,8 +525,6 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * @covers Title::checkUserConfigPermissions
         */
        public function testOtherJsConfigEditPermissions() {
-               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
-                       getFormattedNsText( NS_PROJECT );
                $this->setUser( $this->userName );
 
                $this->setTitle( NS_USER, $this->altUserName . '/test.js' );
@@ -544,7 +538,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
                        [ [ 'badaccess-group0' ], [ 'customjsprotected', 'bogus' ] ],
                        [ [ 'badaccess-group0' ] ],
-                       [ [ 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ] ]
+                       [ [ 'badaccess-groups' ] ]
                );
        }
 
@@ -554,8 +548,6 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * @covers Title::checkUserConfigPermissions
         */
        public function testOtherJsonConfigEditPermissions() {
-               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
-                       getFormattedNsText( NS_PROJECT );
                $this->setUser( $this->userName );
 
                $this->setTitle( NS_USER, $this->altUserName . '/test.json' );
@@ -569,7 +561,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
                        [ [ 'badaccess-group0' ] ],
                        [ [ 'badaccess-group0' ], [ 'customjsonprotected', 'bogus' ] ],
-                       [ [ 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ] ]
+                       [ [ 'badaccess-groups' ] ]
                );
        }
 
@@ -579,8 +571,6 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * @covers Title::checkUserConfigPermissions
         */
        public function testOtherCssConfigEditPermissions() {
-               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
-                       getFormattedNsText( NS_PROJECT );
                $this->setUser( $this->userName );
 
                $this->setTitle( NS_USER, $this->altUserName . '/test.css' );
@@ -594,7 +584,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        [ [ 'badaccess-group0' ] ],
                        [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
                        [ [ 'badaccess-group0' ], [ 'customcssprotected', 'bogus' ] ],
-                       [ [ 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ] ]
+                       [ [ 'badaccess-groups' ] ]
                );
        }
 
@@ -604,8 +594,6 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * @covers Title::checkUserConfigPermissions
         */
        public function testOtherNonConfigEditPermissions() {
-               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
-                       getFormattedNsText( NS_PROJECT );
                $this->setUser( $this->userName );
 
                $this->setTitle( NS_USER, $this->altUserName . '/tempo' );
@@ -619,7 +607,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        [ [ 'badaccess-group0' ] ],
                        [ [ 'badaccess-group0' ] ],
                        [ [ 'badaccess-group0' ] ],
-                       [ [ 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ] ]
+                       [ [ 'badaccess-groups' ] ]
                );
        }
 
@@ -628,8 +616,6 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
         * @covers Title::checkUserConfigPermissions
         */
        public function testPatrolActionConfigEditPermissions() {
-               $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
-                       getFormattedNsText( NS_PROJECT );
                $this->setUser( 'anon' );
                $this->setTitle( NS_USER, 'ToPatrolOrNotToPatrol' );
                $this->runConfigEditPermissions(
@@ -642,7 +628,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        [ [ 'badaccess-group0' ] ],
                        [ [ 'badaccess-group0' ] ],
                        [ [ 'badaccess-group0' ] ],
-                       [ [ 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ] ]
+                       [ [ 'badaccess-groups' ] ]
                );
        }
 
@@ -686,7 +672,7 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
 
                $this->setUserPerm( '' );
                $result = $this->title->getUserPermissionsErrors( 'patrol', $this->user );
-               $this->assertEquals( $resultPatrol, $result );
+               $this->assertEquals( reset( $resultPatrol[0] ), reset( $result[0] ) );
 
                $this->setUserPerm( [ 'edituserjs', 'edituserjson', 'editusercss' ] );
                $result = $this->title->getUserPermissionsErrors( 'bogus', $this->user );
index 4492141..15486fe 100644 (file)
@@ -13,6 +13,34 @@ use Wikimedia\TestingAccessWrapper;
  * @covers ApiLogin
  */
 class ApiLoginTest extends ApiTestCase {
+       public function setUp() {
+               parent::setUp();
+
+               $this->tablesUsed[] = 'bot_passwords';
+       }
+
+       public static function provideEnableBotPasswords() {
+               return [
+                       'Bot passwords enabled' => [ true ],
+                       'Bot passwords disabled' => [ false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideEnableBotPasswords
+        */
+       public function testExtendedDescription( $enableBotPasswords ) {
+               $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
+               $ret = $this->doApiRequest( [
+                       'action' => 'paraminfo',
+                       'modules' => 'login',
+                       'helpformat' => 'raw',
+               ] );
+               $this->assertSame(
+                       'apihelp-login-extended-description' . ( $enableBotPasswords ? '' : '-nobotpasswords' ),
+                       $ret[0]['paraminfo']['modules'][0]['description'][1]['key']
+               );
+       }
 
        /**
         * Test result of attempted login with an empty username
@@ -30,20 +58,86 @@ class ApiLoginTest extends ApiTestCase {
                $this->assertSame( 'Failed', $ret[0]['login']['result'] );
        }
 
-       private function doUserLogin( $name, $password ) {
+       /**
+        * @dataProvider provideEnableBotPasswords
+        */
+       public function testDeprecatedUserLogin( $enableBotPasswords ) {
+               $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
+
+               $user = $this->getTestUser();
+
                $ret = $this->doApiRequest( [
                        'action' => 'login',
-                       'lgname' => $name,
+                       'lgname' => $user->getUser()->getName(),
                ] );
 
+               $this->assertSame(
+                       [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
+                               'apiwarn-deprecation-login-token' )->text() ) ],
+                       $ret[0]['warnings']['login']
+               );
                $this->assertSame( 'NeedToken', $ret[0]['login']['result'] );
 
-               return $this->doApiRequest( [
+               $ret = $this->doApiRequest( [
                        'action' => 'login',
                        'lgtoken' => $ret[0]['login']['token'],
-                       'lgname' => $name,
-                       'lgpassword' => $password,
+                       'lgname' => $user->getUser()->getName(),
+                       'lgpassword' => $user->getPassword(),
                ], $ret[2] );
+
+               $this->assertSame(
+                       [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
+                               'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )
+                               ->text() ) ],
+                       $ret[0]['warnings']['login']
+               );
+               $this->assertSame(
+                       [
+                               'result' => 'Success',
+                               'lguserid' => $user->getUser()->getId(),
+                               'lgusername' => $user->getUser()->getName(),
+                       ],
+                       $ret[0]['login']
+               );
+       }
+
+       /**
+        * Attempts to log in with the given name and password, retrieves the returned token, and makes
+        * a second API request to actually log in with the token.
+        *
+        * @param string $name
+        * @param string $password
+        * @param array $params To pass to second request
+        * @return array Result of second doApiRequest
+        */
+       private function doUserLogin( $name, $password, array $params = [] ) {
+               $ret = $this->doApiRequest( [
+                       'action' => 'query',
+                       'meta' => 'tokens',
+                       'type' => 'login',
+               ] );
+
+               $this->assertArrayNotHasKey( 'warnings', $ret );
+
+               return $this->doApiRequest( array_merge(
+                       [
+                               'action' => 'login',
+                               'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
+                               'lgname' => $name,
+                               'lgpassword' => $password,
+                       ], $params
+               ), $ret[2] );
+       }
+
+       public function testBadToken() {
+               $user = self::$users['sysop'];
+               $userName = $user->getUser()->getName();
+               $password = $user->getPassword();
+               $user->getUser()->logout();
+
+               $ret = $this->doUserLogin( $userName, $password, [ 'lgtoken' => 'invalid token' ] );
+
+               $this->assertSame( 'WrongToken', $ret[0]['login']['result'] );
        }
 
        public function testBadPass() {
@@ -56,7 +150,12 @@ class ApiLoginTest extends ApiTestCase {
                $this->assertSame( 'Failed', $ret[0]['login']['result'] );
        }
 
-       public function testGoodPass() {
+       /**
+        * @dataProvider provideEnableBotPasswords
+        */
+       public function testGoodPass( $enableBotPasswords ) {
+               $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
+
                $user = self::$users['sysop'];
                $userName = $user->getUser()->getName();
                $password = $user->getPassword();
@@ -65,9 +164,56 @@ class ApiLoginTest extends ApiTestCase {
                $ret = $this->doUserLogin( $userName, $password );
 
                $this->assertSame( 'Success', $ret[0]['login']['result'] );
+               $this->assertSame(
+                       [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
+                               'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )->
+                               text() ) ],
+                       $ret[0]['warnings']['login']
+               );
+       }
+
+       /**
+        * @dataProvider provideEnableBotPasswords
+        */
+       public function testUnsupportedAuthResponseType( $enableBotPasswords ) {
+               $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
+
+               $mockProvider = $this->createMock(
+                       MediaWiki\Auth\AbstractSecondaryAuthenticationProvider::class );
+               $mockProvider->method( 'beginSecondaryAuthentication' )->willReturn(
+                       MediaWiki\Auth\AuthenticationResponse::newUI(
+                               [ new MediaWiki\Auth\UsernameAuthenticationRequest ],
+                               // Slightly silly message here
+                               wfMessage( 'mainpage' )
+                       )
+               );
+               $mockProvider->method( 'getAuthenticationRequests' )
+                       ->willReturn( [] );
+
+               $this->mergeMwGlobalArrayValue( 'wgAuthManagerConfig', [
+                       'secondaryauth' => [ [
+                               'factory' => function () use ( $mockProvider ) {
+                                       return $mockProvider;
+                               },
+                       ] ],
+               ] );
+
+               $user = self::$users['sysop'];
+               $userName = $user->getUser()->getName();
+               $password = $user->getPassword();
+               $user->getUser()->logout();
+
+               $ret = $this->doUserLogin( $userName, $password );
+
+               $this->assertSame( [ 'login' => [
+                       'result' => 'Aborted',
+                       'reason' => ApiErrorFormatter::stripMarkup( wfMessage(
+                               'api-login-fail-aborted' . ( $enableBotPasswords ? '' : '-nobotpw' ) )->text() ),
+               ] ], $ret[0] );
        }
 
        /**
+        * @todo Should this test just be deleted?
         * @group Broken
         */
        public function testGotCookie() {
@@ -114,7 +260,10 @@ class ApiLoginTest extends ApiTestCase {
                );
        }
 
-       public function testBotPassword() {
+       /**
+        * @return [ $username, $password ] suitable for passing to an API request for successful login
+        */
+       private function setUpForBotPassword() {
                global $wgSessionProviders;
 
                $this->setMwGlobals( [
@@ -171,12 +320,51 @@ class ApiLoginTest extends ApiTestCase {
 
                $lgName = $user->getUser()->getName() . BotPassword::getSeparator() . 'foo';
 
-               $ret = $this->doUserLogin( $lgName, $password );
+               return [ $lgName, $password ];
+       }
+
+       public function testBotPassword() {
+               $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
 
                $this->assertSame( 'Success', $ret[0]['login']['result'] );
        }
 
-       public function testLoginWithNoSameOriginSecurity() {
+       public function testBotPasswordThrottled() {
+               global $wgPasswordAttemptThrottle;
+
+               $this->setGroupPermissions( 'sysop', 'noratelimit', false );
+               $this->setMwGlobals( 'wgMainCacheType', 'hash' );
+
+               list( $name, $password ) = $this->setUpForBotPassword();
+
+               for ( $i = 0; $i < $wgPasswordAttemptThrottle[0]['count']; $i++ ) {
+                       $this->doUserLogin( $name, 'incorrectpasswordincorrectpassword' );
+               }
+
+               $ret = $this->doUserLogin( $name, $password );
+
+               $this->assertSame( [
+                       'result' => 'Failed',
+                       'reason' => ApiErrorFormatter::stripMarkup( wfMessage( 'login-throttled' )->
+                               durationParams( $wgPasswordAttemptThrottle[0]['seconds'] )->text() ),
+               ], $ret[0]['login'] );
+       }
+
+       public function testBotPasswordLocked() {
+               $this->setTemporaryHook( 'UserIsLocked', function ( User $unused, &$isLocked ) {
+                       $isLocked = true;
+                       return true;
+               } );
+
+               $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
+
+               $this->assertSame( [
+                       'result' => 'Failed',
+                       'reason' => wfMessage( 'botpasswords-locked' )->text(),
+               ], $ret[0]['login'] );
+       }
+
+       public function testNoSameOriginSecurity() {
                $this->setTemporaryHook( 'RequestHasSameOriginSecurity',
                        function () {
                                return false;
@@ -185,11 +373,15 @@ class ApiLoginTest extends ApiTestCase {
 
                $ret = $this->doApiRequest( [
                        'action' => 'login',
+                       'errorformat' => 'plaintext',
                ] )[0]['login'];
 
                $this->assertSame( [
                        'result' => 'Aborted',
-                       'reason' => 'Cannot log in when the same-origin policy is not applied.',
+                       'reason' => [
+                               'code' => 'api-login-fail-sameorigin',
+                               'text' => 'Cannot log in when the same-origin policy is not applied.',
+                       ],
                ], $ret );
        }
 }
index 8e2c6d9..41ecd52 100644 (file)
@@ -2,7 +2,7 @@
 
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\SlotRecord;
 
 /**
  * @group medium
index ac86377..924a1a5 100644 (file)
@@ -15,7 +15,7 @@ class ApiQueryUserContribsTest extends ApiTestCase {
                        $wgActorTableSchemaMigrationStage = $v;
                        $this->overrideMwServices();
                }, [ $wgActorTableSchemaMigrationStage ] );
-               $wgActorTableSchemaMigrationStage = MIGRATION_WRITE_BOTH;
+               $wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD;
                $this->overrideMwServices();
 
                $users = [
@@ -44,19 +44,12 @@ class ApiQueryUserContribsTest extends ApiTestCase {
 
        /**
         * @dataProvider provideSorting
-        * @param int $stage One of the MIGRATION_* constants for $wgActorTableSchemaMigrationStage
+        * @param int $stage SCHEMA_COMPAT contants for $wgActorTableSchemaMigrationStage
         * @param array $params Extra parameters for the query
         * @param bool $reverse Reverse order?
         * @param int $revs Number of revisions to expect
         */
        public function testSorting( $stage, $params, $reverse, $revs ) {
-               if ( isset( $params['ucuserprefix'] ) &&
-                       ( $stage === MIGRATION_WRITE_BOTH || $stage === MIGRATION_WRITE_NEW ) &&
-                       $this->db->getType() === 'mysql' && $this->usesTemporaryTables()
-               ) {
-                       // https://bugs.mysql.com/bug.php?id=10327
-                       $this->markTestSkipped( 'MySQL bug 10327 - can\'t reopen temporary tables' );
-               }
                // FIXME: fails under sqlite
                $this->markTestSkippedIfDbType( 'sqlite' );
 
@@ -127,10 +120,10 @@ class ApiQueryUserContribsTest extends ApiTestCase {
 
                foreach (
                        [
-                               'old' => MIGRATION_OLD,
-                               'write both' => MIGRATION_WRITE_BOTH,
-                               'write new' => MIGRATION_WRITE_NEW,
-                               'new' => MIGRATION_NEW,
+                               'old' => SCHEMA_COMPAT_OLD,
+                               'read old' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+                               'read new' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+                               'new' => SCHEMA_COMPAT_NEW,
                        ] as $stageName => $stage
                ) {
                        foreach ( [ false, true ] as $reverse ) {
@@ -152,7 +145,7 @@ class ApiQueryUserContribsTest extends ApiTestCase {
 
        /**
         * @dataProvider provideInterwikiUser
-        * @param int $stage One of the MIGRATION_* constants for $wgActorTableSchemaMigrationStage
+        * @param int $stage SCHEMA_COMPAT constants for $wgActorTableSchemaMigrationStage
         */
        public function testInterwikiUser( $stage ) {
                $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', $stage );
@@ -186,10 +179,10 @@ class ApiQueryUserContribsTest extends ApiTestCase {
 
        public static function provideInterwikiUser() {
                return [
-                       'old' => [ MIGRATION_OLD ],
-                       'write both' => [ MIGRATION_WRITE_BOTH ],
-                       'write new' => [ MIGRATION_WRITE_NEW ],
-                       'new' => [ MIGRATION_NEW ],
+                       'old' => [ SCHEMA_COMPAT_OLD ],
+                       'read old' => [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD ],
+                       'read new' => [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW ],
+                       'new' => [ SCHEMA_COMPAT_NEW ],
                ];
        }
 
index 38ccb8a..4b89d25 100644 (file)
@@ -44,6 +44,8 @@ class LegacyHookPreAuthenticationProviderTest extends \MediaWikiTestCase {
                $this->mergeMwGlobalArrayValue( 'wgHooks', [
                        $hook => [ $mock ],
                ] );
+               $mockClass = get_class( $mock );
+               $this->hideDeprecated( "$hook hook (used in $mockClass::on$hook)" );
                return $mock->expects( $expect )->method( "on$hook" );
        }
 
index 64c3224..f207564 100644 (file)
@@ -15,6 +15,14 @@ class ChangeTagsTest extends MediaWikiTestCase {
                $this->tablesUsed[] = 'change_tag_def';
                $this->tablesUsed[] = 'tag_summary';
                $this->tablesUsed[] = 'valid_tag';
+
+               // Truncate these to avoid the supposed-to-be-unused IDs in tests here turning
+               // out to be used, leading ChangeTags::updateTags() to pick up bogus rc_id,
+               // log_id, or rev_id values and run into unique constraint violations.
+               $this->tablesUsed[] = 'recentchanges';
+               $this->tablesUsed[] = 'logging';
+               $this->tablesUsed[] = 'revision';
+               $this->tablesUsed[] = 'archive';
        }
 
        // TODO only modifyDisplayQuery and getSoftwareTags are tested, nothing else is
index e469f12..31d90cb 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Revision\SlotRenderingProvider;
-use MediaWiki\Storage\SlotRecord;
 
 /**
  * @group ContentHandler
index 0795609..936bee0 100644 (file)
@@ -57,7 +57,7 @@ class DatabaseTestHelper extends Database {
                        wfWarn( $msg );
                };
                $this->currentDomain = DatabaseDomain::newUnspecified();
-               $this->open( 'localhost', 'testuser', 'password', 'testdb' );
+               $this->open( 'localhost', 'testuser', 'password', 'testdb', null, '' );
        }
 
        /**
@@ -155,7 +155,7 @@ class DatabaseTestHelper extends Database {
                return 'test';
        }
 
-       function open( $server, $user, $password, $dbName ) {
+       function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
                $this->conn = (object)[ 'test' ];
 
                return true;
index 1616139..e84998c 100644 (file)
@@ -608,7 +608,15 @@ class LBFactoryTest extends MediaWikiTestCase {
                        $this->assertFalse( $db->isOpen() );
                } else {
                        \Wikimedia\suppressWarnings();
-                       $this->assertFalse( $db->selectDB( 'garbage-db' ) );
+                       try {
+                               $this->assertFalse( $db->selectDB( 'garbage-db' ) );
+                               $this->fail( "No error thrown." );
+                       } catch ( \Wikimedia\Rdbms\DBExpectedError $e ) {
+                               $this->assertEquals(
+                                       "Could not select database 'garbage-db'.",
+                                       $e->getMessage()
+                               );
+                       }
                        \Wikimedia\restoreWarnings();
                }
        }
index e21ac3b..4f82ff1 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 use Wikimedia\TestingAccessWrapper;
 
 /**
index a86a1c9..b7dbe0b 100644 (file)
@@ -100,7 +100,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
        private function getMockForViews() {
                $db = $this->getMockBuilder( DatabaseMysqli::class )
                        ->disableOriginalConstructor()
-                       ->setMethods( [ 'fetchRow', 'query' ] )
+                       ->setMethods( [ 'fetchRow', 'query', 'getDBname' ] )
                        ->getMock();
 
                $db->method( 'query' )
@@ -110,6 +110,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
                                (object)[ 'Tables_in_' => 'view2' ],
                                (object)[ 'Tables_in_' => 'myview' ]
                        ] ) );
+               $db->method( 'getDBname' )->willReturn( '' );
 
                return $db;
        }
@@ -677,7 +678,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
        public function testIndexAliases() {
                $db = $this->getMockBuilder( DatabaseMysqli::class )
                        ->disableOriginalConstructor()
-                       ->setMethods( [ 'mysqlRealEscapeString' ] )
+                       ->setMethods( [ 'mysqlRealEscapeString', 'dbSchema', 'tablePrefix' ] )
                        ->getMock();
                $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
                        function ( $s ) {
@@ -710,7 +711,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
        public function testTableAliases() {
                $db = $this->getMockBuilder( DatabaseMysqli::class )
                        ->disableOriginalConstructor()
-                       ->setMethods( [ 'mysqlRealEscapeString' ] )
+                       ->setMethods( [ 'mysqlRealEscapeString', 'dbSchema', 'tablePrefix' ] )
                        ->getMock();
                $db->method( 'mysqlRealEscapeString' )->willReturnCallback(
                        function ( $s ) {
index abde37a..762812c 100644 (file)
@@ -218,9 +218,10 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
         * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
         */
        public function testTransactionIdle_TRX() {
-               $db = $this->getMockDB( [ 'isOpen', 'ping' ] );
+               $db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
                $db->method( 'isOpen' )->willReturn( true );
                $db->method( 'ping' )->willReturn( true );
+               $db->method( 'getDBname' )->willReturn( '' );
                $db->setFlag( DBO_TRX );
 
                $lbFactory = LBFactorySingle::newFromConnection( $db );
@@ -311,9 +312,10 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
         * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
         */
        public function testTransactionPreCommitOrIdle_TRX() {
-               $db = $this->getMockDB( [ 'isOpen', 'ping' ] );
+               $db = $this->getMockDB( [ 'isOpen', 'ping', 'getDBname' ] );
                $db->method( 'isOpen' )->willReturn( true );
                $db->method( 'ping' )->willReturn( true );
+               $db->method( 'getDBname' )->willReturn( 'unittest' );
                $db->setFlag( DBO_TRX );
 
                $lbFactory = LBFactorySingle::newFromConnection( $db );
@@ -484,8 +486,9 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
         * @covers Wikimedia\Rdbms\Database::lockIsFree
         */
        public function testGetScopedLock() {
-               $db = $this->getMockDB( [ 'isOpen' ] );
+               $db = $this->getMockDB( [ 'isOpen', 'getDBname' ] );
                $db->method( 'isOpen' )->willReturn( true );
+               $db->method( 'getDBname' )->willReturn( 'unittest' );
 
                $this->assertEquals( 0, $db->trxLevel() );
                $this->assertEquals( true, $db->lockIsFree( 'x', __METHOD__ ) );
@@ -625,21 +628,57 @@ class DatabaseTest extends PHPUnit\Framework\TestCase {
         * @covers Wikimedia\Rdbms\Database::tablePrefix
         * @covers Wikimedia\Rdbms\Database::dbSchema
         */
-       public function testMutators() {
+       public function testSchemaAndPrefixMutators() {
                $old = $this->db->tablePrefix();
+               $oldDomain = $this->db->getDomainId();
                $this->assertInternalType( 'string', $old, 'Prefix is string' );
-               $this->assertEquals( $old, $this->db->tablePrefix(), "Prefix unchanged" );
-               $this->assertEquals( $old, $this->db->tablePrefix( 'xxx' ) );
-               $this->assertEquals( 'xxx', $this->db->tablePrefix(), "Prefix set" );
+               $this->assertSame( $old, $this->db->tablePrefix(), "Prefix unchanged" );
+               $this->assertSame( $old, $this->db->tablePrefix( 'xxx' ) );
+               $this->assertSame( 'xxx', $this->db->tablePrefix(), "Prefix set" );
                $this->db->tablePrefix( $old );
                $this->assertNotEquals( 'xxx', $this->db->tablePrefix() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
 
                $old = $this->db->dbSchema();
+               $oldDomain = $this->db->getDomainId();
                $this->assertInternalType( 'string', $old, 'Schema is string' );
-               $this->assertEquals( $old, $this->db->dbSchema(), "Schema unchanged" );
-               $this->assertEquals( $old, $this->db->dbSchema( 'xxx' ) );
-               $this->assertEquals( 'xxx', $this->db->dbSchema(), "Schema set" );
+               $this->assertSame( $old, $this->db->dbSchema(), "Schema unchanged" );
+               $this->assertSame( $old, $this->db->dbSchema( 'xxx' ) );
+               $this->assertSame( 'xxx', $this->db->dbSchema(), "Schema set" );
                $this->db->dbSchema( $old );
                $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
+       }
+
+       /**
+        * @covers Wikimedia\Rdbms\Database::selectDomain
+        */
+       public function testSelectDomain() {
+               $oldDomain = $this->db->getDomainId();
+               $oldDatabase = $this->db->getDBname();
+               $oldSchema = $this->db->dbSchema();
+               $oldPrefix = $this->db->tablePrefix();
+
+               $this->db->selectDomain( 'testselectdb-xxx' );
+               $this->assertSame( 'testselectdb', $this->db->getDBname() );
+               $this->assertSame( '', $this->db->dbSchema() );
+               $this->assertSame( 'xxx', $this->db->tablePrefix() );
+
+               $this->db->selectDomain( $oldDomain );
+               $this->assertSame( $oldDatabase, $this->db->getDBname() );
+               $this->assertSame( $oldSchema, $this->db->dbSchema() );
+               $this->assertSame( $oldPrefix, $this->db->tablePrefix() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
+
+               $this->db->selectDomain( 'testselectdb-schema-xxx' );
+               $this->assertSame( 'testselectdb', $this->db->getDBname() );
+               $this->assertSame( 'schema', $this->db->dbSchema() );
+               $this->assertSame( 'xxx', $this->db->tablePrefix() );
+
+               $this->db->selectDomain( $oldDomain );
+               $this->assertSame( $oldDatabase, $this->db->getDBname() );
+               $this->assertSame( $oldSchema, $this->db->dbSchema() );
+               $this->assertSame( $oldPrefix, $this->db->tablePrefix() );
+               $this->assertSame( $oldDomain, $this->db->getDomainId() );
        }
 }
index 4af1742..e75b173 100644 (file)
@@ -29,17 +29,19 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
         * @param array $selectFields
         * @param string[]|null $row
         * @param string[]|null $expectedFields
-        * @param string $migration
+        * @param int $commentMigration
+        * @param int $actorMigration
         */
        public function testNewFromId( $id,
                array $selectFields,
                array $row = null,
                array $expectedFields = null,
-               $migration
+               $commentMigration,
+               $actorMigration
        ) {
                $this->setMwGlobals( [
-                       'wgCommentTableSchemaMigrationStage' => $migration,
-                       'wgActorTableSchemaMigrationStage' => $migration,
+                       'wgCommentTableSchemaMigrationStage' => $commentMigration,
+                       'wgActorTableSchemaMigrationStage' => $actorMigration,
                ] );
 
                $row = $row ? (object)$row : null;
@@ -132,6 +134,7 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
                                null,
                                null,
                                MIGRATION_OLD,
+                               SCHEMA_COMPAT_OLD,
                        ],
                        [
                                123,
@@ -144,6 +147,7 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
                                ],
                                [ 'type' => 'foobarize', 'comment' => 'test!' ],
                                MIGRATION_OLD,
+                               SCHEMA_COMPAT_OLD,
                        ],
                        [
                                567,
@@ -156,6 +160,7 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
                                ],
                                [ 'type' => 'foobarize', 'comment' => 'test!' ],
                                MIGRATION_NEW,
+                               SCHEMA_COMPAT_NEW,
                        ],
                ];
        }
index 629621e..466e209 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 use PHPUnit\Framework\MockObject\MockObject;
 
 /**
index d2a8016..1e16097 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Tests\Storage\McrSchemaOverride;
+use MediaWiki\Tests\Revision\McrSchemaOverride;
 
 /**
  * Test class for page archiving, using the new MCR schema.
index 476d5c2..4df4ee1 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\SqlBlobStore;
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
 
 /**
  * Test class for page archiving, using the pre-MCR schema.
index 27e5861..ade8efd 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
 
 /**
  * Base class for tests of PageArchive against different database schemas.
@@ -81,7 +81,7 @@ abstract class PageArchiveTestBase extends MediaWikiTestCase {
                $this->tablesUsed += $this->getMcrTablesToReset();
 
                $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD );
                $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() );
                $this->setMwGlobals(
                        'wgMultiContentRevisionSchemaMigrationStage',
index 67cbf58..d9c92f5 100644 (file)
@@ -2,8 +2,8 @@
 
 use MediaWiki\Edit\PreparedEdit;
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\SlotRecord;
 use PHPUnit\Framework\MockObject\MockObject;
 use Wikimedia\TestingAccessWrapper;
 
@@ -808,6 +808,14 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase {
                                "#REDIRECT [[hello world]]",
                                "Hello world"
                        ],
+                       // The below added to protect against Media namespace
+                       // redirects which throw a fatal: (T203942)
+                       [
+                               'WikiPageTest_testGetRedirectTarget_3',
+                               CONTENT_MODEL_WIKITEXT,
+                               "#REDIRECT [[Media:hello_world]]",
+                               "File:Hello world"
+                       ],
                ];
        }
 
index 02567f8..fa98b52 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-use MediaWiki\Tests\Storage\McrSchemaOverride;
+use MediaWiki\Tests\Revision\McrSchemaOverride;
 
 /**
  * Tests WikiPage against the MCR DB schema after schema migration.
index 458e415..69d12e3 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-use MediaWiki\Tests\Storage\McrReadNewSchemaOverride;
+use MediaWiki\Tests\Revision\McrReadNewSchemaOverride;
 
 /**
  * Tests WikiPage against the intermediate MCR DB schema for use during schema migration.
index 78bbfa7..ef1cb63 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-use MediaWiki\Tests\Storage\McrWriteBothSchemaOverride;
+use MediaWiki\Tests\Revision\McrWriteBothSchemaOverride;
 
 /**
  * Tests WikiPage against the intermediate MCR DB schema for use during schema migration.
index d849124..3677919 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
 
 /**
  * Tests WikiPage against the pre-MCR, pre ContentHandler DB schema.
index 3e7c8fa..b654974 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
 
 /**
  * Tests WikiPage against the pre-MCR DB schema.
index 3e857f0..291d75b 100644 (file)
@@ -1,7 +1,7 @@
 <?php
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
 use MediaWiki\User\UserIdentityValue;
 
 /**
index 47adfc0..19774f0 100644 (file)
@@ -1,8 +1,8 @@
 <?php
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
 
 /**
  * @covers PoolWorkArticleView
index b874215..b8cee67 100644 (file)
@@ -197,15 +197,16 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testRcHidemyselfFilter() {
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->setMwGlobals(
+                       'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+               );
                $this->overrideMwServices();
 
                $user = $this->getTestUser()->getUser();
                $user->getActorId( wfGetDB( DB_MASTER ) );
                $this->assertConditions(
                        [ # expected
-                               "NOT((rc_actor = '{$user->getActorId()}') OR "
-                                       . "(rc_actor = '0' AND rc_user = '{$user->getId()}'))",
+                               "NOT((rc_user = '{$user->getId()}'))",
                        ],
                        [
                                'hidemyself' => 1,
@@ -218,7 +219,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                $id = $user->getActorId( wfGetDB( DB_MASTER ) );
                $this->assertConditions(
                        [ # expected
-                               "NOT((rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13'))",
+                               "NOT((rc_user_text = '10.11.12.13'))",
                        ],
                        [
                                'hidemyself' => 1,
@@ -229,15 +230,16 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testRcHidebyothersFilter() {
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->setMwGlobals(
+                       'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+               );
                $this->overrideMwServices();
 
                $user = $this->getTestUser()->getUser();
                $user->getActorId( wfGetDB( DB_MASTER ) );
                $this->assertConditions(
                        [ # expected
-                               "(rc_actor = '{$user->getActorId()}') OR "
-                               . "(rc_actor = '0' AND rc_user_text = '{$user->getName()}')",
+                               "(rc_user_text = '{$user->getName()}')",
                        ],
                        [
                                'hidebyothers' => 1,
@@ -250,7 +252,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                $id = $user->getActorId( wfGetDB( DB_MASTER ) );
                $this->assertConditions(
                        [ # expected
-                               "(rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13')",
+                               "(rc_user_text = '10.11.12.13')",
                        ],
                        [
                                'hidebyothers' => 1,
@@ -462,13 +464,15 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelAllExperienceLevels() {
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->setMwGlobals(
+                       'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+               );
                $this->overrideMwServices();
 
                $this->assertConditions(
                        [
                                # expected
-                               'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
+                               'rc_user != 0',
                        ],
                        [
                                'userExpLevel' => 'newcomer;learner;experienced',
@@ -478,13 +482,15 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelRegistrered() {
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->setMwGlobals(
+                       'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+               );
                $this->overrideMwServices();
 
                $this->assertConditions(
                        [
                                # expected
-                               'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
+                               'rc_user != 0',
                        ],
                        [
                                'userExpLevel' => 'registered',
@@ -494,13 +500,15 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelUnregistrered() {
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->setMwGlobals(
+                       'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+               );
                $this->overrideMwServices();
 
                $this->assertConditions(
                        [
                                # expected
-                               'COALESCE( actor_rc_user.actor_user, rc_user ) = 0',
+                               'rc_user = 0',
                        ],
                        [
                                'userExpLevel' => 'unregistered',
@@ -510,13 +518,15 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelRegistreredOrLearner() {
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->setMwGlobals(
+                       'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+               );
                $this->overrideMwServices();
 
                $this->assertConditions(
                        [
                                # expected
-                               'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
+                               'rc_user != 0',
                        ],
                        [
                                'userExpLevel' => 'registered;learner',
@@ -526,13 +536,15 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelUnregistreredOrExperienced() {
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->setMwGlobals(
+                       'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
+               );
                $this->overrideMwServices();
 
                $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
 
                $this->assertRegExp(
-                       '/\(COALESCE\( actor_rc_user.actor_user, rc_user \) = 0\) OR '
+                       '/\(rc_user = 0\) OR '
                                . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
                        reset( $conds ),
                        "rc conditions: userExpLevel=unregistered;experienced"
index 0d22b21..bc0946f 100644 (file)
@@ -248,13 +248,13 @@ class BotPasswordTest extends MediaWikiTestCase {
                        [ 'user', 'abc@def', false ],
                        [ 'legacy@user', 'pass', false ],
                        [ 'user@bot', '12345678901234567890123456789012',
-                               [ 'user@bot', '12345678901234567890123456789012', true ] ],
+                               [ 'user@bot', '12345678901234567890123456789012' ] ],
                        [ 'user', 'bot@12345678901234567890123456789012',
-                               [ 'user@bot', '12345678901234567890123456789012', true ] ],
+                               [ 'user@bot', '12345678901234567890123456789012' ] ],
                        [ 'user', 'bot@12345678901234567890123456789012345',
-                               [ 'user@bot', '12345678901234567890123456789012345', true ] ],
+                               [ 'user@bot', '12345678901234567890123456789012345' ] ],
                        [ 'user', 'bot@x@12345678901234567890123456789012',
-                               [ 'user@bot@x', '12345678901234567890123456789012', true ] ],
+                               [ 'user@bot@x', '12345678901234567890123456789012' ] ],
                ];
        }
 
index f86987a..cee15e8 100644 (file)
@@ -22,7 +22,7 @@ class UserTest extends MediaWikiTestCase {
                $this->setMwGlobals( [
                        'wgGroupPermissions' => [],
                        'wgRevokePermissions' => [],
-                       'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+                       'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
                ] );
                $this->overrideMwServices();