Add `actor` table and code to start using it
authorBrad Jorsch <bjorsch@wikimedia.org>
Tue, 12 Sep 2017 17:12:29 +0000 (13:12 -0400)
committerJames D. Forrester <jforrester@wikimedia.org>
Fri, 23 Feb 2018 18:06:20 +0000 (10:06 -0800)
Storing the user name or IP in every row in large tables like revision
and logging takes up space and makes operations on these tables slower.
This patch begins the process of moving those into one "actor" table
which other tables can reference with a single integer field.

A subsequent patch will remove the old columns.

Bug: T167246
Depends-On: I9293fd6e0f958d87e52965de925046f1bb8f8a50
Change-Id: I8d825eb02c69cc66d90bd41325133fd3f99f0226

126 files changed:
RELEASE-NOTES-1.31
autoload.php
includes/ActorMigration.php [new file with mode: 0644]
includes/Block.php
includes/DefaultSettings.php
includes/EditPage.php
includes/Linker.php
includes/MediaWikiServices.php
includes/Revision.php
includes/RevisionList.php
includes/ServiceWiring.php
includes/Storage/RevisionStore.php
includes/Title.php
includes/actions/InfoAction.php
includes/api/ApiQueryAllDeletedRevisions.php
includes/api/ApiQueryAllImages.php
includes/api/ApiQueryAllRevisions.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryBlocks.php
includes/api/ApiQueryContributors.php
includes/api/ApiQueryDeletedRevisions.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryLogEvents.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryRevisions.php
includes/api/ApiQueryUserContributions.php
includes/api/ApiStashEdit.php
includes/cache/UserCache.php
includes/changes/ChangesList.php
includes/changes/RecentChange.php
includes/changetags/ChangeTagsLogItem.php
includes/content/ContentHandler.php
includes/db/DatabaseOracle.php
includes/deferred/SiteStatsUpdate.php
includes/exception/CannotCreateActorException.php [new file with mode: 0644]
includes/export/WikiExporter.php
includes/filerepo/file/ArchivedFile.php
includes/filerepo/file/LocalFile.php
includes/filerepo/file/OldLocalFile.php
includes/import/WikiRevision.php
includes/installer/DatabaseUpdater.php
includes/installer/MssqlUpdater.php
includes/installer/MysqlUpdater.php
includes/installer/OracleUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteUpdater.php
includes/jobqueue/jobs/RecentChangesUpdateJob.php
includes/libs/rdbms/database/IDatabase.php
includes/logging/LogEntry.php
includes/logging/LogPage.php
includes/logging/LogPager.php
includes/page/WikiPage.php
includes/revisiondelete/RevDelArchiveItem.php
includes/revisiondelete/RevDelArchivedFileItem.php
includes/revisiondelete/RevDelFileItem.php
includes/revisiondelete/RevDelList.php
includes/revisiondelete/RevDelLogItem.php
includes/revisiondelete/RevDelLogList.php
includes/revisiondelete/RevDelRevisionItem.php
includes/revisiondelete/RevisionDeleteUser.php
includes/specialpage/ChangesListSpecialPage.php
includes/specials/SpecialFileDuplicateSearch.php
includes/specials/SpecialLog.php
includes/specials/SpecialMIMEsearch.php
includes/specials/SpecialNewpages.php
includes/specials/SpecialRedirect.php
includes/specials/pagers/ActiveUsersPager.php
includes/specials/pagers/BlockListPager.php
includes/specials/pagers/ContribsPager.php
includes/specials/pagers/DeletedContribsPager.php
includes/specials/pagers/ImageListPager.php
includes/specials/pagers/NewFilesPager.php
includes/specials/pagers/NewPagesPager.php
includes/specials/pagers/ProtectedPagesPager.php
includes/user/User.php
includes/user/UserIdentity.php
includes/user/UserIdentityValue.php
includes/watcheditem/WatchedItemQueryService.php
maintenance/archives/patch-actor-table.sql [new file with mode: 0644]
maintenance/deleteDefaultMessages.php
maintenance/fixUserRegistration.php
maintenance/initEditCount.php
maintenance/migrateActors.php [new file with mode: 0644]
maintenance/mssql/archives/patch-actor-table.sql [new file with mode: 0644]
maintenance/mssql/tables.sql
maintenance/oracle/archives/patch-actor-table.sql [new file with mode: 0644]
maintenance/oracle/tables.sql
maintenance/orphans.php
maintenance/populateIpChanges.php
maintenance/populateLogSearch.php
maintenance/populateLogUsertext.php
maintenance/postgres/archives/patch-actor-table.sql [new file with mode: 0644]
maintenance/postgres/tables.sql
maintenance/reassignEdits.php
maintenance/rebuildrecentchanges.php
maintenance/removeUnusedAccounts.php
maintenance/rollbackEdits.php
maintenance/sqlite/archives/patch-actor-table.sql [new file with mode: 0644]
maintenance/tables.sql
tests/parser/ParserTestRunner.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/ActorMigrationTest.php [new file with mode: 0644]
tests/phpunit/includes/BlockTest.php
tests/phpunit/includes/CommentStoreTest.php
tests/phpunit/includes/PageArchiveTest.php
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/Storage/RevisionStoreDbTest.php
tests/phpunit/includes/Storage/RevisionStoreRecordTest.php
tests/phpunit/includes/Storage/RevisionStoreTest.php
tests/phpunit/includes/api/ApiBaseTest.php
tests/phpunit/includes/api/ApiQueryRecentChangesIntegrationTest.php
tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php
tests/phpunit/includes/api/query/ApiQueryUserContributionsTest.php [new file with mode: 0644]
tests/phpunit/includes/auth/AuthManagerTest.php
tests/phpunit/includes/auth/CheckBlocksSecondaryAuthenticationProviderTest.php
tests/phpunit/includes/changes/RecentChangeTest.php
tests/phpunit/includes/import/ImportTest.php
tests/phpunit/includes/logging/LogFormatterTestCase.php
tests/phpunit/includes/page/WikiPageDbTestBase.php
tests/phpunit/includes/password/UserPasswordPolicyTest.php
tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php
tests/phpunit/includes/user/UserGroupMembershipTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php

index ed1c4df..f79747a 100644 (file)
@@ -51,6 +51,18 @@ production.
 * Style tags with a 'data-mw-deduplicate' attribute will be deduplicated as a
   ParserOutput::getText() post-cache transformation. This may be disabled by
   passing 'deduplicateStyles' => false to that method.
+* The identity of the logged-in or IP "actor" for logged actions is being moved
+  into a new actor table, with the rows in tables such as revision and logging
+  referring to the actor ID instead of storing the user ID and name/IP in
+  every row.
+  * This is currently gated by $wgActorTableSchemaMigrationStage. Most wikis
+    can set this to MIGRATION_NEW and run maintenance/migrateActors.php as
+    soon as any necessary extensions are updated.
+  * Most code accessing rows for logged actions from the database should use
+    the relevant getQueryInfo() methods to get the information needed to build
+    the SQL query. The ActorMigration class may also be used to get feature-flagged
+    information needed to access actor-related fields during the migration
+    period.
 
 === External library changes in 1.31 ===
 
index 7f90d47..cff05b8 100644 (file)
@@ -11,6 +11,7 @@ $wgAutoloadLocalClasses = [
        'Action' => __DIR__ . '/includes/actions/Action.php',
        'ActiveUsersPager' => __DIR__ . '/includes/specials/pagers/ActiveUsersPager.php',
        'ActivityUpdateJob' => __DIR__ . '/includes/jobqueue/jobs/ActivityUpdateJob.php',
+       'ActorMigration' => __DIR__ . '/includes/ActorMigration.php',
        'AddRFCAndPMIDInterwiki' => __DIR__ . '/maintenance/addRFCandPMIDInterwiki.php',
        'AddSite' => __DIR__ . '/maintenance/addSite.php',
        'AjaxDispatcher' => __DIR__ . '/includes/AjaxDispatcher.php',
@@ -220,6 +221,7 @@ $wgAutoloadLocalClasses = [
        'CachedAction' => __DIR__ . '/includes/actions/CachedAction.php',
        'CachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/CachedBagOStuff.php',
        'CachingSiteStore' => __DIR__ . '/includes/site/CachingSiteStore.php',
+       'CannotCreateActorException' => __DIR__ . '/includes/exception/CannotCreateActorException.php',
        'CapsCleanup' => __DIR__ . '/maintenance/cleanupCaps.php',
        'CategoriesRdf' => __DIR__ . '/includes/CategoriesRdf.php',
        'Category' => __DIR__ . '/includes/Category.php',
@@ -1020,6 +1022,7 @@ $wgAutoloadLocalClasses = [
        'MessageContent' => __DIR__ . '/includes/content/MessageContent.php',
        'MessageLocalizer' => __DIR__ . '/languages/MessageLocalizer.php',
        'MessageSpecifier' => __DIR__ . '/includes/libs/MessageSpecifier.php',
+       'MigrateActors' => __DIR__ . '/maintenance/migrateActors.php',
        'MigrateArchiveText' => __DIR__ . '/maintenance/migrateArchiveText.php',
        'MigrateComments' => __DIR__ . '/maintenance/migrateComments.php',
        'MigrateFileRepoLayout' => __DIR__ . '/maintenance/migrateFileRepoLayout.php',
diff --git a/includes/ActorMigration.php b/includes/ActorMigration.php
new file mode 100644 (file)
index 0000000..161c7a9
--- /dev/null
@@ -0,0 +1,383 @@
+<?php
+/**
+ * Methods to help with the actor table migration
+ *
+ * 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
+ */
+
+use MediaWiki\MediaWikiServices;
+use MediaWiki\User\UserIdentity;
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * This class handles the logic for the actor table migration.
+ *
+ * This is not intended to be a long-term part of MediaWiki; it will be
+ * deprecated and removed along with $wgActorTableSchemaMigrationStage.
+ *
+ * @since 1.31
+ */
+class ActorMigration {
+
+       /**
+        * Define fields that use temporary tables for transitional purposes
+        * @var array Keys are '$key', values are arrays with four fields:
+        *  - table: Temporary table name
+        *  - pk: Temporary table column referring to the main table's primary key
+        *  - field: Temporary table column referring actor.actor_id
+        *  - joinPK: Main table's primary key
+        */
+       private static $tempTables = [
+               'rev_user' => [
+                       'table' => 'revision_actor_temp',
+                       'pk' => 'revactor_rev',
+                       'field' => 'revactor_actor',
+                       'joinPK' => 'rev_id',
+                       'extra' => [
+                               'revactor_timestamp' => 'rev_timestamp',
+                               'revactor_page' => 'rev_page',
+                       ],
+               ],
+       ];
+
+       /**
+        * Fields that formerly used $tempTables
+        * @var array Key is '$key', value is the MediaWiki version in which it was
+        *  removed from $tempTables.
+        */
+       private static $formerTempTables = [];
+
+       /**
+        * Define fields that use non-standard mapping
+        * @var array Keys are the user id column name, values are arrays with two
+        *  elements (the user text column name and the actor id column name)
+        */
+       private static $specialFields = [
+               'ipb_by' => [ 'ipb_by_text', 'ipb_by_actor' ],
+       ];
+
+       /** @var array|null Cache for `self::getJoin()` */
+       private $joinCache = null;
+
+       /** @var int One of the MIGRATION_* constants */
+       private $stage;
+
+       /** @private */
+       public function __construct( $stage ) {
+               $this->stage = $stage;
+       }
+
+       /**
+        * Static constructor
+        * @return ActorMigration
+        */
+       public static function newMigration() {
+               return MediaWikiServices::getInstance()->getActorMigration();
+       }
+
+       /**
+        * Return an SQL condition to test if a user field is anonymous
+        * @param string $field Field name or SQL fragment
+        * @return string
+        */
+       public function isAnon( $field ) {
+               return $this->stage === MIGRATION_NEW ? "$field IS NULL" : "$field = 0";
+       }
+
+       /**
+        * Return an SQL condition to test if a user field is non-anonymous
+        * @param string $field Field name or SQL fragment
+        * @return string
+        */
+       public function isNotAnon( $field ) {
+               return $this->stage === MIGRATION_NEW ? "$field IS NOT NULL" : "$field != 0";
+       }
+
+       /**
+        * @param string $key A key such as "rev_user" identifying the actor
+        *  field being fetched.
+        * @return string[] [ $text, $actor ]
+        */
+       private static function getFieldNames( $key ) {
+               if ( isset( self::$specialFields[$key] ) ) {
+                       return self::$specialFields[$key];
+               }
+
+               return [ $key . '_text', substr( $key, 0, -5 ) . '_actor' ];
+       }
+
+       /**
+        * Get SELECT fields and joins for the actor key
+        *
+        * @param string $key A key such as "rev_user" identifying the actor
+        *  field being fetched.
+        * @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()`
+        *  All tables, fields, and joins are aliased, so `+` is safe to use.
+        */
+       public function getJoin( $key ) {
+               if ( !isset( $this->joinCache[$key] ) ) {
+                       $tables = [];
+                       $fields = [];
+                       $joins = [];
+
+                       list( $text, $actor ) = self::getFieldNames( $key );
+
+                       if ( $this->stage === MIGRATION_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']}" ];
+                                       $joinField = "{$alias}.{$t['field']}";
+                               } else {
+                                       $joinField = $actor;
+                               }
+
+                               $alias = "actor_$key";
+                               $tables[$alias] = 'actor';
+                               $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[$actor] = $joinField;
+                       }
+
+                       $this->joinCache[$key] = [
+                               'tables' => $tables,
+                               'fields' => $fields,
+                               'joins' => $joins,
+                       ];
+               }
+
+               return $this->joinCache[$key];
+       }
+
+       /**
+        * Get UPDATE fields for the actor
+        *
+        * @param IDatabase $dbw Database to use for creating an actor ID, if necessary
+        * @param string $key A key such as "rev_user" identifying the actor
+        *  field being fetched.
+        * @param UserIdentity $user User to set in the update
+        * @return array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()`
+        */
+       public function getInsertValues( IDatabase $dbw, $key, UserIdentity $user ) {
+               if ( isset( self::$tempTables[$key] ) ) {
+                       throw new InvalidArgumentException( "Must use getInsertValuesWithTempTable() for $key" );
+               }
+
+               list( $text, $actor ) = self::getFieldNames( $key );
+               $ret = [];
+               if ( $this->stage <= MIGRATION_WRITE_BOTH ) {
+                       $ret[$key] = $user->getId();
+                       $ret[$text] = $user->getName();
+               }
+               if ( $this->stage >= MIGRATION_WRITE_BOTH ) {
+                       // 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 );
+                       }
+                       $ret[$actor] = $user->getActorId( $dbw );
+               }
+               return $ret;
+       }
+
+       /**
+        * Get UPDATE fields for the actor
+        *
+        * @param IDatabase $dbw Database to use for creating an actor ID, if necessary
+        * @param string $key A key such as "rev_user" identifying the actor
+        *  field being fetched.
+        * @param UserIdentity $user User to set in the update
+        * @return array with two values:
+        *  - array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()`
+        *  - callback to call with the the primary key for the main table insert
+        *    and extra fields needed for the temp table.
+        */
+       public function getInsertValuesWithTempTable( IDatabase $dbw, $key, UserIdentity $user ) {
+               if ( isset( self::$formerTempTables[$key] ) ) {
+                       wfDeprecated( __METHOD__ . " for $key", self::$formerTempTables[$key] );
+               } elseif ( !isset( self::$tempTables[$key] ) ) {
+                       throw new InvalidArgumentException( "Must use getInsertValues() for $key" );
+               }
+
+               list( $text, $actor ) = self::getFieldNames( $key );
+               $ret = [];
+               $callback = null;
+               if ( $this->stage <= MIGRATION_WRITE_BOTH ) {
+                       $ret[$key] = $user->getId();
+                       $ret[$text] = $user->getName();
+               }
+               if ( $this->stage >= MIGRATION_WRITE_BOTH ) {
+                       // 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 );
+                       }
+                       $id = $user->getActorId( $dbw );
+
+                       if ( isset( self::$tempTables[$key] ) ) {
+                               $func = __METHOD__;
+                               $callback = function ( $pk, array $extra ) use ( $dbw, $key, $id, $func ) {
+                                       $t = self::$tempTables[$key];
+                                       $set = [ $t['field'] => $id ];
+                                       foreach ( $t['extra'] as $to => $from ) {
+                                               if ( !array_key_exists( $from, $extra ) ) {
+                                                       throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
+                                               }
+                                               $set[$to] = $extra[$from];
+                                       }
+                                       $dbw->upsert(
+                                               $t['table'],
+                                               [ $t['pk'] => $pk ] + $set,
+                                               [ $t['pk'] ],
+                                               $set,
+                                               $func
+                                       );
+                               };
+                       } else {
+                               $ret[$actor] = $id;
+                               $callback = function ( $pk, array $extra ) {
+                               };
+                       }
+               } elseif ( isset( self::$tempTables[$key] ) ) {
+                       $func = __METHOD__;
+                       $callback = function ( $pk, array $extra ) use ( $key, $func ) {
+                               $t = self::$tempTables[$key];
+                               foreach ( $t['extra'] as $to => $from ) {
+                                       if ( !array_key_exists( $from, $extra ) ) {
+                                               throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
+                                       }
+                               }
+                       };
+               } else {
+                       $callback = function ( $pk, array $extra ) {
+                       };
+               }
+               return [ $ret, $callback ];
+       }
+
+       /**
+        * Get WHERE condition for the actor
+        *
+        * @param IDatabase $db Database to use for quoting and list-making
+        * @param string $key A key such as "rev_user" identifying the actor
+        *  field being fetched.
+        * @param UserIdentity|UserIdentity[] $users Users to test for
+        * @param bool $useId If false, don't try to query by the user ID.
+        *  Intended for use with rc_user since it has an index on
+        *  (rc_user_text,rc_timestamp) but not (rc_user,rc_timestamp).
+        * @return array With three keys:
+        *   - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+        *   - conds: (string) to include in the `$cond` to `IDatabase->select()`
+        *   - 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'.
+        *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+        *  All tables and joins are aliased, so `+` is safe to use.
+        */
+       public function getWhere( IDatabase $db, $key, $users, $useId = true ) {
+               $tables = [];
+               $conds = [];
+               $joins = [];
+
+               if ( $users instanceof UserIdentity ) {
+                       $users = [ $users ];
+               }
+
+               // Get information about all the passed users
+               $ids = [];
+               $names = [];
+               $actors = [];
+               foreach ( $users as $user ) {
+                       if ( $useId && $user->getId() ) {
+                               $ids[] = $user->getId();
+                       } else {
+                               $names[] = $user->getName();
+                       }
+                       $actorId = $user->getActorId();
+                       if ( $actorId ) {
+                               $actors[] = $actorId;
+                       }
+               }
+
+               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" ];
+                       }
+               } 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
+                       );
+               }
+
+               return [
+                       'tables' => $tables,
+                       'conds' => $conds ? $db->makeList( array_values( $conds ), IDatabase::LIST_OR ) : '1=0',
+                       'orconds' => $conds,
+                       'joins' => $joins,
+               ];
+       }
+
+}
index bdc6702..e23a8ff 100644 (file)
@@ -206,12 +206,25 @@ class Block {
         * @return array
         */
        public static function selectFields() {
+               global $wgActorTableSchemaMigrationStage;
+
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+                       // 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.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                       );
+               }
+
                wfDeprecated( __METHOD__, '1.31' );
                return [
                        'ipb_id',
                        'ipb_address',
                        'ipb_by',
                        'ipb_by_text',
+                       'ipb_by_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'ipb_by_actor' : null,
                        'ipb_timestamp',
                        'ipb_auto',
                        'ipb_anon_only',
@@ -236,13 +249,12 @@ class Block {
         */
        public static function getQueryInfo() {
                $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
                return [
-                       'tables' => [ 'ipblocks' ] + $commentQuery['tables'],
+                       'tables' => [ 'ipblocks' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        'fields' => [
                                'ipb_id',
                                'ipb_address',
-                               'ipb_by',
-                               'ipb_by_text',
                                'ipb_timestamp',
                                'ipb_auto',
                                'ipb_anon_only',
@@ -253,8 +265,8 @@ class Block {
                                'ipb_block_email',
                                'ipb_allow_usertalk',
                                'ipb_parent_block_id',
-                       ] + $commentQuery['fields'],
-                       'joins' => $commentQuery['joins'],
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
+                       'joins' => $commentQuery['joins'] + $actorQuery['joins'],
                ];
        }
 
@@ -445,11 +457,9 @@ class Block {
         */
        protected function initFromRow( $row ) {
                $this->setTarget( $row->ipb_address );
-               if ( $row->ipb_by ) { // local user
-                       $this->setBlocker( User::newFromId( $row->ipb_by ) );
-               } else { // foreign user
-                       $this->setBlocker( $row->ipb_by_text );
-               }
+               $this->setBlocker( User::newFromAnyId(
+                       $row->ipb_by, $row->ipb_by_text, isset( $row->ipb_by_actor ) ? $row->ipb_by_actor : null
+               ) );
 
                $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp );
                $this->mAuto = $row->ipb_auto;
@@ -519,6 +529,9 @@ class Block {
                if ( $this->getSystemBlockType() !== null ) {
                        throw new MWException( 'Cannot insert a system block into the database' );
                }
+               if ( !$this->getBlocker() || $this->getBlocker()->getName() === '' ) {
+                       throw new MWException( 'Cannot insert a block without a blocker set' );
+               }
 
                wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" );
 
@@ -640,8 +653,6 @@ class Block {
                $a = [
                        'ipb_address'          => (string)$this->target,
                        'ipb_user'             => $uid,
-                       'ipb_by'               => $this->getBy(),
-                       'ipb_by_text'          => $this->getByName(),
                        'ipb_timestamp'        => $dbw->timestamp( $this->mTimestamp ),
                        'ipb_auto'             => $this->mAuto,
                        'ipb_anon_only'        => !$this->isHardblock(),
@@ -654,7 +665,8 @@ class Block {
                        'ipb_block_email'      => $this->prevents( 'sendemail' ),
                        'ipb_allow_usertalk'   => !$this->prevents( 'editownusertalk' ),
                        'ipb_parent_block_id'  => $this->mParentBlockId
-               ] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason );
+               ] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason )
+                       + ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() );
 
                return $a;
        }
@@ -665,12 +677,11 @@ class Block {
         */
        protected function getAutoblockUpdateArray( IDatabase $dbw ) {
                return [
-                       'ipb_by'               => $this->getBy(),
-                       'ipb_by_text'          => $this->getByName(),
                        'ipb_create_account'   => $this->prevents( 'createaccount' ),
                        'ipb_deleted'          => (int)$this->mHideName, // typecast required for SQLite
                        'ipb_allow_usertalk'   => !$this->prevents( 'editownusertalk' ),
-               ] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason );
+               ] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason )
+                       + ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() );
        }
 
        /**
@@ -710,16 +721,27 @@ class Block {
                        return;
                }
 
+               $target = $block->getTarget();
+               if ( is_string( $target ) ) {
+                       $target = User::newFromName( $target, false );
+               }
+
                $dbr = wfGetDB( DB_REPLICA );
+               $rcQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $target, false );
 
                $options = [ 'ORDER BY' => 'rc_timestamp DESC' ];
-               $conds = [ 'rc_user_text' => (string)$block->getTarget() ];
 
                // Just the last IP used.
                $options['LIMIT'] = 1;
 
-               $res = $dbr->select( 'recentchanges', [ 'rc_ip' ], $conds,
-                       __METHOD__, $options );
+               $res = $dbr->select(
+                       [ 'recentchanges' ] + $rcQuery['tables'],
+                       [ 'rc_ip' ],
+                       $rcQuery['conds'],
+                       __METHOD__,
+                       $options,
+                       $rcQuery['joins']
+               );
 
                if ( !$res->numRows() ) {
                        # No results, don't autoblock anything
@@ -1471,7 +1493,7 @@ class Block {
 
        /**
         * Get the user who implemented this block
-        * @return User|string Local User object or string for a foreign user
+        * @return User User object. May name a foreign user.
         */
        public function getBlocker() {
                return $this->blocker;
index 35b821c..2fa0710 100644 (file)
@@ -8830,6 +8830,13 @@ $wgInterwikiPrefixDisplayTypes = [];
  */
 $wgCommentTableSchemaMigrationStage = MIGRATION_OLD;
 
+/**
+ * Actor table schema migration stage.
+ * @since 1.31
+ * @var int One of the MIGRATION_* constants
+ */
+$wgActorTableSchemaMigrationStage = MIGRATION_OLD;
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index 652b355..d6f9fdf 100644 (file)
@@ -3785,30 +3785,30 @@ ERROR;
        protected function getLastDelete() {
                $dbr = wfGetDB( DB_REPLICA );
                $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
                $data = $dbr->selectRow(
-                       [ 'logging', 'user' ] + $commentQuery['tables'],
+                       array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
                        [
                                'log_type',
                                'log_action',
                                'log_timestamp',
-                               'log_user',
                                'log_namespace',
                                'log_title',
                                'log_params',
                                'log_deleted',
                                'user_name'
-                       ] + $commentQuery['fields'], [
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
+                       [
                                'log_namespace' => $this->mTitle->getNamespace(),
                                'log_title' => $this->mTitle->getDBkey(),
                                'log_type' => 'delete',
                                'log_action' => 'delete',
-                               'user_id=log_user'
                        ],
                        __METHOD__,
                        [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
                        [
-                               'user' => [ 'JOIN', 'user_id=log_user' ],
-                       ] + $commentQuery['joins']
+                               'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
+                       ] + $commentQuery['joins'] + $actorQuery['joins']
                );
                // Quick paranoid permission checks...
                if ( is_object( $data ) ) {
index fb446b4..5fc5eb1 100644 (file)
@@ -1752,9 +1752,10 @@ class Linker {
                $dbr = wfGetDB( DB_REPLICA );
 
                // Up to the value of $wgShowRollbackEditCount revisions are counted
+               $revQuery = Revision::getQueryInfo();
                $res = $dbr->select(
-                       'revision',
-                       [ 'rev_user_text', 'rev_deleted' ],
+                       $revQuery['tables'],
+                       [ 'rev_user_text' => $revQuery['fields']['rev_user_text'], 'rev_deleted' ],
                        // $rev->getPage() returns null sometimes
                        [ 'rev_page' => $rev->getTitle()->getArticleID() ],
                        __METHOD__,
@@ -1762,7 +1763,8 @@ class Linker {
                                'USE INDEX' => [ 'revision' => 'page_timestamp' ],
                                'ORDER BY' => 'rev_timestamp DESC',
                                'LIMIT' => $wgShowRollbackEditCount + 1
-                       ]
+                       ],
+                       $revQuery['joins']
                );
 
                $editCount = 0;
index 59f194d..3c8ce65 100644 (file)
@@ -794,6 +794,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'CommentStore' );
        }
 
+       /**
+        * @since 1.31
+        * @return ActorMigration
+        */
+       public function getActorMigration() {
+               return $this->getService( 'ActorMigration' );
+       }
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service getter here, don't forget to add a test
        // case for it in MediaWikiServicesTest::provideGetters() and in
index d9d3149..f8a3fcc 100644 (file)
@@ -29,7 +29,6 @@ use MediaWiki\Storage\RevisionStore;
 use MediaWiki\Storage\RevisionStoreRecord;
 use MediaWiki\Storage\SlotRecord;
 use MediaWiki\Storage\SqlBlobStore;
-use MediaWiki\User\UserIdentityValue;
 use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
@@ -316,7 +315,18 @@ class Revision implements IDBAccessObject {
         * @return array
         */
        public static function userJoinCond() {
+               global $wgActorTableSchemaMigrationStage;
+
                wfDeprecated( __METHOD__, '1.31' );
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+                       // 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.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                       );
+               }
+
                return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
        }
 
@@ -339,7 +349,17 @@ class Revision implements IDBAccessObject {
         * @return array
         */
        public static function selectFields() {
-               global $wgContentHandlerUseDB;
+               global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage;
+
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+                       // 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.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                       );
+               }
 
                wfDeprecated( __METHOD__, '1.31' );
 
@@ -350,6 +370,7 @@ class Revision implements IDBAccessObject {
                        'rev_timestamp',
                        'rev_user_text',
                        'rev_user',
+                       'rev_actor' => 'NULL',
                        'rev_minor_edit',
                        'rev_deleted',
                        'rev_len',
@@ -374,7 +395,17 @@ class Revision implements IDBAccessObject {
         * @return array
         */
        public static function selectArchiveFields() {
-               global $wgContentHandlerUseDB;
+               global $wgContentHandlerUseDB, $wgActorTableSchemaMigrationStage;
+
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+                       // 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.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                       );
+               }
 
                wfDeprecated( __METHOD__, '1.31' );
 
@@ -387,6 +418,7 @@ class Revision implements IDBAccessObject {
                        'ar_timestamp',
                        'ar_user_text',
                        'ar_user',
+                       'ar_actor' => 'NULL',
                        'ar_minor_edit',
                        'ar_deleted',
                        'ar_len',
@@ -623,7 +655,7 @@ class Revision implements IDBAccessObject {
         */
        public function setUserIdAndName( $id, $name ) {
                if ( $this->mRecord instanceof MutableRevisionRecord ) {
-                       $user = new UserIdentityValue( intval( $id ), $name );
+                       $user = User::newFromAnyId( intval( $id ), $name, null );
                        $this->mRecord->setUser( $user );
                } else {
                        throw new MWException( __METHOD__ . ' is not supported on this instance' );
index fa454e0..5243cc6 100644 (file)
@@ -203,6 +203,16 @@ abstract class RevisionItemBase {
                return false;
        }
 
+       /**
+        * Get the DB field name storing actor ids.
+        * Override this function.
+        * @since 1.31
+        * @return bool
+        */
+       public function getAuthorActorField() {
+               return false;
+       }
+
        /**
         * Get the ID, as it would appear in the ids URL parameter
         * @return int
@@ -257,6 +267,16 @@ abstract class RevisionItemBase {
                return strval( $this->row->$field );
        }
 
+       /**
+        * Get the author actor ID
+        * @since 1.31
+        * @return string
+        */
+       public function getAuthorActor() {
+               $field = $this->getAuthorActorField();
+               return strval( $this->row->$field );
+       }
+
        /**
         * Returns true if the current user can view the item
         */
index dab9fb9..3e3c897 100644 (file)
@@ -179,7 +179,8 @@ return [
        'WatchedItemQueryService' => function ( MediaWikiServices $services ) {
                return new WatchedItemQueryService(
                        $services->getDBLoadBalancer(),
-                       $services->getCommentStore()
+                       $services->getCommentStore(),
+                       $services->getActorMigration()
                );
        },
 
@@ -500,7 +501,8 @@ return [
                        $services->getDBLoadBalancer(),
                        $blobStore,
                        $services->getMainWANObjectCache(),
-                       $services->getCommentStore()
+                       $services->getCommentStore(),
+                       $services->getActorMigration()
                );
 
                $store->setLogger( LoggerFactory::getInstance( 'RevisionStore' ) );
@@ -555,7 +557,13 @@ return [
                        $wgContLang,
                        $services->getMainConfig()->get( 'CommentTableSchemaMigrationStage' )
                );
-       }
+       },
+
+       'ActorMigration' => function ( MediaWikiServices $services ) {
+               return new ActorMigration(
+                       $services->getMainConfig()->get( 'ActorTableSchemaMigrationStage' )
+               );
+       },
 
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service here, don't forget to add a getter function
index db06afe..e13fc1f 100644 (file)
@@ -26,6 +26,7 @@
 
 namespace MediaWiki\Storage;
 
+use ActorMigration;
 use CommentStore;
 use CommentStoreComment;
 use Content;
@@ -97,6 +98,11 @@ class RevisionStore
         */
        private $commentStore;
 
+       /**
+        * @var ActorMigration
+        */
+       private $actorMigration;
+
        /**
         * @var LoggerInterface
         */
@@ -109,6 +115,7 @@ class RevisionStore
         * @param SqlBlobStore $blobStore
         * @param WANObjectCache $cache
         * @param CommentStore $commentStore
+        * @param ActorMigration $actorMigration
         * @param bool|string $wikiId
         */
        public function __construct(
@@ -116,6 +123,7 @@ class RevisionStore
                SqlBlobStore $blobStore,
                WANObjectCache $cache,
                CommentStore $commentStore,
+               ActorMigration $actorMigration,
                $wikiId = false
        ) {
                Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
@@ -124,6 +132,7 @@ class RevisionStore
                $this->blobStore = $blobStore;
                $this->cache = $cache;
                $this->commentStore = $commentStore;
+               $this->actorMigration = $actorMigration;
                $this->wikiId = $wikiId;
                $this->logger = new NullLogger();
        }
@@ -388,14 +397,16 @@ class RevisionStore
                $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
                $timestamp = $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
 
+               // Checks.
+               $this->failOnNull( $user->getId(), 'user field' );
+               $this->failOnEmpty( $user->getName(), 'user_text field' );
+
                # Record the edit in revisions
                $row = [
                        'rev_page'       => $pageId,
                        'rev_parent_id'  => $parentId,
                        'rev_text_id'    => $textId,
                        'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
-                       'rev_user'       => $this->failOnNull( $user->getId(), 'user field' ),
-                       'rev_user_text'  => $this->failOnEmpty( $user->getName(), 'user_text field' ),
                        'rev_timestamp'  => $dbw->timestamp( $timestamp ),
                        'rev_deleted'    => $rev->getVisibility(),
                        'rev_len'        => $size,
@@ -411,6 +422,10 @@ class RevisionStore
                        $this->commentStore->insertWithTempTable( $dbw, 'rev_comment', $comment );
                $row += $commentFields;
 
+               list( $actorFields, $actorCallback ) =
+                       $this->actorMigration->getInsertValuesWithTempTable( $dbw, 'rev_user', $user );
+               $row += $actorFields;
+
                if ( $this->contentHandlerUseDB ) {
                        // MCR migration note: rev_content_model and rev_content_format will go away
 
@@ -428,13 +443,14 @@ class RevisionStore
                        $row['rev_id'] = intval( $dbw->insertId() );
                }
                $commentCallback( $row['rev_id'] );
+               $actorCallback( $row['rev_id'], $row );
 
                // Insert IP revision into ip_changes for use when querying for a range.
-               if ( $row['rev_user'] === 0 && IP::isValid( $row['rev_user_text'] ) ) {
+               if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
                        $ipcRow = [
                                'ipc_rev_id'        => $row['rev_id'],
                                'ipc_rev_timestamp' => $row['rev_timestamp'],
-                               'ipc_hex'           => IP::toHex( $row['rev_user_text'] ),
+                               'ipc_hex'           => IP::toHex( $user->getName() ),
                        ];
                        $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
                }
@@ -442,8 +458,6 @@ class RevisionStore
                $newSlot = SlotRecord::newSaved( $row['rev_id'], $blobAddress, $slot );
                $slots = new RevisionSlots( [ 'main' => $newSlot ] );
 
-               $user = new UserIdentityValue( intval( $row['rev_user'] ), $row['rev_user_text'] );
-
                $rev = new RevisionStoreRecord(
                        $title,
                        $user,
@@ -583,6 +597,7 @@ class RevisionStore
                                'page'       => $title->getArticleID(),
                                'user_text'  => $user->getName(),
                                'user'       => $user->getId(),
+                               'actor'      => $user->getActorId(),
                                'comment'    => $comment,
                                'minor_edit' => $minor,
                                'text_id'    => $current->rev_text_id,
@@ -654,9 +669,10 @@ class RevisionStore
                }
 
                // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
+               $actorWhere = $this->actorMigration->getWhere( $dbr, 'rc_user', $rev->getUser(), false );
                $rc = RecentChange::newFromConds(
                        [
-                               'rc_user_text' => $userIdentity->getName(),
+                               $actorWhere['conds'],
                                'rc_timestamp' => $dbr->timestamp( $rev->getTimestamp() ),
                                'rc_this_oldid' => $rev->getId()
                        ],
@@ -691,6 +707,7 @@ class RevisionStore
                        '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',
@@ -741,7 +758,7 @@ class RevisionStore
 
                if ( is_object( $row ) ) {
                        // archive row
-                       if ( !isset( $row->rev_id ) && isset( $row->ar_user ) ) {
+                       if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
                                $row = $this->mapArchiveFields( $row );
                        }
 
@@ -1080,7 +1097,16 @@ class RevisionStore
                        $row->$field = $value;
                }
 
-               $user = $this->getUserIdentityFromRowObject( $row, 'ar_' );
+               try {
+                       $user = User::newFromAnyId(
+                               isset( $row->ar_user ) ? $row->ar_user : null,
+                               isset( $row->ar_user_text ) ? $row->ar_user_text : null,
+                               isset( $row->ar_actor ) ? $row->ar_actor : null
+                       );
+               } catch ( InvalidArgumentException $ex ) {
+                       wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
+                       $user = new UserIdentityValue( 0, '', 0 );
+               }
 
                $comment = $this->commentStore
                        // Legacy because $row may have come from self::selectFields()
@@ -1092,34 +1118,6 @@ class RevisionStore
                return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
        }
 
-       /**
-        * @param object $row
-        * @param string $prefix Field prefix, such as 'rev_' or 'ar_'.
-        *
-        * @return UserIdentityValue
-        */
-       private function getUserIdentityFromRowObject( $row, $prefix = 'rev_' ) {
-               $idField = "{$prefix}user";
-               $nameField = "{$prefix}user_text";
-
-               $userId = intval( $row->$idField );
-
-               if ( isset( $row->user_name ) ) {
-                       $userName = $row->user_name;
-               } elseif ( isset( $row->$nameField ) ) {
-                       $userName = $row->$nameField;
-               } else {
-                       $userName = User::whoIs( $userId );
-               }
-
-               if ( $userName === false ) {
-                       wfWarn( __METHOD__ . ': Cannot determine user name for user ID ' . $userId );
-                       $userName = '';
-               }
-
-               return new UserIdentityValue( $userId, $userName );
-       }
-
        /**
         * @see RevisionFactory::newRevisionFromRow_1_29
         *
@@ -1150,7 +1148,16 @@ class RevisionStore
                        }
                }
 
-               $user = $this->getUserIdentityFromRowObject( $row );
+               try {
+                       $user = User::newFromAnyId(
+                               isset( $row->rev_user ) ? $row->rev_user : null,
+                               isset( $row->rev_user_text ) ? $row->rev_user_text : null,
+                               isset( $row->rev_actor ) ? $row->rev_actor : null
+                       );
+               } catch ( InvalidArgumentException $ex ) {
+                       wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
+                       $user = new UserIdentityValue( 0, '', 0 );
+               }
 
                $comment = $this->commentStore
                        // Legacy because $row may have come from self::selectFields()
@@ -1229,27 +1236,6 @@ class RevisionStore
                        }
                }
 
-               // Replaces old lazy loading logic in Revision::getUserText.
-               if ( !isset( $fields['user_text'] ) && isset( $fields['user'] ) ) {
-                       if ( $fields['user'] instanceof UserIdentity ) {
-                               /** @var User $user */
-                               $user = $fields['user'];
-                               $fields['user_text'] = $user->getName();
-                               $fields['user'] = $user->getId();
-                       } else {
-                               // TODO: wrap this in a callback to make it lazy again.
-                               $name = $fields['user'] === 0 ? false : User::whoIs( $fields['user'] );
-
-                               if ( $name === false ) {
-                                       throw new MWException(
-                                               'user_text not given, and unknown user ID ' . $fields['user']
-                                       );
-                               }
-
-                               $fields['user_text'] = $name;
-                       }
-               }
-
                if (
                        isset( $fields['comment'] )
                        && !( $fields['comment'] instanceof CommentStoreComment )
@@ -1292,16 +1278,15 @@ class RevisionStore
 
                if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
                        $user = $fields['user'];
-               } elseif ( isset( $fields['user'] ) && isset( $fields['user_text'] ) ) {
-                       $user = new UserIdentityValue( intval( $fields['user'] ), $fields['user_text'] );
-               } elseif ( isset( $fields['user'] ) ) {
-                       $user = User::newFromId( intval( $fields['user'] ) );
-               } elseif ( isset( $fields['user_text'] ) ) {
-                       $user = User::newFromName( $fields['user_text'] );
-
-                       // User::newFromName will return false for IP addresses (and invalid names)
-                       if ( $user == false ) {
-                               $user = new UserIdentityValue( 0, $fields['user_text'] );
+               } else {
+                       try {
+                               $user = User::newFromAnyId(
+                                       isset( $fields['user'] ) ? $fields['user'] : null,
+                                       isset( $fields['user_text'] ) ? $fields['user_text'] : null,
+                                       isset( $fields['actor'] ) ? $fields['actor'] : null
+                               );
+                       } catch ( InvalidArgumentException $ex ) {
+                               $user = null;
                        }
                }
 
@@ -1618,8 +1603,6 @@ class RevisionStore
                        'rev_page',
                        'rev_text_id',
                        'rev_timestamp',
-                       'rev_user_text',
-                       'rev_user',
                        'rev_minor_edit',
                        'rev_deleted',
                        'rev_len',
@@ -1632,6 +1615,11 @@ class RevisionStore
                $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->contentHandlerUseDB ) {
                        $ret['fields'][] = 'rev_content_format';
                        $ret['fields'][] = 'rev_content_model';
@@ -1655,7 +1643,8 @@ class RevisionStore
                        $ret['fields'] = array_merge( $ret['fields'], [
                                'user_name',
                        ] );
-                       $ret['joins']['user'] = [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
+                       $u = $actorQuery['fields']['rev_user'];
+                       $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
                }
 
                if ( in_array( 'text', $options, true ) ) {
@@ -1685,8 +1674,9 @@ class RevisionStore
         */
        public function getArchiveQueryInfo() {
                $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
+               $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
                $ret = [
-                       'tables' => [ 'archive' ] + $commentQuery['tables'],
+                       'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        'fields' => [
                                        'ar_id',
                                        'ar_page_id',
@@ -1696,15 +1686,13 @@ class RevisionStore
                                        'ar_text',
                                        'ar_text_id',
                                        'ar_timestamp',
-                                       'ar_user_text',
-                                       'ar_user',
                                        'ar_minor_edit',
                                        'ar_deleted',
                                        'ar_len',
                                        'ar_parent_id',
                                        'ar_sha1',
-                               ] + $commentQuery['fields'],
-                       'joins' => $commentQuery['joins'],
+                               ] + $commentQuery['fields'] + $actorQuery['fields'],
+                       'joins' => $commentQuery['joins'] + $actorQuery['joins'],
                ];
 
                if ( $this->contentHandlerUseDB ) {
@@ -1927,15 +1915,19 @@ class RevisionStore
                        return false;
                }
 
+               $revQuery = self::getQueryInfo();
                $res = $db->select(
-                       'revision',
-                       'rev_user',
+                       $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 ]
+                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
+                       $revQuery['joins']
                );
                foreach ( $res as $row ) {
                        if ( $row->rev_user != $userId ) {
index 82d9fd9..6dc7db5 100644 (file)
@@ -4468,17 +4468,18 @@ class Title implements LinkTarget {
                        return $authors;
                }
                $dbr = wfGetDB( DB_REPLICA );
-               $res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
+               $revQuery = Revision::getQueryInfo();
+               $authors = $dbr->selectFieldValues(
+                       $revQuery['tables'],
+                       $revQuery['fields']['rev_user_text'],
                        [
                                'rev_page' => $this->getArticleID(),
                                "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
                                "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
                        ], __METHOD__,
-                       [ 'LIMIT' => $limit + 1 ] // add one so caller knows it was truncated
+                       [ 'DISTINCT', 'LIMIT' => $limit + 1 ], // add one so caller knows it was truncated
+                       $revQuery['joins']
                );
-               foreach ( $res as $row ) {
-                       $authors[] = $row->rev_user_text;
-               }
                return $authors;
        }
 
index 1165a26..0988f73 100644 (file)
@@ -718,6 +718,8 @@ class InfoAction extends FormlessAction {
                        self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ),
                        WANObjectCache::TTL_WEEK,
                        function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
+                               global $wgActorTableSchemaMigrationStage;
+
                                $title = $page->getTitle();
                                $id = $title->getArticleID();
 
@@ -725,6 +727,29 @@ class InfoAction extends FormlessAction {
                                $dbrWatchlist = wfGetDB( DB_REPLICA, 'watchlist' );
                                $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
 
+                               if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                                       $tables = [ 'revision_actor_temp' ];
+                                       $field = 'revactor_actor';
+                                       $pageField = 'revactor_page';
+                                       $tsField = 'revactor_timestamp';
+                                       $joins = [];
+                               } elseif ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+                                       $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();
 
                                $result = [];
@@ -752,10 +777,12 @@ class InfoAction extends FormlessAction {
                                        $result['authors'] = 0;
                                } else {
                                        $result['authors'] = (int)$dbr->selectField(
-                                               'revision',
-                                               'COUNT(DISTINCT rev_user_text)',
-                                               [ 'rev_page' => $id ],
-                                               $fname
+                                               $tables,
+                                               "COUNT(DISTINCT $field)",
+                                               [ $pageField => $id ],
+                                               $fname,
+                                               [],
+                                               $joins
                                        );
                                }
 
@@ -776,13 +803,15 @@ class InfoAction extends FormlessAction {
 
                                // Recent number of distinct authors
                                $result['recent_authors'] = (int)$dbr->selectField(
-                                       'revision',
-                                       'COUNT(DISTINCT rev_user_text)',
+                                       $tables,
+                                       "COUNT(DISTINCT $field)",
                                        [
-                                               'rev_page' => $id,
-                                               "rev_timestamp >= " . $dbr->addQuotes( $threshold )
+                                               $pageField => $id,
+                                               "$tsField >= " . $dbr->addQuotes( $threshold )
                                        ],
-                                       $fname
+                                       $fname,
+                                       [],
+                                       $joins
                                );
 
                                // Subpages (if enabled)
index 32d081e..f885b72 100644 (file)
@@ -224,10 +224,19 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                }
 
                if ( !is_null( $params['user'] ) ) {
-                       $this->addWhereFld( 'ar_user_text', $params['user'] );
+                       // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( $actorQuery['conds'] );
                } elseif ( !is_null( $params['excludeuser'] ) ) {
-                       $this->addWhere( 'ar_user_text != ' .
-                               $db->addQuotes( $params['excludeuser'] ) );
+                       // Here there's no chance of using ar_usertext_timestamp.
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
                }
 
                if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
index dde22d8..14f1cc4 100644 (file)
@@ -85,7 +85,6 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
                $db = $this->getDB();
 
                $params = $this->extractRequestParams();
-               $userId = !is_null( $params['user'] ) ? User::idFromName( $params['user'] ) : null;
 
                // Table and return fields
                $prop = array_flip( $params['prop'] );
@@ -192,19 +191,22 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
 
                        // Image filters
                        if ( !is_null( $params['user'] ) ) {
-                               if ( $userId ) {
-                                       $this->addWhereFld( 'img_user', $userId );
-                               } else {
-                                       $this->addWhereFld( 'img_user_text', $params['user'] );
-                               }
+                               $actorQuery = ActorMigration::newMigration()
+                                       ->getWhere( $db, 'img_user', User::newFromName( $params['user'], false ) );
+                               $this->addTables( $actorQuery['tables'] );
+                               $this->addJoinConds( $actorQuery['joins'] );
+                               $this->addWhere( $actorQuery['conds'] );
                        }
                        if ( $params['filterbots'] != 'all' ) {
+                               $actorQuery = ActorMigration::newMigration()->getJoin( 'img_user' );
+                               $this->addTables( $actorQuery['tables'] );
                                $this->addTables( 'user_groups' );
+                               $this->addJoinConds( $actorQuery['joins'] );
                                $this->addJoinConds( [ 'user_groups' => [
                                        'LEFT JOIN',
                                        [
                                                'ug_group' => User::getGroupsWithPermission( 'bot' ),
-                                               'ug_user = img_user',
+                                               'ug_user = ' . $actorQuery['fields']['img_user'],
                                                'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() )
                                        ]
                                ] ] );
@@ -273,15 +275,6 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
                }
                if ( $params['sort'] == 'timestamp' ) {
                        $this->addOption( 'ORDER BY', 'img_timestamp' . $sortFlag );
-                       if ( !is_null( $params['user'] ) ) {
-                               if ( $userId ) {
-                                       $this->addOption( 'USE INDEX', [ 'image' => 'img_user_timestamp' ] );
-                               } else {
-                                       $this->addOption( 'USE INDEX', [ 'image' => 'img_usertext_timestamp' ] );
-                               }
-                       } else {
-                               $this->addOption( 'USE INDEX', [ 'image' => 'img_timestamp' ] );
-                       }
                } else {
                        $this->addOption( 'ORDER BY', 'img_name' . $sortFlag );
                }
index 6823646..3af2459 100644 (file)
@@ -104,19 +104,17 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
                }
 
                if ( $params['user'] !== null ) {
-                       $id = User::idFromName( $params['user'] );
-                       if ( $id ) {
-                               $this->addWhereFld( 'rev_user', $id );
-                       } else {
-                               $this->addWhereFld( 'rev_user_text', $params['user'] );
-                       }
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $db, 'rev_user', User::newFromName( $params['user'], false ) );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( $actorQuery['conds'] );
                } elseif ( $params['excludeuser'] !== null ) {
-                       $id = User::idFromName( $params['excludeuser'] );
-                       if ( $id ) {
-                               $this->addWhere( 'rev_user != ' . $id );
-                       } else {
-                               $this->addWhere( 'rev_user_text != ' . $db->addQuotes( $params['excludeuser'] ) );
-                       }
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $db, 'rev_user', User::newFromName( $params['excludeuser'], false ) );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
                }
 
                if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
index 26844f3..9652f81 100644 (file)
@@ -41,6 +41,8 @@ class ApiQueryAllUsers extends ApiQueryBase {
        }
 
        public function execute() {
+               global $wgActorTableSchemaMigrationStage;
+
                $params = $this->extractRequestParams();
                $activeUserDays = $this->getConfig()->get( 'ActiveUserDays' );
 
@@ -178,17 +180,36 @@ class ApiQueryAllUsers extends ApiQueryBase {
                        ] ] );
 
                        // Actually count the actions using a subquery (T66505 and T66507)
+                       $tables = [ 'recentchanges' ];
+                       $joins = [];
+                       if ( $wgActorTableSchemaMigrationStage === MIGRATION_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)';
+                               }
+                       }
                        $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds );
                        $this->addFields( [
                                'recentactions' => '(' . $db->selectSQLText(
-                                       'recentchanges',
+                                       $tables,
                                        'COUNT(*)',
                                        [
-                                               'rc_user_text = user_name',
+                                               $userCond,
                                                'rc_type != ' . $db->addQuotes( RC_EXTERNAL ), // no wikidata
                                                'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ),
                                                'rc_timestamp >= ' . $db->addQuotes( $timestamp ),
-                                       ]
+                                       ],
+                                       __METHOD__,
+                                       [],
+                                       $joins
                                ) . ')'
                        ] );
                }
index 84169cb..3ad45bb 100644 (file)
@@ -446,11 +446,13 @@ abstract class ApiQueryBase extends ApiBase {
                if ( $showBlockInfo ) {
                        $this->addFields( [
                                'ipb_id',
-                               'ipb_by',
-                               'ipb_by_text',
                                'ipb_expiry',
                                'ipb_timestamp'
                        ] );
+                       $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addFields( $actorQuery['fields'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
                        $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' );
                        $this->addTables( $commentQuery['tables'] );
                        $this->addFields( $commentQuery['fields'] );
index 10695b3..08c13e7 100644 (file)
@@ -55,8 +55,12 @@ class ApiQueryBlocks extends ApiQueryBase {
                $this->addFields( [ 'ipb_auto', 'ipb_id', 'ipb_timestamp' ] );
 
                $this->addFieldsIf( [ 'ipb_address', 'ipb_user' ], $fld_user || $fld_userid );
-               $this->addFieldsIf( 'ipb_by_text', $fld_by );
-               $this->addFieldsIf( 'ipb_by', $fld_byid );
+               if ( $fld_by || $fld_byid ) {
+                       $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addFields( $actorQuery['fields'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+               }
                $this->addFieldsIf( 'ipb_expiry', $fld_expiry );
                $this->addFieldsIf( [ 'ipb_range_start', 'ipb_range_end' ], $fld_range );
                $this->addFieldsIf( [ 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock',
index 25b7c84..48516a7 100644 (file)
@@ -43,6 +43,8 @@ class ApiQueryContributors extends ApiQueryBase {
        }
 
        public function execute() {
+               global $wgActorTableSchemaMigrationStage;
+
                $db = $this->getDB();
                $params = $this->extractRequestParams();
                $this->requireMaxOneParameter( $params, 'group', 'excludegroup', 'rights', 'excluderights' );
@@ -73,17 +75,27 @@ class ApiQueryContributors extends ApiQueryBase {
                }
 
                $result = $this->getResult();
+               $revQuery = Revision::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
+                       ? 'revactor_actor' : $revQuery['fields']['rev_user'];
+               $countField = $wgActorTableSchemaMigrationStage === MIGRATION_NEW
+                       ? 'revactor_actor' : $revQuery['fields']['rev_user_text'];
 
                // First, count anons
-               $this->addTables( 'revision' );
+               $this->addTables( $revQuery['tables'] );
+               $this->addJoinConds( $revQuery['joins'] );
                $this->addFields( [
-                       'page' => 'rev_page',
-                       'anons' => 'COUNT(DISTINCT rev_user_text)',
+                       'page' => $pageField,
+                       'anons' => "COUNT(DISTINCT $countField)",
                ] );
-               $this->addWhereFld( 'rev_page', $pages );
-               $this->addWhere( 'rev_user = 0' );
+               $this->addWhereFld( $pageField, $pages );
+               $this->addWhere( ActorMigration::newMigration()->isAnon( $revQuery['fields']['rev_user'] ) );
                $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' );
-               $this->addOption( 'GROUP BY', 'rev_page' );
+               $this->addOption( 'GROUP BY', $pageField );
                $res = $this->select( __METHOD__ );
                foreach ( $res as $row ) {
                        $fit = $result->addValue( [ 'query', 'pages', $row->page ],
@@ -103,24 +115,27 @@ class ApiQueryContributors extends ApiQueryBase {
 
                // Next, add logged-in users
                $this->resetQueryParams();
-               $this->addTables( 'revision' );
+               $this->addTables( $revQuery['tables'] );
+               $this->addJoinConds( $revQuery['joins'] );
                $this->addFields( [
-                       'page' => 'rev_page',
-                       'user' => 'rev_user',
-                       'username' => 'MAX(rev_user_text)', // Non-MySQL databases don't like partial group-by
+                       'page' => $pageField,
+                       'id' => $idField,
+                       // Non-MySQL databases don't like partial group-by
+                       'userid' => 'MAX(' . $revQuery['fields']['rev_user'] . ')',
+                       'username' => 'MAX(' . $revQuery['fields']['rev_user_text'] . ')',
                ] );
-               $this->addWhereFld( 'rev_page', $pages );
-               $this->addWhere( 'rev_user != 0' );
+               $this->addWhereFld( $pageField, $pages );
+               $this->addWhere( ActorMigration::newMigration()->isNotAnon( $revQuery['fields']['rev_user'] ) );
                $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' );
-               $this->addOption( 'GROUP BY', 'rev_page, rev_user' );
+               $this->addOption( 'GROUP BY', [ $pageField, $idField ] );
                $this->addOption( 'LIMIT', $params['limit'] + 1 );
 
                // Force a sort order to ensure that properties are grouped by page
-               // But only if pp_page is not constant in the WHERE clause.
+               // But only if rev_page is not constant in the WHERE clause.
                if ( count( $pages ) > 1 ) {
-                       $this->addOption( 'ORDER BY', 'rev_page, rev_user' );
+                       $this->addOption( 'ORDER BY', [ 'page', 'id' ] );
                } else {
-                       $this->addOption( 'ORDER BY', 'rev_user' );
+                       $this->addOption( 'ORDER BY', 'id' );
                }
 
                $limitGroups = [];
@@ -159,7 +174,7 @@ class ApiQueryContributors extends ApiQueryBase {
                        $this->addJoinConds( [ 'user_groups' => [
                                $excludeGroups ? 'LEFT OUTER JOIN' : 'INNER JOIN',
                                [
-                                       'ug_user=rev_user',
+                                       'ug_user=' . $actorQuery['fields']['rev_user'],
                                        'ug_group' => $limitGroups,
                                        'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() )
                                ]
@@ -171,11 +186,11 @@ class ApiQueryContributors extends ApiQueryBase {
                        $cont = explode( '|', $params['continue'] );
                        $this->dieContinueUsageIf( count( $cont ) != 2 );
                        $cont_page = (int)$cont[0];
-                       $cont_user = (int)$cont[1];
+                       $cont_id = (int)$cont[1];
                        $this->addWhere(
-                               "rev_page > $cont_page OR " .
-                               "(rev_page = $cont_page AND " .
-                               "rev_user >= $cont_user)"
+                               "$pageField > $cont_page OR " .
+                               "($pageField = $cont_page AND " .
+                               "$idField >= $cont_id)"
                        );
                }
 
@@ -185,18 +200,16 @@ class ApiQueryContributors extends ApiQueryBase {
                        if ( ++$count > $params['limit'] ) {
                                // We've reached the one extra which shows that
                                // there are additional pages to be had. Stop here...
-                               $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->user );
-
+                               $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->id );
                                return;
                        }
 
                        $fit = $this->addPageSubItem( $row->page,
-                               [ 'userid' => (int)$row->user, 'name' => $row->username ],
+                               [ 'userid' => (int)$row->userid, 'name' => $row->username ],
                                'user'
                        );
                        if ( !$fit ) {
-                               $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->user );
-
+                               $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->id );
                                return;
                        }
                }
index f579065..b7fd8d4 100644 (file)
@@ -117,10 +117,19 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
                }
 
                if ( !is_null( $params['user'] ) ) {
-                       $this->addWhereFld( 'ar_user_text', $params['user'] );
+                       // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( $actorQuery['conds'] );
                } elseif ( !is_null( $params['excludeuser'] ) ) {
-                       $this->addWhere( 'ar_user_text != ' .
-                               $db->addQuotes( $params['excludeuser'] ) );
+                       // Here there's no chance of using ar_usertext_timestamp.
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
                }
 
                if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
index 6e6757e..2d50741 100644 (file)
@@ -110,8 +110,12 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
 
                $this->addFieldsIf( 'ar_parent_id', $fld_parentid );
                $this->addFieldsIf( 'ar_rev_id', $fld_revid );
-               $this->addFieldsIf( 'ar_user_text', $fld_user );
-               $this->addFieldsIf( 'ar_user', $fld_userid );
+               if ( $fld_user || $fld_userid ) {
+                       $actorQuery = ActorMigration::newMigration()->getJoin( 'ar_user' );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addFields( $actorQuery['fields'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+               }
                $this->addFieldsIf( 'ar_minor_edit', $fld_minor );
                $this->addFieldsIf( 'ar_len', $fld_len );
                $this->addFieldsIf( 'ar_sha1', $fld_sha1 );
@@ -199,10 +203,19 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                }
 
                if ( !is_null( $params['user'] ) ) {
-                       $this->addWhereFld( 'ar_user_text', $params['user'] );
+                       // Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( $actorQuery['conds'] );
                } elseif ( !is_null( $params['excludeuser'] ) ) {
-                       $this->addWhere( 'ar_user_text != ' .
-                               $db->addQuotes( $params['excludeuser'] ) );
+                       // Here there's no chance of using ar_usertext_timestamp.
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
                }
 
                if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
@@ -251,10 +264,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                }
 
                $this->addOption( 'LIMIT', $limit + 1 );
-               $this->addOption(
-                       'USE INDEX',
-                       [ 'archive' => ( $mode == 'user' ? 'ar_usertext_timestamp' : 'name_title_timestamp' ) ]
-               );
                if ( $mode == 'all' ) {
                        if ( $params['unique'] ) {
                                // @todo Does this work on non-MySQL?
index f345300..68902a3 100644 (file)
@@ -62,11 +62,15 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        $this->addWhere( $hideLogs );
                }
 
-               // Order is significant here
-               $this->addTables( [ 'logging', 'user', 'page' ] );
+               $actorMigration = ActorMigration::newMigration();
+               $actorQuery = $actorMigration->getJoin( 'log_user' );
+               $this->addTables( 'logging' );
+               $this->addTables( $actorQuery['tables'] );
+               $this->addTables( [ 'user', 'page' ] );
+               $this->addJoinConds( $actorQuery['joins'] );
                $this->addJoinConds( [
                        'user' => [ 'LEFT JOIN',
-                               'user_id=log_user' ],
+                               'user_id=' . $actorQuery['fields']['log_user'] ],
                        'page' => [ 'LEFT JOIN',
                                [ 'log_namespace=page_namespace',
                                        'log_title=page_title' ] ] ] );
@@ -84,8 +88,8 @@ class ApiQueryLogEvents extends ApiQueryBase {
                // join at query time.  This leads to different results in various
                // scenarios, e.g. deletion, recreation.
                $this->addFieldsIf( 'log_page', $this->fld_ids );
-               $this->addFieldsIf( [ 'log_user', 'log_user_text', 'user_name' ], $this->fld_user );
-               $this->addFieldsIf( 'log_user', $this->fld_userid );
+               $this->addFieldsIf( $actorQuery['fields'] + [ 'user_name' ], $this->fld_user );
+               $this->addFieldsIf( $actorQuery['fields'], $this->fld_userid );
                $this->addFieldsIf(
                        [ 'log_namespace', 'log_title' ],
                        $this->fld_title || $this->fld_parsedcomment
@@ -166,12 +170,14 @@ class ApiQueryLogEvents extends ApiQueryBase {
 
                $user = $params['user'];
                if ( !is_null( $user ) ) {
-                       $userid = User::idFromName( $user );
-                       if ( $userid ) {
-                               $this->addWhereFld( 'log_user', $userid );
-                       } else {
-                               $this->addWhereFld( 'log_user_text', $user );
-                       }
+                       // Note the joins in $q are the same as those from ->getJoin() above
+                       // so we only need to add 'conds' here.
+                       // Don't query by user ID here, it might be able to use the
+                       // log_user_text_time or log_user_text_type_time index.
+                       $q = $actorMigration->getWhere(
+                               $db, 'log_user', User::newFromName( $params['user'], false ), false
+                       );
+                       $this->addWhere( $q['conds'] );
                }
 
                $title = $params['title'];
index e289e42..e431202 100644 (file)
@@ -211,8 +211,18 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                        $this->addWhereIf( 'rc_minor != 0', isset( $show['minor'] ) );
                        $this->addWhereIf( 'rc_bot = 0', isset( $show['!bot'] ) );
                        $this->addWhereIf( 'rc_bot != 0', isset( $show['bot'] ) );
-                       $this->addWhereIf( 'rc_user = 0', isset( $show['anon'] ) );
-                       $this->addWhereIf( 'rc_user != 0', isset( $show['!anon'] ) );
+                       if ( isset( $show['anon'] ) || isset( $show['!anon'] ) ) {
+                               $actorMigration = ActorMigration::newMigration();
+                               $actorQuery = $actorMigration->getJoin( 'rc_user' );
+                               $this->addTables( $actorQuery['tables'] );
+                               $this->addJoinConds( $actorQuery['joins'] );
+                               $this->addWhereIf(
+                                       $actorMigration->isAnon( $actorQuery['fields']['rc_user'] ), isset( $show['anon'] )
+                               );
+                               $this->addWhereIf(
+                                       $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] ), isset( $show['!anon'] )
+                               );
+                       }
                        $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
                        $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
                        $this->addWhereIf( 'page_is_redirect = 1', isset( $show['redirect'] ) );
@@ -237,14 +247,21 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
 
                if ( !is_null( $params['user'] ) ) {
-                       $this->addWhereFld( 'rc_user_text', $params['user'] );
+                       // Don't query by user ID here, it might be able to use the rc_user_text index.
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['user'], false ), false );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( $actorQuery['conds'] );
                }
 
                if ( !is_null( $params['excludeuser'] ) ) {
-                       // We don't use the rc_user_text index here because
-                       // * it would require us to sort by rc_user_text before rc_timestamp
-                       // * the != condition doesn't throw out too many rows anyway
-                       $this->addWhere( 'rc_user_text != ' . $this->getDB()->addQuotes( $params['excludeuser'] ) );
+                       // Here there's no chance to use the rc_user_text index, so allow ID to be used.
+                       $actorQuery = ActorMigration::newMigration()
+                               ->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['excludeuser'], false ) );
+                       $this->addTables( $actorQuery['tables'] );
+                       $this->addJoinConds( $actorQuery['joins'] );
+                       $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
                }
 
                /* Add the fields we're concerned with to our query. */
@@ -272,8 +289,12 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
 
                        /* Add fields to our query if they are specified as a needed parameter. */
                        $this->addFieldsIf( [ 'rc_this_oldid', 'rc_last_oldid' ], $this->fld_ids );
-                       $this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid );
-                       $this->addFieldsIf( 'rc_user_text', $this->fld_user );
+                       if ( $this->fld_user || $this->fld_userid ) {
+                               $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+                               $this->addTables( $actorQuery['tables'] );
+                               $this->addFields( $actorQuery['fields'] );
+                               $this->addJoinConds( $actorQuery['joins'] );
+                       }
                        $this->addFieldsIf( [ 'rc_minor', 'rc_type', 'rc_bot' ], $this->fld_flags );
                        $this->addFieldsIf( [ 'rc_old_len', 'rc_new_len' ], $this->fld_sizes );
                        $this->addFieldsIf( [ 'rc_patrolled', 'rc_log_type' ], $this->fld_patrolled );
index ef0223a..5858bc7 100644 (file)
@@ -286,20 +286,17 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
                        $this->addWhereFld( 'rev_page', reset( $ids ) );
 
                        if ( $params['user'] !== null ) {
-                               $user = User::newFromName( $params['user'] );
-                               if ( $user && $user->getId() > 0 ) {
-                                       $this->addWhereFld( 'rev_user', $user->getId() );
-                               } else {
-                                       $this->addWhereFld( 'rev_user_text', $params['user'] );
-                               }
+                               $actorQuery = ActorMigration::newMigration()
+                                       ->getWhere( $db, 'rev_user', User::newFromName( $params['user'], false ) );
+                               $this->addTables( $actorQuery['tables'] );
+                               $this->addJoinConds( $actorQuery['joins'] );
+                               $this->addWhere( $actorQuery['conds'] );
                        } elseif ( $params['excludeuser'] !== null ) {
-                               $user = User::newFromName( $params['excludeuser'] );
-                               if ( $user && $user->getId() > 0 ) {
-                                       $this->addWhere( 'rev_user != ' . $user->getId() );
-                               } else {
-                                       $this->addWhere( 'rev_user_text != ' .
-                                               $db->addQuotes( $params['excludeuser'] ) );
-                               }
+                               $actorQuery = ActorMigration::newMigration()
+                                       ->getWhere( $db, 'rev_user', User::newFromName( $params['excludeuser'], false ) );
+                               $this->addTables( $actorQuery['tables'] );
+                               $this->addJoinConds( $actorQuery['joins'] );
+                               $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
                        }
                        if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
                                // Paranoia: avoid brute force searches (T19342)
index 1705c57..e587ef4 100644 (file)
@@ -31,13 +31,14 @@ class ApiQueryContributions extends ApiQueryBase {
                parent::__construct( $query, $moduleName, 'uc' );
        }
 
-       private $params, $prefixMode, $userprefix, $multiUserMode, $idMode, $usernames, $userids,
-               $parentLens, $commentStore;
+       private $params, $multiUserMode, $orderBy, $parentLens;
        private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
                $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false,
                $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false;
 
        public function execute() {
+               global $wgActorTableSchemaMigrationStage;
+
                // Parse some parameters
                $this->params = $this->extractRequestParams();
 
@@ -63,36 +64,173 @@ class ApiQueryContributions extends ApiQueryBase {
                // TODO: if the query is going only against the revision table, should this be done?
                $this->selectNamedDB( 'contributions', DB_REPLICA, 'contributions' );
 
-               $this->requireOnlyOneParameter( $this->params, 'userprefix', 'userids', 'user' );
+               $sort = ( $this->params['dir'] == 'newer' ? '' : ' DESC' );
+               $op = ( $this->params['dir'] == 'older' ? '<' : '>' );
 
-               $this->idMode = false;
+               // Create an Iterator that produces the UserIdentity objects we need, depending
+               // on which of the 'userprefix', 'userids', or 'user' params was
+               // specified.
+               $this->requireOnlyOneParameter( $this->params, 'userprefix', 'userids', 'user' );
                if ( isset( $this->params['userprefix'] ) ) {
-                       $this->prefixMode = true;
                        $this->multiUserMode = true;
-                       $this->userprefix = $this->params['userprefix'];
-               } elseif ( isset( $this->params['userids'] ) ) {
-                       $this->userids = [];
+                       $this->orderBy = 'name';
+                       $fname = __METHOD__;
+
+                       // Because 'userprefix' might produce a huge number of users (e.g.
+                       // a wiki with users "Test00000001" to "Test99999999"), use a
+                       // generator with batched lookup and continuation.
+                       $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname ) {
+                               global $wgActorTableSchemaMigrationStage;
+
+                               $from = $fromName = false;
+                               if ( !is_null( $this->params['continue'] ) ) {
+                                       $continue = explode( '|', $this->params['continue'] );
+                                       $this->dieContinueUsageIf( count( $continue ) != 4 );
+                                       $this->dieContinueUsageIf( $continue[0] !== 'name' );
+                                       $fromName = $continue[1];
+                                       $from = "$op= " . $dbSecondary->addQuotes( $fromName );
+                               }
+                               $like = $dbSecondary->buildLike( $this->params['userprefix'], $dbSecondary->anyString() );
+
+                               $limit = 501;
+
+                               do {
+                                       // 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 ) {
+                                               $res = $dbSecondary->select(
+                                                       'actor',
+                                                       [ 'actor_id', 'user_id' => 'COALESCE(actor_user,0)', 'user_name' => 'actor_name' ],
+                                                       array_merge( [ "actor_name$like" ], $from ? [ "actor_name $from" ] : [] ),
+                                                       $fname,
+                                                       [ 'ORDER BY' => [ "user_name $sort" ], 'LIMIT' => $limit ]
+                                               );
+                                       } elseif ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+                                               $res = $dbSecondary->select(
+                                                       'revision',
+                                                       [ 'actor_id' => 'NULL', 'user_id' => 'rev_user', 'user_name' => 'rev_user_text' ],
+                                                       array_merge( [ "rev_user_text$like" ], $from ? [ "rev_user_text $from" ] : [] ),
+                                                       $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;
+                                       $from = null;
+                                       foreach ( $res as $row ) {
+                                               if ( ++$count >= $limit ) {
+                                                       $from = $row->user_name;
+                                                       break;
+                                               }
+                                               yield User::newFromRow( $row );
+                                       }
+                               } while ( $from !== null );
+                       } );
+                       // Do the actual sorting client-side, because otherwise
+                       // prepareQuery might try to sort by actor and confuse everything.
+                       $batchSize = 1;
+               } elseif ( isset( $this->params['userids'] ) ) {
                        if ( !count( $this->params['userids'] ) ) {
                                $encParamName = $this->encodeParamName( 'userids' );
                                $this->dieWithError( [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName" );
                        }
 
+                       $ids = [];
                        foreach ( $this->params['userids'] as $uid ) {
                                if ( $uid <= 0 ) {
                                        $this->dieWithError( [ 'apierror-invaliduserid', $uid ], 'invaliduserid' );
                                }
+                               $ids[] = $uid;
+                       }
+
+                       $this->orderBy = 'id';
+                       $this->multiUserMode = count( $ids ) > 1;
 
-                               $this->userids[] = $uid;
+                       $from = $fromId = false;
+                       if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) {
+                               $continue = explode( '|', $this->params['continue'] );
+                               $this->dieContinueUsageIf( count( $continue ) != 4 );
+                               $this->dieContinueUsageIf( $continue[0] !== 'id' && $continue[0] !== 'actor' );
+                               $fromId = (int)$continue[1];
+                               $this->dieContinueUsageIf( $continue[1] !== (string)$fromId );
+                               $from = "$op= $fromId";
                        }
 
-                       $this->prefixMode = false;
-                       $this->multiUserMode = ( count( $this->params['userids'] ) > 1 );
-                       $this->idMode = true;
+                       // 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 ) {
+                               $res = $dbSecondary->select(
+                                       'actor',
+                                       [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ],
+                                       array_merge( [ 'actor_user' => $ids ], $from ? [ "actor_id $from" ] : [] ),
+                                       __METHOD__,
+                                       [ 'ORDER BY' => "user_id $sort" ]
+                               );
+                       } elseif ( $wgActorTableSchemaMigrationStage === MIGRATION_OLD ) {
+                               $res = $dbSecondary->select(
+                                       'user',
+                                       [ 'actor_id' => 'NULL', 'user_id' => 'user_id', 'user_name' => 'user_name' ],
+                                       array_merge( [ 'user_id' => $ids ], $from ? [ "user_id $from" ] : [] ),
+                                       __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 );
                } else {
-                       $anyIPs = false;
-                       $this->userids = [];
-                       $this->usernames = [];
+                       $names = [];
                        if ( !count( $this->params['user'] ) ) {
                                $encParamName = $this->encodeParamName( 'user' );
                                $this->dieWithError(
@@ -108,8 +246,7 @@ class ApiQueryContributions extends ApiQueryBase {
                                }
 
                                if ( User::isIP( $u ) ) {
-                                       $anyIPs = true;
-                                       $this->usernames[] = $u;
+                                       $names[$u] = null;
                                } else {
                                        $name = User::getCanonicalName( $u, 'valid' );
                                        if ( $name === false ) {
@@ -118,94 +255,218 @@ class ApiQueryContributions extends ApiQueryBase {
                                                        [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $u ) ], "baduser_$encParamName"
                                                );
                                        }
-                                       $this->usernames[] = $name;
+                                       $names[$name] = null;
                                }
                        }
-                       $this->prefixMode = false;
-                       $this->multiUserMode = ( count( $this->params['user'] ) > 1 );
 
-                       if ( !$anyIPs ) {
-                               $dbr = $this->getDB();
-                               $res = $dbr->select( 'user', 'user_id', [ 'user_name' => $this->usernames ], __METHOD__ );
+                       $this->orderBy = 'name';
+                       $this->multiUserMode = count( $names ) > 1;
+
+                       $from = $fromName = false;
+                       if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) {
+                               $continue = explode( '|', $this->params['continue'] );
+                               $this->dieContinueUsageIf( count( $continue ) != 4 );
+                               $this->dieContinueUsageIf( $continue[0] !== 'name' && $continue[0] !== 'actor' );
+                               $fromName = $continue[1];
+                               $from = "$op= " . $dbSecondary->addQuotes( $fromName );
+                       }
+
+                       // 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 ) {
+                               $res = $dbSecondary->select(
+                                       'actor',
+                                       [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ],
+                                       array_merge( [ 'actor_name' => array_keys( $names ) ], $from ? [ "actor_id $from" ] : [] ),
+                                       __METHOD__,
+                                       [ 'ORDER BY' => "actor_name $sort" ]
+                               );
+                               $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' ] ]
+                                       );
+                               }
                                foreach ( $res as $row ) {
-                                       $this->userids[] = $row->user_id;
+                                       $names[$row->user_name] = $row;
                                }
-                               $this->idMode = count( $this->userids ) === count( $this->usernames );
+                               call_user_func_array(
+                                       $this->params['dir'] == 'newer' ? 'ksort' : 'krsort', [ &$names, SORT_STRING ]
+                               );
+                               $neg = $op === '>' ? -1 : 1;
+                               $userIter = call_user_func( function () use ( $names, $fromName, $neg ) {
+                                       foreach ( $names as $name => $row ) {
+                                               if ( $fromName === false || $neg * strcmp( $name, $fromName ) <= 0 ) {
+                                                       $user = $row ? User::newFromRow( $row ) : User::newFromName( $name, false );
+                                                       yield $user;
+                                               }
+                                       }
+                               } );
                        }
+                       $batchSize = count( $names );
                }
 
-               $this->prepareQuery();
-
-               $hookData = [];
-               // Do the actual query.
-               $res = $this->select( __METHOD__, [], $hookData );
+               // 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;
+               }
 
-               if ( $this->fld_sizediff ) {
-                       $revIds = [];
-                       foreach ( $res as $row ) {
-                               if ( $row->rev_parent_id ) {
-                                       $revIds[] = $row->rev_parent_id;
-                               }
-                       }
-                       $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds );
-                       $res->rewind(); // reset
+               // With the new schema, the DB query will order by actor so update $this->orderBy to match.
+               if ( $batchSize > 1 && $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                       $this->orderBy = 'actor';
                }
 
-               // Initialise some variables
                $count = 0;
                $limit = $this->params['limit'];
+               $userIter->rewind();
+               while ( $userIter->valid() ) {
+                       $users = [];
+                       while ( count( $users ) < $batchSize && $userIter->valid() ) {
+                               $users[] = $userIter->current();
+                               $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;
+                               }
 
-               // Fetch each row
-               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...
-                               $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
-                               break;
+                               return $neg * $ret;
+                       } );
+                       $merged = array_slice( $merged, 0, $limit - $count + 1 );
+                       // (end "Ugh")
+
+                       if ( $this->fld_sizediff ) {
+                               $revIds = [];
+                               foreach ( $merged as $data ) {
+                                       if ( $data[0]->rev_parent_id ) {
+                                               $revIds[] = $data[0]->rev_parent_id;
+                                       }
+                               }
+                               $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds );
                        }
 
-                       $vals = $this->extractRowInfo( $row );
-                       $fit = $this->processRow( $row, $vals, $hookData ) &&
-                               $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
-                       if ( !$fit ) {
-                               $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
-                               break;
+                       foreach ( $merged as $data ) {
+                               $row = $data[0];
+                               $hookData = &$data[1];
+                               if ( ++$count > $limit ) {
+                                       // We've reached the one extra which shows that there are
+                                       // additional pages to be had. Stop here...
+                                       $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
+                                       break 2;
+                               }
+
+                               $vals = $this->extractRowInfo( $row );
+                               $fit = $this->processRow( $row, $vals, $hookData ) &&
+                                       $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
+                               if ( !$fit ) {
+                                       $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
+                                       break 2;
+                               }
                        }
                }
 
-               $this->getResult()->addIndexedTagName(
-                       [ 'query', $this->getModuleName() ],
-                       'item'
-               );
+               $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
        }
 
        /**
         * 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() {
-               // We're after the revision table, and the corresponding page
-               // row for anything we retrieve. We may also need the
-               // recentchanges row and/or tag summary row.
-               $user = $this->getUser();
-               $tables = [ 'page', 'revision' ]; // Order may change
-               $this->addWhere( 'page_id=rev_page' );
+       private function prepareQuery( array $users, $limit, $which ) {
+               global $wgActorTableSchemaMigrationStage;
+
+               $this->resetQueryParams();
+               $db = $this->getDB();
+
+               $revQuery = Revision::getQueryInfo( [ 'page' ] );
+               $this->addTables( $revQuery['tables'] );
+               $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 ) {
+                       $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 {
+                       $tsField = 'rev_timestamp';
+                       $idField = 'rev_id';
+               }
 
                // Handle continue parameter
                if ( !is_null( $this->params['continue'] ) ) {
                        $continue = explode( '|', $this->params['continue'] );
-                       $db = $this->getDB();
                        if ( $this->multiUserMode ) {
                                $this->dieContinueUsageIf( count( $continue ) != 4 );
                                $modeFlag = array_shift( $continue );
-                               $this->dieContinueUsageIf( !in_array( $modeFlag, [ 'id', 'name' ] ) );
-                               if ( $this->idMode && $modeFlag === 'name' ) {
-                                       // The users were created since this query started, but we
-                                       // can't go back and change modes now. So just keep on with
-                                       // name mode.
-                                       $this->idMode = false;
-                               }
-                               $this->dieContinueUsageIf( ( $modeFlag === 'id' ) !== $this->idMode );
-                               $userField = $this->idMode ? 'rev_user' : 'rev_user_text';
+                               $this->dieContinueUsageIf( $modeFlag !== $this->orderBy );
                                $encUser = $db->addQuotes( array_shift( $continue ) );
                        } else {
                                $this->dieContinueUsageIf( count( $continue ) != 2 );
@@ -218,21 +479,22 @@ class ApiQueryContributions extends ApiQueryBase {
                                $this->addWhere(
                                        "$userField $op $encUser OR " .
                                        "($userField = $encUser AND " .
-                                       "(rev_timestamp $op $encTS OR " .
-                                       "(rev_timestamp = $encTS AND " .
-                                       "rev_id $op= $encId)))"
+                                       "($tsField $op $encTS OR " .
+                                       "($tsField = $encTS AND " .
+                                       "$idField $op= $encId)))"
                                );
                        } else {
                                $this->addWhere(
-                                       "rev_timestamp $op $encTS OR " .
-                                       "(rev_timestamp = $encTS AND " .
-                                       "rev_id $op= $encId)"
+                                       "$tsField $op $encTS OR " .
+                                       "($tsField = $encTS AND " .
+                                       "$idField $op= $encId)"
                                );
                        }
                }
 
                // Don't include any revisions where we're not supposed to be able to
                // see the username.
+               $user = $this->getUser();
                if ( !$user->isAllowed( 'deletedhistory' ) ) {
                        $bitmask = Revision::DELETED_USER;
                } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
@@ -241,29 +503,20 @@ class ApiQueryContributions extends ApiQueryBase {
                        $bitmask = 0;
                }
                if ( $bitmask ) {
-                       $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
+                       $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
                }
 
-               // We only want pages by the specified users.
-               if ( $this->prefixMode ) {
-                       $this->addWhere( 'rev_user_text' .
-                               $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) );
-               } elseif ( $this->idMode ) {
-                       $this->addWhereFld( 'rev_user', $this->userids );
-               } else {
-                       $this->addWhereFld( 'rev_user_text', $this->usernames );
-               }
-               // ... and in the specified timeframe.
-               // Ensure the same sort order for rev_user/rev_user_text and rev_timestamp
-               // so our query is indexed
-               if ( $this->multiUserMode ) {
-                       $this->addWhereRange( $this->idMode ? 'rev_user' : 'rev_user_text',
-                               $this->params['dir'], null, null );
+               // Add the user field to ORDER BY if there are multiple users
+               if ( count( $users ) > 1 ) {
+                       $this->addWhereRange( $orderUserField, $this->params['dir'], null, null );
                }
-               $this->addTimestampWhereRange( 'rev_timestamp',
+
+               // Then timestamp
+               $this->addTimestampWhereRange( $tsField,
                        $this->params['dir'], $this->params['start'], $this->params['end'] );
-               // Include in ORDER BY for uniqueness
-               $this->addWhereRange( 'rev_id', $this->params['dir'], null, null );
+
+               // Then rev_id for a total ordering
+               $this->addWhereRange( $idField, $this->params['dir'], null, null );
 
                $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
 
@@ -286,25 +539,12 @@ class ApiQueryContributions extends ApiQueryBase {
                        $this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) );
                        $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
                        $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
-                       $this->addWhereIf( 'rev_id != page_latest', isset( $show['!top'] ) );
-                       $this->addWhereIf( 'rev_id = page_latest', isset( $show['top'] ) );
+                       $this->addWhereIf( $idField . ' != page_latest', isset( $show['!top'] ) );
+                       $this->addWhereIf( $idField . ' = page_latest', isset( $show['top'] ) );
                        $this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) );
                        $this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) );
                }
-               $this->addOption( 'LIMIT', $this->params['limit'] + 1 );
-
-               // Mandatory fields: timestamp allows request continuation
-               // ns+title checks if the user has access rights for this page
-               // user_text is necessary if multiple users were specified
-               $this->addFields( [
-                       'rev_id',
-                       'rev_timestamp',
-                       'page_namespace',
-                       'page_title',
-                       'rev_user',
-                       'rev_user_text',
-                       'rev_deleted'
-               ] );
+               $this->addOption( 'LIMIT', $limit + 1 );
 
                if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
                        $this->fld_patrolled
@@ -313,48 +553,25 @@ class ApiQueryContributions extends ApiQueryBase {
                                $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
                        }
 
-                       // Use a redundant join condition on both
-                       // timestamp and ID so we can use the timestamp
-                       // index
-                       $index['recentchanges'] = 'rc_user_text';
-                       if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) {
-                               // Put the tables in the right order for
-                               // STRAIGHT_JOIN
-                               $tables = [ 'revision', 'recentchanges', 'page' ];
-                               $this->addOption( 'STRAIGHT_JOIN' );
-                               $this->addWhere( 'rc_user_text=rev_user_text' );
-                               $this->addWhere( 'rc_timestamp=rev_timestamp' );
-                               $this->addWhere( 'rc_this_oldid=rev_id' );
-                       } else {
-                               $tables[] = 'recentchanges';
-                               $this->addJoinConds( [ 'recentchanges' => [
-                                       'LEFT JOIN', [
-                                               'rc_user_text=rev_user_text',
-                                               'rc_timestamp=rev_timestamp',
-                                               'rc_this_oldid=rev_id' ] ] ] );
-                       }
+                       $this->addTables( 'recentchanges' );
+                       $this->addJoinConds( [ 'recentchanges' => [
+                               isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ? 'JOIN' : 'LEFT JOIN',
+                               [
+                                       // This is a crazy hack. recentchanges has no index on rc_this_oldid, so instead of adding
+                                       // one T19237 did a join using rc_user_text and rc_timestamp instead. Now rc_user_text is
+                                       // probably unavailable, so just do rc_timestamp.
+                                       'rc_timestamp = ' . $tsField,
+                                       'rc_this_oldid = ' . $idField,
+                               ]
+                       ] ] );
                }
 
-               $this->addTables( $tables );
-               $this->addFieldsIf( 'rev_page', $this->fld_ids );
-               $this->addFieldsIf( 'page_latest', $this->fld_flags );
-               // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed?
-               $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff );
-               $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags );
-               $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids );
                $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
 
-               if ( $this->fld_comment || $this->fld_parsedcomment ) {
-                       $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
-                       $this->addTables( $commentQuery['tables'] );
-                       $this->addFields( $commentQuery['fields'] );
-                       $this->addJoinConds( $commentQuery['joins'] );
-               }
-
                if ( $this->fld_tags ) {
                        $this->addTables( 'tag_summary' );
                        $this->addJoinConds(
-                               [ 'tag_summary' => [ 'LEFT JOIN', [ 'rev_id=ts_rev_id' ] ] ]
+                               [ 'tag_summary' => [ 'LEFT JOIN', [ $idField . ' = ts_rev_id' ] ] ]
                        );
                        $this->addFields( 'ts_tags' );
                }
@@ -362,14 +579,12 @@ class ApiQueryContributions extends ApiQueryBase {
                if ( isset( $this->params['tag'] ) ) {
                        $this->addTables( 'change_tag' );
                        $this->addJoinConds(
-                               [ 'change_tag' => [ 'INNER JOIN', [ 'rev_id=ct_rev_id' ] ] ]
+                               [ 'change_tag' => [ 'INNER JOIN', [ $idField . ' = ct_rev_id' ] ] ]
                        );
                        $this->addWhereFld( 'ct_tag', $this->params['tag'] );
                }
 
-               if ( isset( $index ) ) {
-                       $this->addOption( 'USE INDEX', $index );
-               }
+               return true;
        }
 
        /**
@@ -480,10 +695,13 @@ class ApiQueryContributions extends ApiQueryBase {
 
        private function continueStr( $row ) {
                if ( $this->multiUserMode ) {
-                       if ( $this->idMode ) {
-                               return "id|$row->rev_user|$row->rev_timestamp|$row->rev_id";
-                       } else {
-                               return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
+                       switch ( $this->orderBy ) {
+                               case 'id':
+                                       return "id|$row->rev_user|$row->rev_timestamp|$row->rev_id";
+                               case 'name':
+                                       return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
+                               case 'actor':
+                                       return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
                        }
                } else {
                        return "$row->rev_timestamp|$row->rev_id";
index b4b9321..23163c2 100644 (file)
@@ -340,11 +340,15 @@ class ApiStashEdit extends ApiBase {
         * @return string|null TS_MW timestamp or null
         */
        private static function lastEditTime( User $user ) {
-               $time = wfGetDB( DB_REPLICA )->selectField(
-                       'recentchanges',
+               $db = wfGetDB( DB_REPLICA );
+               $actorQuery = ActorMigration::newMigration()->getWhere( $db, 'rc_user', $user, false );
+               $time = $db->selectField(
+                       [ 'recentchanges' ] + $actorQuery['tables'],
                        'MAX(rc_timestamp)',
-                       [ 'rc_user_text' => $user->getName() ],
-                       __METHOD__
+                       [ $actorQuery['conds'] ],
+                       __METHOD__,
+                       [],
+                       $actorQuery['joins']
                );
 
                return wfTimestampOrNull( TS_MW, $time );
index 5c75292..cb68571 100644 (file)
@@ -80,6 +80,8 @@ class UserCache {
         * @param string $caller The calling method
         */
        public function doQuery( array $userIds, $options = [], $caller = '' ) {
+               global $wgActorTableSchemaMigrationStage;
+
                $usersToCheck = [];
                $usersToQuery = [];
 
@@ -100,21 +102,34 @@ class UserCache {
                // Lookup basic info for users not yet loaded...
                if ( count( $usersToQuery ) ) {
                        $dbr = wfGetDB( DB_REPLICA );
-                       $table = [ 'user' ];
+                       $tables = [ 'user' ];
                        $conds = [ 'user_id' => $usersToQuery ];
                        $fields = [ 'user_name', 'user_real_name', 'user_registration', 'user_id' ];
+                       $joinConds = [];
+
+                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                               $tables[] = 'actor';
+                               $fields[] = 'actor_id';
+                               $joinConds['actor'] = [
+                                       $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+                                       [ 'actor_user = user_id' ]
+                               ];
+                       }
 
                        $comment = __METHOD__;
                        if ( strval( $caller ) !== '' ) {
                                $comment .= "/$caller";
                        }
 
-                       $res = $dbr->select( $table, $fields, $conds, $comment );
+                       $res = $dbr->select( $tables, $fields, $conds, $comment, [], $joinConds );
                        foreach ( $res as $row ) { // load each user into cache
                                $userId = (int)$row->user_id;
                                $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 ) {
+                                       $this->cache[$userId]['actor'] = $row->actor_id;
+                               }
                                $usersToCheck[$userId] = $row->user_name;
                        }
                }
index 5b8559e..ac029a2 100644 (file)
@@ -646,6 +646,7 @@ class ChangesList extends ContextSource {
                                        'id' => $rc->mAttribs['rc_this_oldid'],
                                        'user' => $rc->mAttribs['rc_user'],
                                        'user_text' => $rc->mAttribs['rc_user_text'],
+                                       'actor' => isset( $rc->mAttribs['rc_actor'] ) ? $rc->mAttribs['rc_actor'] : null,
                                        'deleted' => $rc->mAttribs['rc_deleted']
                                ] );
                                $s .= ' ' . Linker::generateRollback( $rev, $this->getContext() );
index dfaa398..3dacf6a 100644 (file)
@@ -34,6 +34,7 @@
  *  rc_cur_id       page_id of associated page entry
  *  rc_user         user id who made the entry
  *  rc_user_text    user name who made the entry
+ *  rc_actor        actor id who made the entry
  *  rc_comment      edit summary
  *  rc_this_oldid   rev_id associated with this entry (or zero)
  *  rc_last_oldid   rev_id associated with the entry before this one (or zero)
@@ -210,12 +211,25 @@ class RecentChange {
         * @return array
         */
        public static function selectFields() {
+               global $wgActorTableSchemaMigrationStage;
+
                wfDeprecated( __METHOD__, '1.31' );
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+                       // 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.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                       );
+               }
+
                return [
                        'rc_id',
                        'rc_timestamp',
                        'rc_user',
                        'rc_user_text',
+                       'rc_actor' => 'NULL',
                        'rc_namespace',
                        'rc_title',
                        'rc_minor',
@@ -249,13 +263,12 @@ class RecentChange {
         */
        public static function getQueryInfo() {
                $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
                return [
-                       'tables' => [ 'recentchanges' ] + $commentQuery['tables'],
+                       'tables' => [ 'recentchanges' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        'fields' => [
                                'rc_id',
                                'rc_timestamp',
-                               'rc_user',
-                               'rc_user_text',
                                'rc_namespace',
                                'rc_title',
                                'rc_minor',
@@ -275,8 +288,8 @@ class RecentChange {
                                'rc_log_type',
                                'rc_log_action',
                                'rc_params',
-                       ] + $commentQuery['fields'],
-                       'joins' => $commentQuery['joins'],
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
+                       'joins' => $commentQuery['joins'] + $actorQuery['joins'],
                ];
        }
 
@@ -314,10 +327,14 @@ class RecentChange {
         */
        public function getPerformer() {
                if ( $this->mPerformer === false ) {
-                       if ( $this->mAttribs['rc_user'] ) {
+                       if ( !empty( $this->mAttribs['rc_actor'] ) ) {
+                               $this->mPerformer = User::newFromActorId( $this->mAttribs['rc_actor'] );
+                       } elseif ( !empty( $this->mAttribs['rc_user'] ) ) {
                                $this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] );
-                       } else {
+                       } elseif ( !empty( $this->mAttribs['rc_user_text'] ) ) {
                                $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
+                       } else {
+                               throw new MWException( 'RecentChange object lacks rc_actor, rc_user, and rc_user_text' );
                        }
                }
 
@@ -368,12 +385,22 @@ class RecentChange {
                        unset( $this->mAttribs['rc_cur_id'] );
                }
 
-               # Convert mAttribs['rc_comment'] for CommentStore
                $row = $this->mAttribs;
+
+               # Convert mAttribs['rc_comment'] for CommentStore
                $comment = $row['rc_comment'];
                unset( $row['rc_comment'], $row['rc_comment_text'], $row['rc_comment_data'] );
                $row += CommentStore::getStore()->insert( $dbw, 'rc_comment', $comment );
 
+               # Convert mAttribs['rc_user'] etc for ActorMigration
+               $user = User::newFromAnyId(
+                       isset( $row['rc_user'] ) ? $row['rc_user'] : null,
+                       isset( $row['rc_user_text'] ) ? $row['rc_user_text'] : null,
+                       isset( $row['rc_actor'] ) ? $row['rc_actor'] : null
+               );
+               unset( $row['rc_user'], $row['rc_user_text'], $row['rc_actor'] );
+               $row += ActorMigration::newMigration()->getInsertValues( $dbw, 'rc_user', $user );
+
                # Don't reuse an existing rc_id for the new row, if one happens to be
                # set for some reason.
                unset( $row['rc_id'] );
@@ -642,6 +669,7 @@ class RecentChange {
                        'rc_cur_id' => $title->getArticleID(),
                        'rc_user' => $user->getId(),
                        'rc_user_text' => $user->getName(),
+                       'rc_actor' => $user->getActorId(),
                        'rc_comment' => &$comment,
                        'rc_comment_text' => &$comment,
                        'rc_comment_data' => null,
@@ -717,6 +745,7 @@ class RecentChange {
                        'rc_cur_id' => $title->getArticleID(),
                        'rc_user' => $user->getId(),
                        'rc_user_text' => $user->getName(),
+                       'rc_actor' => $user->getActorId(),
                        'rc_comment' => &$comment,
                        'rc_comment_text' => &$comment,
                        'rc_comment_data' => null,
@@ -849,6 +878,7 @@ class RecentChange {
                        'rc_cur_id' => $target->getArticleID(),
                        'rc_user' => $user->getId(),
                        'rc_user_text' => $user->getName(),
+                       'rc_actor' => $user->getActorId(),
                        'rc_comment' => &$logComment,
                        'rc_comment_text' => &$logComment,
                        'rc_comment_data' => null,
@@ -934,6 +964,7 @@ class RecentChange {
                        'rc_cur_id' => $pageTitle->getArticleID(),
                        'rc_user' => $user ? $user->getId() : 0,
                        'rc_user_text' => $user ? $user->getName() : '',
+                       'rc_actor' => $user ? $user->getActorId() : null,
                        'rc_comment' => &$comment,
                        'rc_comment_text' => &$comment,
                        'rc_comment_data' => null,
@@ -1002,6 +1033,15 @@ class RecentChange {
                $this->mAttribs['rc_comment'] = &$comment;
                $this->mAttribs['rc_comment_text'] = &$comment;
                $this->mAttribs['rc_comment_data'] = null;
+
+               $user = User::newFromAnyId(
+                       isset( $this->mAttribs['rc_user'] ) ? $this->mAttribs['rc_user'] : null,
+                       isset( $this->mAttribs['rc_user_text'] ) ? $this->mAttribs['rc_user_text'] : null,
+                       isset( $this->mAttribs['rc_actor'] ) ? $this->mAttribs['rc_actor'] : null
+               );
+               $this->mAttribs['rc_user'] = $user->getId();
+               $this->mAttribs['rc_user_text'] = $user->getName();
+               $this->mAttribs['rc_actor'] = $user->getActorId();
        }
 
        /**
@@ -1015,6 +1055,24 @@ class RecentChange {
                        return CommentStore::getStore()
                                ->getComment( 'rc_comment', $this->mAttribs, true )->text;
                }
+
+               if ( $name === 'rc_user' || $name === 'rc_user_text' || $name === 'rc_actor' ) {
+                       $user = User::newFromAnyId(
+                               isset( $this->mAttribs['rc_user'] ) ? $this->mAttribs['rc_user'] : null,
+                               isset( $this->mAttribs['rc_user_text'] ) ? $this->mAttribs['rc_user_text'] : null,
+                               isset( $this->mAttribs['rc_actor'] ) ? $this->mAttribs['rc_actor'] : null
+                       );
+                       if ( $name === 'rc_user' ) {
+                               return $user->getId();
+                       }
+                       if ( $name === 'rc_user_text' ) {
+                               return $user->getName();
+                       }
+                       if ( $name === 'rc_actor' ) {
+                               return $user->getActorId();
+                       }
+               }
+
                return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
        }
 
index b78efaf..a248c6e 100644 (file)
@@ -44,6 +44,10 @@ class ChangeTagsLogItem extends RevisionItemBase {
                return 'log_user_text';
        }
 
+       public function getAuthorActorField() {
+               return 'log_actor';
+       }
+
        public function canView() {
                return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED, $this->list->getUser() );
        }
index edfc81c..eab3afb 100644 (file)
@@ -986,13 +986,17 @@ abstract class ContentHandler {
 
                // Find out if there was only one contributor
                // Only scan the last 20 revisions
-               $res = $dbr->select( 'revision', 'rev_user_text',
+               $revQuery = Revision::getQueryInfo();
+               $res = $dbr->select(
+                       $revQuery['tables'],
+                       [ 'rev_user_text' => $revQuery['fields']['rev_user_text'] ],
                        [
                                'rev_page' => $title->getArticleID(),
                                $dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
                        ],
                        __METHOD__,
-                       [ 'LIMIT' => 20 ]
+                       [ 'LIMIT' => 20 ],
+                       $revQuery['joins']
                );
 
                if ( $res === false ) {
index 156e315..225a36c 100644 (file)
@@ -1179,13 +1179,16 @@ class DatabaseOracle extends Database {
        }
 
        public function delete( $table, $conds, $fname = __METHOD__ ) {
+               global $wgActorTableSchemaMigrationStage;
+
                if ( is_array( $conds ) ) {
                        $conds = $this->wrapConditionsForWhere( $table, $conds );
                }
                // a hack for deleting pages, users and images (which have non-nullable FKs)
                // 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' ) ) {
+               if ( $table == $this->tableName( 'user' ) && $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
                        $this->update( 'archive', [ 'ar_user' => 0 ],
                                [ 'ar_user' => $conds['user_id'] ], $fname );
                        $this->update( 'ipblocks', [ 'ipb_user' => 0 ],
index 2f882b8..79aab7d 100644 (file)
@@ -148,18 +148,21 @@ class SiteStatsUpdate implements DeferrableUpdate, MergeableUpdate {
                $dbr = $services->getDBLoadBalancer()->getConnection( DB_REPLICA, 'vslow' );
                # Get non-bot users than did some recent action other than making accounts.
                # If account creation is included, the number gets inflated ~20+ fold on enwiki.
+               $rcQuery = RecentChange::getQueryInfo();
                $activeUsers = $dbr->selectField(
-                       'recentchanges',
-                       'COUNT( DISTINCT rc_user_text )',
+                       $rcQuery['tables'],
+                       'COUNT( DISTINCT ' . $rcQuery['fields']['rc_user_text'] . ' )',
                        [
                                'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Exclude external (Wikidata)
-                               'rc_user != 0',
+                               ActorMigration::newMigration()->isNotAnon( $rcQuery['fields']['rc_user'] ),
                                'rc_bot' => 0,
                                'rc_log_type != ' . $dbr->addQuotes( 'newusers' ) . ' OR rc_log_type IS NULL',
                                'rc_timestamp >= ' . $dbr->addQuotes(
                                        $dbr->timestamp( time() - $config->get( 'ActiveUserDays' ) * 24 * 3600 ) ),
                        ],
-                       __METHOD__
+                       __METHOD__,
+                       [],
+                       $rcQuery['joins']
                );
                $dbw->update(
                        'site_stats',
diff --git a/includes/exception/CannotCreateActorException.php b/includes/exception/CannotCreateActorException.php
new file mode 100644 (file)
index 0000000..7c7ccfc
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Exception thrown when some operation failed
+ *
+ * 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
+ *
+ * @since 1.31
+ */
+
+/**
+ * Exception thrown when an actor can't be created.
+ */
+class CannotCreateActorException extends RuntimeException {
+}
index 6ce55ea..6c7a449 100644 (file)
@@ -227,15 +227,20 @@ class WikiExporter {
                $this->author_list = "<contributors>";
                // rev_deleted
 
+               $revQuery = Revision::getQueryInfo( [ 'page' ] );
                $res = $this->db->select(
-                       [ 'page', 'revision' ],
-                       [ 'DISTINCT rev_user_text', 'rev_user' ],
+                       $revQuery['tables'],
+                       [
+                               'rev_user_text' => $revQuery['fields']['rev_user_text'],
+                               'rev_user' => $revQuery['fields']['rev_user'],
+                       ],
                        [
                                $this->db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0',
                                $cond,
-                               'page_id = rev_id',
                        ],
-                       __METHOD__
+                       __METHOD__,
+                       [ 'DISTINCT' ],
+                       $revQuery['joins']
                );
 
                foreach ( $res as $row ) {
@@ -279,14 +284,18 @@ class WikiExporter {
                        $result = null; // Assuring $result is not undefined, if exception occurs early
 
                        $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+                       $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
 
                        try {
-                               $result = $this->db->select( [ 'logging', 'user' ] + $commentQuery['tables'],
-                                       [ "{$logging}.*", 'user_name' ] + $commentQuery['fields'], // grab the user name
+                               $result = $this->db->select(
+                                       array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
+                                       [ "{$logging}.*", 'user_name' ] + $commentQuery['fields'] + $actorQuery['fields'],
                                        $where,
                                        __METHOD__,
                                        [ 'ORDER BY' => 'log_id', 'USE INDEX' => [ 'logging' => 'PRIMARY' ] ],
-                                       [ 'user' => [ 'JOIN', 'user_id = log_user' ] ] + $commentQuery['joins']
+                                       [
+                                               'user' => [ 'JOIN', 'user_id = ' . $actorQuery['fields']['log_user'] ]
+                                       ] + $commentQuery['joins'] + $actorQuery['joins']
                                );
                                $this->outputLogStream( $result );
                                if ( $this->buffer == self::STREAM ) {
@@ -321,13 +330,29 @@ class WikiExporter {
                        }
                # For page dumps...
                } else {
-                       $tables = [ 'page', 'revision' ];
+                       $revOpts = [ 'page' ];
+                       if ( $this->text != self::STUB ) {
+                               $revOpts[] = 'text';
+                       }
+                       $revQuery = Revision::getQueryInfo( $revOpts );
+
+                       // We want page primary rather than revision
+                       $tables = array_merge( [ 'page' ], array_diff( $revQuery['tables'], [ 'page' ] ) );
+                       $join = $revQuery['joins'] + [
+                               'revision' => $revQuery['joins']['page']
+                       ];
+                       unset( $join['page'] );
+
+                       $fields = array_merge( $revQuery['fields'], [ 'page_restrictions' ] );
+
+                       $conds = [];
+                       if ( $cond !== '' ) {
+                               $conds[] = $cond;
+                       }
                        $opts = [ 'ORDER BY' => 'page_id ASC' ];
                        $opts['USE INDEX'] = [];
-                       $join = [];
                        if ( is_array( $this->history ) ) {
                                # Time offset/limit for all pages/history...
-                               $revJoin = 'page_id=rev_page';
                                # Set time order
                                if ( $this->history['dir'] == 'asc' ) {
                                        $op = '>';
@@ -338,10 +363,9 @@ class WikiExporter {
                                }
                                # Set offset
                                if ( !empty( $this->history['offset'] ) ) {
-                                       $revJoin .= " AND rev_timestamp $op " .
+                                       $conds[] = "rev_timestamp $op " .
                                                $this->db->addQuotes( $this->db->timestamp( $this->history['offset'] ) );
                                }
-                               $join['revision'] = [ 'INNER JOIN', $revJoin ];
                                # Set query limit
                                if ( !empty( $this->history['limit'] ) ) {
                                        $opts['LIMIT'] = intval( $this->history['limit'] );
@@ -350,13 +374,11 @@ class WikiExporter {
                                # Full history dumps...
                                # query optimization for history stub dumps
                                if ( $this->text == self::STUB && $orderRevs ) {
-                                       $tables = [ 'revision', 'page' ];
-                                       $opts[] = 'STRAIGHT_JOIN';
+                                       $tables = $revQuery['tables'];
                                        $opts['ORDER BY'] = [ 'rev_page ASC', 'rev_id ASC' ];
                                        $opts['USE INDEX']['revision'] = 'rev_page_id';
+                                       unset( $join['revision'] );
                                        $join['page'] = [ 'INNER JOIN', 'rev_page=page_id' ];
-                               } else {
-                                       $join['revision'] = [ 'INNER JOIN', 'page_id=rev_page' ];
                                }
                        } elseif ( $this->history & self::CURRENT ) {
                                # Latest revision dumps...
@@ -374,22 +396,11 @@ class WikiExporter {
                                }
                        } elseif ( $this->history & self::RANGE ) {
                                # Dump of revisions within a specified range
-                               $join['revision'] = [ 'INNER JOIN', 'page_id=rev_page' ];
                                $opts['ORDER BY'] = [ 'rev_page ASC', 'rev_id ASC' ];
                        } else {
                                # Unknown history specification parameter?
                                throw new MWException( __METHOD__ . " given invalid history dump type." );
                        }
-                       # Query optimization hacks
-                       if ( $cond == '' ) {
-                               $opts[] = 'STRAIGHT_JOIN';
-                               $opts['USE INDEX']['page'] = 'PRIMARY';
-                       }
-                       # Build text join options
-                       if ( $this->text != self::STUB ) { // 1-pass
-                               $tables[] = 'text';
-                               $join['text'] = [ 'INNER JOIN', 'rev_text_id=old_id' ];
-                       }
 
                        if ( $this->buffer == self::STREAM ) {
                                $prev = $this->db->bufferResults( false );
@@ -399,16 +410,14 @@ class WikiExporter {
                                Hooks::run( 'ModifyExportQuery',
                                                [ $this->db, &$tables, &$cond, &$opts, &$join ] );
 
-                               $commentQuery = CommentStore::getStore()->getJoin( 'rev_comment' );
-
                                # Do the query!
                                $result = $this->db->select(
-                                       $tables + $commentQuery['tables'],
-                                       [ '*' ] + $commentQuery['fields'],
-                                       $cond,
+                                       $tables,
+                                       $fields,
+                                       $conds,
                                        __METHOD__,
                                        $opts,
-                                       $join + $commentQuery['joins']
+                                       $join
                                );
                                # Output dump results
                                $this->outputPageStream( $result );
index 6577ab6..982fea4 100644 (file)
@@ -63,12 +63,9 @@ class ArchivedFile {
        /** @var string Upload description */
        private $description;
 
-       /** @var int User ID of uploader */
+       /** @var User|null Uploader */
        private $user;
 
-       /** @var string User name of uploader */
-       private $user_text;
-
        /** @var string Time of upload */
        private $timestamp;
 
@@ -116,8 +113,7 @@ class ArchivedFile {
                $this->mime = "unknown/unknown";
                $this->media_type = '';
                $this->description = '';
-               $this->user = 0;
-               $this->user_text = '';
+               $this->user = null;
                $this->timestamp = null;
                $this->deleted = 0;
                $this->dataLoaded = false;
@@ -221,6 +217,18 @@ class ArchivedFile {
         * @return array
         */
        static function selectFields() {
+               global $wgActorTableSchemaMigrationStage;
+
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+                       // 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.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                       );
+               }
+
                wfDeprecated( __METHOD__, '1.31' );
                return [
                        'fa_id',
@@ -238,6 +246,7 @@ class ArchivedFile {
                        'fa_minor_mime',
                        'fa_user',
                        'fa_user_text',
+                       'fa_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'fa_actor' : null,
                        'fa_timestamp',
                        'fa_deleted',
                        'fa_deleted_timestamp', /* Used by LocalFileRestoreBatch */
@@ -256,8 +265,9 @@ class ArchivedFile {
         */
        public static function getQueryInfo() {
                $commentQuery = CommentStore::getStore()->getJoin( 'fa_description' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'fa_user' );
                return [
-                       'tables' => [ 'filearchive' ] + $commentQuery['tables'],
+                       'tables' => [ 'filearchive' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        'fields' => [
                                'fa_id',
                                'fa_name',
@@ -272,14 +282,12 @@ class ArchivedFile {
                                'fa_media_type',
                                'fa_major_mime',
                                'fa_minor_mime',
-                               'fa_user',
-                               'fa_user_text',
                                'fa_timestamp',
                                'fa_deleted',
                                'fa_deleted_timestamp', /* Used by LocalFileRestoreBatch */
                                'fa_sha1',
-                       ] + $commentQuery['fields'],
-                       'joins' => $commentQuery['joins'],
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
+                       'joins' => $commentQuery['joins'] + $actorQuery['joins'],
                ];
        }
 
@@ -305,8 +313,7 @@ class ArchivedFile {
                $this->description = CommentStore::getStore()
                        // Legacy because $row may have come from self::selectFields()
                        ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'fa_description', $row )->text;
-               $this->user = $row->fa_user;
-               $this->user_text = $row->fa_user_text;
+               $this->user = User::newFromAnyId( $row->fa_user, $row->fa_user_text, $row->fa_actor );
                $this->timestamp = $row->fa_timestamp;
                $this->deleted = $row->fa_deleted;
                if ( isset( $row->fa_sha1 ) ) {
@@ -519,17 +526,20 @@ class ArchivedFile {
         * @note Prior to MediaWiki 1.23, this method always
         *   returned the user id, and was inconsistent with
         *   the rest of the file classes.
-        * @param string $type 'text' or 'id'
-        * @return int|string
+        * @param string $type 'text', 'id', or 'object'
+        * @return int|string|User|null
         * @throws MWException
+        * @since 1.31 added 'object'
         */
        public function getUser( $type = 'text' ) {
                $this->load();
 
-               if ( $type == 'text' ) {
-                       return $this->user_text;
-               } elseif ( $type == 'id' ) {
-                       return (int)$this->user;
+               if ( $type === 'object' ) {
+                       return $this->user;
+               } elseif ( $type === 'text' ) {
+                       return $this->user ? $this->user->getName() : '';
+               } elseif ( $type === 'id' ) {
+                       return $this->user ? $this->user->getId() : 0;
                }
 
                throw new MWException( "Unknown type '$type'." );
@@ -555,9 +565,7 @@ class ArchivedFile {
         * @return int
         */
        public function getRawUser() {
-               $this->load();
-
-               return $this->user;
+               return $this->getUser( 'id' );
        }
 
        /**
@@ -566,9 +574,7 @@ class ArchivedFile {
         * @return string
         */
        public function getRawUserText() {
-               $this->load();
-
-               return $this->user_text;
+               return $this->getUser( 'text' );
        }
 
        /**
index 263e45b..ec4a5fb 100644 (file)
@@ -102,12 +102,9 @@ class LocalFile extends File {
        /** @var string Upload timestamp */
        private $timestamp;
 
-       /** @var int User ID of uploader */
+       /** @var User Uploader */
        private $user;
 
-       /** @var string User name of uploader */
-       private $user_text;
-
        /** @var string Description of current revision of the file */
        private $description;
 
@@ -201,7 +198,19 @@ class LocalFile extends File {
         * @return array
         */
        static function selectFields() {
+               global $wgActorTableSchemaMigrationStage;
+
                wfDeprecated( __METHOD__, '1.31' );
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+                       // 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.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                       );
+               }
+
                return [
                        'img_name',
                        'img_size',
@@ -214,6 +223,7 @@ class LocalFile extends File {
                        'img_minor_mime',
                        'img_user',
                        'img_user_text',
+                       'img_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'img_actor' : null,
                        'img_timestamp',
                        'img_sha1',
                ] + CommentStore::getStore()->getFields( 'img_description' );
@@ -232,8 +242,9 @@ class LocalFile extends File {
         */
        public static function getQueryInfo( array $options = [] ) {
                $commentQuery = CommentStore::getStore()->getJoin( 'img_description' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'img_user' );
                $ret = [
-                       'tables' => [ 'image' ] + $commentQuery['tables'],
+                       'tables' => [ 'image' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        'fields' => [
                                'img_name',
                                'img_size',
@@ -244,12 +255,10 @@ class LocalFile extends File {
                                'img_media_type',
                                'img_major_mime',
                                'img_minor_mime',
-                               'img_user',
-                               'img_user_text',
                                'img_timestamp',
                                'img_sha1',
-                       ] + $commentQuery['fields'],
-                       'joins' => $commentQuery['joins'],
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
+                       'joins' => $commentQuery['joins'] + $actorQuery['joins'],
                ];
 
                if ( in_array( 'omit-nonlazy', $options, true ) ) {
@@ -330,6 +339,10 @@ class LocalFile extends File {
                                                $cacheVal[$field] = $this->$field;
                                        }
                                }
+                               $cacheVal['user'] = $this->user ? $this->user->getId() : 0;
+                               $cacheVal['user_text'] = $this->user ? $this->user->getName() : '';
+                               $cacheVal['actor'] = $this->user ? $this->user->getActorId() : null;
+
                                // Strip off excessive entries from the subset of fields that can become large.
                                // If the cache value gets to large it will not fit in memcached and nothing will
                                // get cached at all, causing master queries for any file access.
@@ -407,8 +420,7 @@ class LocalFile extends File {
                // and self::loadFromCache() for the caching, and self::setProps() for
                // populating the object from an array of data.
                return [ 'size', 'width', 'height', 'bits', 'media_type',
-                       'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
-                       'user_text', 'description' ];
+                       'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'description' ];
        }
 
        /**
@@ -570,6 +582,13 @@ class LocalFile extends File {
                $decoded['description'] = CommentStore::getStore()
                        ->getComment( 'description', (object)$decoded )->text;
 
+               $decoded['user'] = User::newFromAnyId(
+                       isset( $decoded['user'] ) ? $decoded['user'] : null,
+                       isset( $decoded['user_text'] ) ? $decoded['user_text'] : null,
+                       isset( $decoded['actor'] ) ? $decoded['actor'] : null
+               );
+               unset( $decoded['user_text'], $decoded['actor'] );
+
                $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
 
                $decoded['metadata'] = $this->repo->getReplicaDB()->decodeBlob( $decoded['metadata'] );
@@ -751,6 +770,14 @@ class LocalFile extends File {
                        }
                }
 
+               if ( isset( $info['user'] ) || isset( $info['user_text'] ) || isset( $info['actor'] ) ) {
+                       $this->user = User::newFromAnyId(
+                               isset( $info['user'] ) ? $info['user'] : null,
+                               isset( $info['user_text'] ) ? $info['user_text'] : null,
+                               isset( $info['actor'] ) ? $info['actor'] : null
+                       );
+               }
+
                // Fix up mime fields
                if ( isset( $info['major_mime'] ) ) {
                        $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
@@ -845,19 +872,24 @@ class LocalFile extends File {
        }
 
        /**
-        * Returns ID or name of user who uploaded the file
+        * Returns user who uploaded the file
         *
-        * @param string $type 'text' or 'id'
-        * @return int|string
+        * @param string $type 'text', 'id', or 'object'
+        * @return int|string|User
+        * @since 1.31 Added 'object'
         */
        function getUser( $type = 'text' ) {
                $this->load();
 
-               if ( $type == 'text' ) {
-                       return $this->user_text;
-               } else { // id
-                       return (int)$this->user;
+               if ( $type === 'object' ) {
+                       return $this->user;
+               } elseif ( $type === 'text' ) {
+                       return $this->user->getName();
+               } elseif ( $type === 'id' ) {
+                       return $this->user->getId();
                }
+
+               throw new MWException( "Unknown type '$type'." );
        }
 
        /**
@@ -1392,7 +1424,7 @@ class LocalFile extends File {
        function recordUpload2(
                $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
        ) {
-               global $wgCommentTableSchemaMigrationStage;
+               global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
 
                if ( is_null( $user ) ) {
                        global $wgUser;
@@ -1414,6 +1446,7 @@ class LocalFile extends File {
                $props['description'] = $comment;
                $props['user'] = $user->getId();
                $props['user_text'] = $user->getName();
+               $props['actor'] = $user->getActorId( $dbw );
                $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
                $this->setProps( $props );
 
@@ -1432,6 +1465,8 @@ class LocalFile extends File {
                $commentStore = CommentStore::getStore();
                list( $commentFields, $commentCallback ) =
                        $commentStore->insertWithTempTable( $dbw, 'img_description', $comment );
+               $actorMigration = ActorMigration::newMigration();
+               $actorFields = $actorMigration->getInsertValues( $dbw, 'img_user', $user );
                $dbw->insert( 'image',
                        [
                                'img_name' => $this->getName(),
@@ -1443,11 +1478,9 @@ class LocalFile extends File {
                                'img_major_mime' => $this->major_mime,
                                'img_minor_mime' => $this->minor_mime,
                                'img_timestamp' => $timestamp,
-                               'img_user' => $user->getId(),
-                               'img_user_text' => $user->getName(),
                                'img_metadata' => $dbw->encodeBlob( $this->metadata ),
                                'img_sha1' => $this->sha1
-                       ] + $commentFields,
+                       ] + $commentFields + $actorFields,
                        __METHOD__,
                        'IGNORE'
                );
@@ -1490,8 +1523,6 @@ class LocalFile extends File {
                                'oi_height' => 'img_height',
                                'oi_bits' => 'img_bits',
                                'oi_timestamp' => 'img_timestamp',
-                               'oi_user' => 'img_user',
-                               'oi_user_text' => 'img_user_text',
                                'oi_metadata' => 'img_metadata',
                                'oi_media_type' => 'img_media_type',
                                'oi_major_mime' => 'img_major_mime',
@@ -1534,6 +1565,37 @@ class LocalFile extends File {
                                }
                        }
 
+                       if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                               $fields['oi_user'] = 'img_user';
+                               $fields['oi_user_text'] = 'img_user_text';
+                       }
+                       if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                               $fields['oi_actor'] = 'img_actor';
+                       }
+
+                       if ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD &&
+                               $wgActorTableSchemaMigrationStage !== MIGRATION_NEW
+                       ) {
+                               // Upgrade any rows that are still old-style. Otherwise an upgrade
+                               // might be missed if a deletion happens while the migration script
+                               // is running.
+                               $res = $dbw->select(
+                                       [ 'image' ],
+                                       [ 'img_name', 'img_user', 'img_user_text' ],
+                                       [ 'img_name' => $this->getName(), 'img_actor' => 0 ],
+                                       __METHOD__
+                               );
+                               foreach ( $res as $row ) {
+                                       $actorId = User::newFromAnyId( $row->img_user, $row->img_user_text, null )->getActorId( $dbw );
+                                       $dbw->update(
+                                               'image',
+                                               [ 'img_actor' => $actorId ],
+                                               [ 'img_name' => $row->img_name, 'img_actor' => 0 ],
+                                               __METHOD__
+                                       );
+                               }
+                       }
+
                        # (T36993) Note: $oldver can be empty here, if the previous
                        # version of the file was broken. Allow registration of the new
                        # version to continue anyway, because that's better than having
@@ -1554,11 +1616,9 @@ class LocalFile extends File {
                                        'img_major_mime' => $this->major_mime,
                                        'img_minor_mime' => $this->minor_mime,
                                        'img_timestamp' => $timestamp,
-                                       'img_user' => $user->getId(),
-                                       'img_user_text' => $user->getName(),
                                        'img_metadata' => $dbw->encodeBlob( $this->metadata ),
                                        'img_sha1' => $this->sha1
-                               ] + $commentFields,
+                               ] + $commentFields + $actorFields,
                                [ 'img_name' => $this->getName() ],
                                __METHOD__
                        );
@@ -2405,12 +2465,13 @@ class LocalFileDeleteBatch {
        }
 
        protected function doDBInserts() {
-               global $wgCommentTableSchemaMigrationStage;
+               global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
 
                $now = time();
                $dbw = $this->file->repo->getMasterDB();
 
                $commentStore = CommentStore::getStore();
+               $actorMigration = ActorMigration::newMigration();
 
                $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
                $encUserId = $dbw->addQuotes( $this->user->getId() );
@@ -2449,8 +2510,6 @@ class LocalFileDeleteBatch {
                                'fa_media_type' => 'img_media_type',
                                'fa_major_mime' => 'img_major_mime',
                                'fa_minor_mime' => 'img_minor_mime',
-                               'fa_user' => 'img_user',
-                               'fa_user_text' => 'img_user_text',
                                'fa_timestamp' => 'img_timestamp',
                                'fa_sha1' => 'img_sha1'
                        ];
@@ -2495,6 +2554,37 @@ class LocalFileDeleteBatch {
                                }
                        }
 
+                       if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                               $fields['fa_user'] = 'img_user';
+                               $fields['fa_user_text'] = 'img_user_text';
+                       }
+                       if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                               $fields['fa_actor'] = 'img_actor';
+                       }
+
+                       if ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD &&
+                               $wgActorTableSchemaMigrationStage !== MIGRATION_NEW
+                       ) {
+                               // Upgrade any rows that are still old-style. Otherwise an upgrade
+                               // might be missed if a deletion happens while the migration script
+                               // is running.
+                               $res = $dbw->select(
+                                       [ 'image' ],
+                                       [ 'img_name', 'img_user', 'img_user_text' ],
+                                       [ 'img_name' => $this->file->getName(), 'img_actor' => 0 ],
+                                       __METHOD__
+                               );
+                               foreach ( $res as $row ) {
+                                       $actorId = User::newFromAnyId( $row->img_user, $row->img_user_text, null )->getActorId( $dbw );
+                                       $dbw->update(
+                                               'image',
+                                               [ 'img_actor' => $actorId ],
+                                               [ 'img_name' => $row->img_name, 'img_actor' => 0 ],
+                                               __METHOD__
+                                       );
+                               }
+                       }
+
                        $dbw->insertSelect( 'filearchive', $tables, $fields,
                                [ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins );
                }
@@ -2517,6 +2607,7 @@ class LocalFileDeleteBatch {
                                $reason = $commentStore->createComment( $dbw, $this->reason );
                                foreach ( $res as $row ) {
                                        $comment = $commentStore->getComment( 'oi_description', $row );
+                                       $user = User::newFromAnyId( $row->oi_user, $row->oi_user_text, $row->oi_actor );
                                        $rowsInsert[] = [
                                                // Deletion-specific fields
                                                'fa_storage_group' => 'deleted',
@@ -2537,12 +2628,11 @@ class LocalFileDeleteBatch {
                                                'fa_media_type' => $row->oi_media_type,
                                                'fa_major_mime' => $row->oi_major_mime,
                                                'fa_minor_mime' => $row->oi_minor_mime,
-                                               'fa_user' => $row->oi_user,
-                                               'fa_user_text' => $row->oi_user_text,
                                                'fa_timestamp' => $row->oi_timestamp,
                                                'fa_sha1' => $row->oi_sha1
                                        ] + $commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
-                                       + $commentStore->insert( $dbw, 'fa_description', $comment );
+                                       + $commentStore->insert( $dbw, 'fa_description', $comment )
+                                       + $actorMigration->getInsertValues( $dbw, 'fa_user', $user );
                                }
                        }
 
@@ -2741,6 +2831,7 @@ class LocalFileRestoreBatch {
                $dbw = $this->file->repo->getMasterDB();
 
                $commentStore = CommentStore::getStore();
+               $actorMigration = ActorMigration::newMigration();
 
                $status = $this->file->repo->newGood();
 
@@ -2829,11 +2920,13 @@ class LocalFileRestoreBatch {
                        }
 
                        $comment = $commentStore->getComment( 'fa_description', $row );
+                       $user = User::newFromAnyId( $row->fa_user, $row->fa_user_text, $row->fa_actor );
                        if ( $first && !$exists ) {
                                // This revision will be published as the new current version
                                $destRel = $this->file->getRel();
                                list( $commentFields, $commentCallback ) =
                                        $commentStore->insertWithTempTable( $dbw, 'img_description', $comment );
+                               $actorFields = $actorMigration->getInsertValues( $dbw, 'img_user', $user );
                                $insertCurrent = [
                                        'img_name' => $row->fa_name,
                                        'img_size' => $row->fa_size,
@@ -2844,11 +2937,9 @@ class LocalFileRestoreBatch {
                                        'img_media_type' => $props['media_type'],
                                        'img_major_mime' => $props['major_mime'],
                                        'img_minor_mime' => $props['minor_mime'],
-                                       'img_user' => $row->fa_user,
-                                       'img_user_text' => $row->fa_user_text,
                                        'img_timestamp' => $row->fa_timestamp,
                                        'img_sha1' => $sha1
-                               ] + $commentFields;
+                               ] + $commentFields + $actorFields;
 
                                // The live (current) version cannot be hidden!
                                if ( !$this->unsuppress && $row->fa_deleted ) {
@@ -2880,8 +2971,6 @@ class LocalFileRestoreBatch {
                                        'oi_width' => $row->fa_width,
                                        'oi_height' => $row->fa_height,
                                        'oi_bits' => $row->fa_bits,
-                                       'oi_user' => $row->fa_user,
-                                       'oi_user_text' => $row->fa_user_text,
                                        'oi_timestamp' => $row->fa_timestamp,
                                        'oi_metadata' => $props['metadata'],
                                        'oi_media_type' => $props['media_type'],
@@ -2889,7 +2978,8 @@ class LocalFileRestoreBatch {
                                        'oi_minor_mime' => $props['minor_mime'],
                                        'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
                                        'oi_sha1' => $sha1
-                               ] + $commentStore->insert( $dbw, 'oi_description', $comment );
+                               ] + $commentStore->insert( $dbw, 'oi_description', $comment )
+                               + $actorMigration->getInsertValues( $dbw, 'oi_user', $user );
                        }
 
                        $deleteIds[] = $row->fa_id;
index d08d0ae..65f0fb1 100644 (file)
@@ -110,7 +110,19 @@ class OldLocalFile extends LocalFile {
         * @return array
         */
        static function selectFields() {
+               global $wgActorTableSchemaMigrationStage;
+
                wfDeprecated( __METHOD__, '1.31' );
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
+                       // 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.
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
+                       );
+               }
+
                return [
                        'oi_name',
                        'oi_archive_name',
@@ -124,6 +136,7 @@ class OldLocalFile extends LocalFile {
                        'oi_minor_mime',
                        'oi_user',
                        'oi_user_text',
+                       'oi_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'oi_actor' : null,
                        'oi_timestamp',
                        'oi_deleted',
                        'oi_sha1',
@@ -143,8 +156,9 @@ class OldLocalFile extends LocalFile {
         */
        public static function getQueryInfo( array $options = [] ) {
                $commentQuery = CommentStore::getStore()->getJoin( 'oi_description' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'oi_user' );
                $ret = [
-                       'tables' => [ 'oldimage' ] + $commentQuery['tables'],
+                       'tables' => [ 'oldimage' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        'fields' => [
                                'oi_name',
                                'oi_archive_name',
@@ -155,13 +169,11 @@ class OldLocalFile extends LocalFile {
                                'oi_media_type',
                                'oi_major_mime',
                                'oi_minor_mime',
-                               'oi_user',
-                               'oi_user_text',
                                'oi_timestamp',
                                'oi_deleted',
                                'oi_sha1',
-                       ] + $commentQuery['fields'],
-                       'joins' => $commentQuery['joins'],
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
+                       'joins' => $commentQuery['joins'] + $actorQuery['joins'],
                ];
 
                if ( in_array( 'omit-nonlazy', $options, true ) ) {
@@ -435,6 +447,7 @@ class OldLocalFile extends LocalFile {
                }
 
                $commentFields = CommentStore::getStore()->insert( $dbw, 'oi_description', $comment );
+               $actorFields = ActorMigration::newMigration()->getInsertValues( $dbw, 'oi_user', $user );
                $dbw->insert( 'oldimage',
                        [
                                'oi_name' => $this->getName(),
@@ -444,14 +457,12 @@ class OldLocalFile extends LocalFile {
                                'oi_height' => intval( $props['height'] ),
                                'oi_bits' => $props['bits'],
                                'oi_timestamp' => $dbw->timestamp( $timestamp ),
-                               'oi_user' => $user->getId(),
-                               'oi_user_text' => $user->getName(),
                                'oi_metadata' => $props['metadata'],
                                'oi_media_type' => $props['media_type'],
                                'oi_major_mime' => $props['major_mime'],
                                'oi_minor_mime' => $props['minor_mime'],
                                'oi_sha1' => $props['sha1'],
-                       ] + $commentFields, __METHOD__
+                       ] + $commentFields + $actorFields, __METHOD__
                );
 
                return true;
index 4325a1a..55a7b2d 100644 (file)
@@ -609,14 +609,7 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
        public function importLogItem() {
                $dbw = wfGetDB( DB_MASTER );
 
-               $user = $this->getUserObj() ?: User::newFromName( $this->getUser() );
-               if ( $user ) {
-                       $userId = intval( $user->getId() );
-                       $userText = $user->getName();
-               } else {
-                       $userId = 0;
-                       $userText = $this->getUser();
-               }
+               $user = $this->getUserObj() ?: User::newFromName( $this->getUser(), false );
 
                # @todo FIXME: This will not record autoblocks
                if ( !$this->getTitle() ) {
@@ -632,7 +625,6 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
                                'log_timestamp' => $dbw->timestamp( $this->timestamp ),
                                'log_namespace' => $this->getTitle()->getNamespace(),
                                'log_title' => $this->getTitle()->getDBkey(),
-                               # 'log_user_text' => $this->user_text,
                                'log_params' => $this->params ],
                        __METHOD__
                );
@@ -647,12 +639,11 @@ class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
                        'log_type' => $this->type,
                        'log_action' => $this->action,
                        'log_timestamp' => $dbw->timestamp( $this->timestamp ),
-                       'log_user' => $userId,
-                       'log_user_text' => $userText,
                        'log_namespace' => $this->getTitle()->getNamespace(),
                        'log_title' => $this->getTitle()->getDBkey(),
                        'log_params' => $this->params
-               ] + CommentStore::getStore()->insert( $dbw, 'log_comment', $this->getComment() );
+               ] + CommentStore::getStore()->insert( $dbw, 'log_comment', $this->getComment() )
+                       + ActorMigration::newMigration()->getInsertValues( $dbw, 'log_user', $user );
                $dbw->insert( 'logging', $data, __METHOD__ );
 
                return true;
index 2083500..500bc5a 100644 (file)
@@ -1228,7 +1228,27 @@ abstract class DatabaseUpdater {
                        );
                        $task = $this->maintenance->runChild( MigrateComments::class, 'migrateComments.php' );
                        $task->execute();
-                       $this->output( "done.\n" );
+                       $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
+               }
+       }
+
+       /**
+        * Migrate actors to the new 'actor' table
+        * @since 1.31
+        */
+       protected function migrateActors() {
+               global $wgActorTableSchemaMigrationStage;
+               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_NEW &&
+                       !$this->updateRowExists( 'MigrateActors' )
+               ) {
+                       $this->output(
+                               "Migrating actors to the 'actor' table, printing progress markers. For large\n" .
+                               "databases, you may want to hit Ctrl-C and do this manually with\n" .
+                               "maintenance/migrateActors.php.\n"
+                       );
+                       $task = $this->maintenance->runChild( 'MigrateActors', 'migrateActors.php' );
+                       $ok = $task->execute();
+                       $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
                }
        }
 
index 1fd1d9b..38a9ede 100644 (file)
@@ -116,6 +116,8 @@ class MssqlUpdater extends DatabaseUpdater {
                        [ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
                        [ 'addTable', 'content_models', 'patch-content_models.sql' ],
                        [ 'migrateArchiveText' ],
+                       [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+                       [ 'migrateActors' ],
                ];
        }
 
index bce5405..350962f 100644 (file)
@@ -336,6 +336,8 @@ class MysqlUpdater extends DatabaseUpdater {
                        [ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
                        [ 'addTable', 'content_models', 'patch-content_models.sql' ],
                        [ 'migrateArchiveText' ],
+                       [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+                       [ 'migrateActors' ],
                ];
        }
 
index 3ee51ea..60ac23c 100644 (file)
@@ -137,6 +137,8 @@ class OracleUpdater extends DatabaseUpdater {
                        [ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
                        [ 'addTable', 'content_models', 'patch-content_models.sql' ],
                        [ 'migrateArchiveText' ],
+                       [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+                       [ 'migrateActors' ],
 
                        // KEEP THIS AT THE BOTTOM!!
                        [ 'doRebuildDuplicateFunction' ],
index 8d12404..2bfadf4 100644 (file)
@@ -491,6 +491,39 @@ class PostgresUpdater extends DatabaseUpdater {
                        [ 'addTable', 'content_models', 'patch-content_models-table.sql' ],
                        [ 'addTable', 'slot_roles', 'patch-slot_roles-table.sql' ],
                        [ 'migrateArchiveText' ],
+                       [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+                       [ 'setDefault', 'revision', 'rev_user', 0 ],
+                       [ 'setDefault', 'revision', 'rev_user_text', '' ],
+                       [ 'setDefault', 'archive', 'ar_user', 0 ],
+                       [ 'changeNullableField', 'archive', 'ar_user', 'NOT NULL', true ],
+                       [ 'setDefault', 'archive', 'ar_user_text', '' ],
+                       [ 'addPgField', 'archive', 'ar_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'addPgIndex', 'archive', 'archive_actor', '( ar_actor )' ],
+                       [ 'setDefault', 'ipblocks', 'ipb_by', 0 ],
+                       [ 'addPgField', 'ipblocks', 'ipb_by_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'image', 'img_user', 0 ],
+                       [ 'changeNullableField', 'image', 'img_user', 'NOT NULL', true ],
+                       [ 'setDefault', 'image', 'img_user_text', '' ],
+                       [ 'addPgField', 'image', 'img_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'oldimage', 'oi_user', 0 ],
+                       [ 'changeNullableField', 'oldimage', 'oi_user', 'NOT NULL', true ],
+                       [ 'setDefault', 'oldimage', 'oi_user_text', '' ],
+                       [ 'addPgField', 'oldimage', 'oi_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'filearchive', 'fa_user', 0 ],
+                       [ 'changeNullableField', 'filearchive', 'fa_user', 'NOT NULL', true ],
+                       [ 'setDefault', 'filearchive', 'fa_user_text', '' ],
+                       [ 'addPgField', 'filearchive', 'fa_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'recentchanges', 'rc_user', 0 ],
+                       [ 'changeNullableField', 'recentchanges', 'rc_user', 'NOT NULL', true ],
+                       [ 'setDefault', 'recentchanges', 'rc_user_text', '' ],
+                       [ 'addPgField', 'recentchanges', 'rc_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'setDefault', 'logging', 'log_user', 0 ],
+                       [ 'changeNullableField', 'logging', 'log_user', 'NOT NULL', true ],
+                       [ 'addPgField', 'logging', 'log_actor', 'INTEGER NOT NULL DEFAULT 0' ],
+                       [ 'addPgIndex', 'logging', 'logging_actor_time_backwards', '( log_timestamp, log_actor )' ],
+                       [ 'addPgIndex', 'logging', 'logging_actor_type_time', '( log_actor, log_type, log_timestamp )' ],
+                       [ 'addPgIndex', 'logging', 'logging_actor_time', '( log_actor, log_timestamp )' ],
+                       [ 'migrateActors' ],
                ];
        }
 
index afb8b22..3a755b6 100644 (file)
@@ -200,6 +200,8 @@ class SqliteUpdater extends DatabaseUpdater {
                        [ 'addTable', 'slots', 'patch-slots.sql' ],
                        [ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
                        [ 'migrateArchiveText' ],
+                       [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+                       [ 'migrateActors' ],
                ];
        }
 
index d97e4f9..77daca7 100644 (file)
@@ -158,11 +158,15 @@ class RecentChangesUpdateJob extends Job {
                                $eTimestamp = min( $sTimestamp + $window, $nowUnix );
 
                                // Get all the users active since the last update
+                               $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
                                $res = $dbw->select(
-                                       [ 'recentchanges' ],
-                                       [ 'rc_user_text', 'lastedittime' => 'MAX(rc_timestamp)' ],
+                                       [ 'recentchanges' ] + $actorQuery['tables'],
                                        [
-                                               'rc_user > 0', // actual accounts
+                                               'rc_user_text' => $actorQuery['fields']['rc_user_text'],
+                                               'lastedittime' => 'MAX(rc_timestamp)'
+                                       ],
+                                       [
+                                               $actorQuery['fields']['rc_user'] . ' > 0', // actual accounts
                                                'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
                                                'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
                                                'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
@@ -172,7 +176,8 @@ class RecentChangesUpdateJob extends Job {
                                        [
                                                'GROUP BY' => [ 'rc_user_text' ],
                                                'ORDER BY' => 'NULL' // avoid filesort
-                                       ]
+                                       ],
+                                       $actorQuery['joins']
                                );
                                $names = [];
                                foreach ( $res as $row ) {
index d59bee3..2922bce 100644 (file)
@@ -962,11 +962,11 @@ interface IDatabase {
         * Example usage:
         * @code
         *     $sql = $db->makeList( [
-        *         'rev_user' => $id,
+        *         'rev_page' => $id,
         *         $db->makeList( [ 'rev_minor' => 1, 'rev_len' < 500 ], $db::LIST_OR ] )
         *     ], $db::LIST_AND );
         * @endcode
-        * This would set $sql to "rev_user = '$id' AND (rev_minor = '1' OR rev_len < '500')"
+        * This would set $sql to "rev_page = '$id' AND (rev_minor = '1' OR rev_len < '500')"
         *
         * @param array $a Containing the data
         * @param int $mode IDatabase class constant:
index 35502c7..395110b 100644 (file)
@@ -171,20 +171,22 @@ class DatabaseLogEntry extends LogEntryBase {
         */
        public static function getSelectQueryData() {
                $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
 
-               $tables = [ 'logging', 'user' ] + $commentQuery['tables'];
+               $tables = array_merge(
+                       [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ]
+               );
                $fields = [
                        'log_id', 'log_type', 'log_action', 'log_timestamp',
-                       'log_user', 'log_user_text',
                        'log_namespace', 'log_title', // unused log_page
                        'log_params', 'log_deleted',
                        'user_id', 'user_name', 'user_editcount',
-               ] + $commentQuery['fields'];
+               ] + $commentQuery['fields'] + $actorQuery['fields'];
 
                $joins = [
                        // IPs don't have an entry in user table
-                       'user' => [ 'LEFT JOIN', 'log_user=user_id' ],
-               ] + $commentQuery['joins'];
+                       'user' => [ 'LEFT JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
+               ] + $commentQuery['joins'] + $actorQuery['joins'];
 
                return [
                        'tables' => $tables,
@@ -293,11 +295,14 @@ class DatabaseLogEntry extends LogEntryBase {
 
        public function getPerformer() {
                if ( !$this->performer ) {
+                       $actorId = isset( $this->row->log_actor ) ? (int)$this->row->log_actor : 0;
                        $userId = (int)$this->row->log_user;
-                       if ( $userId !== 0 ) {
+                       if ( $userId !== 0 || $actorId !== 0 ) {
                                // logged-in users
                                if ( isset( $this->row->user_name ) ) {
                                        $this->performer = User::newFromRow( $this->row );
+                               } elseif ( $actorId !== 0 ) {
+                                       $this->performer = User::newFromActorId( $actorId );
                                } else {
                                        $this->performer = User::newFromId( $userId );
                                }
@@ -356,8 +361,11 @@ class RCDatabaseLogEntry extends DatabaseLogEntry {
 
        public function getPerformer() {
                if ( !$this->performer ) {
+                       $actorId = isset( $this->row->rc_actor ) ? (int)$this->row->rc_actor : 0;
                        $userId = (int)$this->row->rc_user;
-                       if ( $userId !== 0 ) {
+                       if ( $actorId !== 0 ) {
+                               $this->performer = User::newFromActorId( $actorId );
+                       } elseif ( $userId !== 0 ) {
                                $this->performer = User::newFromId( $userId );
                        } else {
                                $userText = $this->row->rc_user_text;
@@ -593,6 +601,8 @@ class ManualLogEntry extends LogEntryBase {
         * @throws MWException
         */
        public function insert( IDatabase $dbw = null ) {
+               global $wgActorTableSchemaMigrationStage;
+
                $dbw = $dbw ?: wfGetDB( DB_MASTER );
 
                if ( $this->timestamp === null ) {
@@ -605,6 +615,31 @@ class ManualLogEntry extends LogEntryBase {
                $params = $this->getParameters();
                $relations = $this->relations;
 
+               // Ensure actor relations are set
+               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH &&
+                       empty( $relations['target_author_actor'] )
+               ) {
+                       $actorIds = [];
+                       if ( !empty( $relations['target_author_id'] ) ) {
+                               foreach ( $relations['target_author_id'] as $id ) {
+                                       $actorIds[] = User::newFromId( $id )->getActorId( $dbw );
+                               }
+                       }
+                       if ( !empty( $relations['target_author_ip'] ) ) {
+                               foreach ( $relations['target_author_ip'] as $ip ) {
+                                       $actorIds[] = User::newFromName( $ip, false )->getActorId( $dbw );
+                               }
+                       }
+                       if ( $actorIds ) {
+                               $relations['target_author_actor'] = $actorIds;
+                               $params['authorActors'] = $actorIds;
+                       }
+               }
+               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_NEW ) {
+                       unset( $relations['target_author_id'], $relations['target_author_ip'] );
+                       unset( $params['authorIds'], $params['authorIPs'] );
+               }
+
                // Additional fields for which there's no space in the database table schema
                $revId = $this->getAssociatedRevId();
                if ( $revId ) {
@@ -616,8 +651,6 @@ class ManualLogEntry extends LogEntryBase {
                        'log_type' => $this->getType(),
                        'log_action' => $this->getSubtype(),
                        'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ),
-                       'log_user' => $this->getPerformer()->getId(),
-                       'log_user_text' => $this->getPerformer()->getName(),
                        'log_namespace' => $this->getTarget()->getNamespace(),
                        'log_title' => $this->getTarget()->getDBkey(),
                        'log_page' => $this->getTarget()->getArticleID(),
@@ -627,6 +660,8 @@ class ManualLogEntry extends LogEntryBase {
                        $data['log_deleted'] = $this->deleted;
                }
                $data += CommentStore::getStore()->insert( $dbw, 'log_comment', $comment );
+               $data += ActorMigration::newMigration()
+                       ->getInsertValues( $dbw, 'log_user', $this->getPerformer() );
 
                $dbw->insert( 'logging', $data, __METHOD__ );
                $this->id = $dbw->insertId();
index c84352e..28c1a87 100644 (file)
@@ -97,14 +97,13 @@ class LogPage {
                        'log_type' => $this->type,
                        'log_action' => $this->action,
                        'log_timestamp' => $dbw->timestamp( $now ),
-                       'log_user' => $this->doer->getId(),
-                       'log_user_text' => $this->doer->getName(),
                        'log_namespace' => $this->target->getNamespace(),
                        'log_title' => $this->target->getDBkey(),
                        'log_page' => $this->target->getArticleID(),
                        'log_params' => $this->params
                ];
                $data += CommentStore::getStore()->insert( $dbw, 'log_comment', $this->comment );
+               $data += ActorMigration::newMigration()->getInsertValues( $dbw, 'log_user', $this->doer );
                $dbw->insert( 'logging', $data, __METHOD__ );
                $newId = $dbw->insertId();
 
index 5404f35..dc9af5a 100644 (file)
@@ -176,13 +176,14 @@ class LogPager extends ReverseChronologicalPager {
                // Normalize username first so that non-existent users used
                // in maintenance scripts work
                $name = $usertitle->getText();
-               /* Fetch userid at first, if known, provides awesome query plan afterwards */
-               $userid = User::idFromName( $name );
-               if ( !$userid ) {
-                       $this->mConds['log_user_text'] = IP::sanitizeIP( $name );
-               } else {
-                       $this->mConds['log_user'] = $userid;
-               }
+
+               // Assume no joins required for log_user
+               // Don't query by user ID here, it might be able to use the
+               // log_user_text_time or log_user_text_type_time index.
+               $this->mConds[] = ActorMigration::newMigration()->getWhere(
+                       wfGetDB( DB_REPLICA ), 'log_user', User::newFromName( $name, false ), false
+               )['conds'];
+
                $this->enforcePerformerRestrictions();
 
                $this->performer = $name;
index 544d23d..d266e1d 100644 (file)
@@ -1036,11 +1036,15 @@ class WikiPage implements Page, IDBAccessObject {
 
                $dbr = wfGetDB( DB_REPLICA );
 
-               $tables = [ 'revision', 'user' ];
+               $actorMigration = ActorMigration::newMigration();
+               $actorQuery = $actorMigration->getJoin( 'rev_user' );
+
+               $tables = array_merge( [ 'revision' ], $actorQuery['tables'], [ 'user' ] );
 
                $fields = [
-                       'user_id' => 'rev_user',
-                       'user_name' => 'rev_user_text',
+                       'user_id' => $actorQuery['fields']['rev_user'],
+                       'user_name' => $actorQuery['fields']['rev_user_text'],
+                       'actor_id' => $actorQuery['fields']['rev_actor'],
                        'user_real_name' => 'MIN(user_real_name)',
                        'timestamp' => 'MAX(rev_timestamp)',
                ];
@@ -1049,22 +1053,20 @@ class WikiPage implements Page, IDBAccessObject {
 
                // The user who made the top revision gets credited as "this page was last edited by
                // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
-               $user = $this->getUser();
-               if ( $user ) {
-                       $conds[] = "rev_user != $user";
-               } else {
-                       $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
-               }
+               $user = $this->getUser()
+                       ? User::newFromId( $this->getUser() )
+                       : User::newFromName( $this->getUserText(), false );
+               $conds[] = 'NOT(' . $actorMigration->getWhere( $dbr, 'rev_user', $user )['conds'] . ')';
 
                // Username hidden?
                $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
 
                $jconds = [
-                       'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
-               ];
+                       'user' => [ 'LEFT JOIN', $actorQuery['fields']['rev_user'] . ' = user_id' ],
+               ] + $actorQuery['joins'];
 
                $options = [
-                       'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
+                       'GROUP BY' => [ $fields['user_id'], $fields['user_name'] ],
                        'ORDER BY' => 'timestamp DESC',
                ];
 
@@ -2783,7 +2785,8 @@ class WikiPage implements Page, IDBAccessObject {
                $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
                $tags = [], $logsubtype = 'delete'
        ) {
-               global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage;
+               global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage,
+                       $wgActorTableSchemaMigrationStage;
 
                wfDebug( __METHOD__ . "\n" );
 
@@ -2848,6 +2851,7 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                $commentStore = CommentStore::getStore();
+               $actorMigration = ActorMigration::newMigration();
 
                $revQuery = Revision::getQueryInfo();
                $bitfield = false;
@@ -2884,11 +2888,10 @@ class WikiPage implements Page, IDBAccessObject {
 
                foreach ( $res as $row ) {
                        $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_user'       => $row->rev_user,
-                               'ar_user_text'  => $row->rev_user_text,
                                'ar_timestamp'  => $row->rev_timestamp,
                                'ar_minor_edit' => $row->rev_minor_edit,
                                'ar_rev_id'     => $row->rev_id,
@@ -2900,7 +2903,8 @@ class WikiPage implements Page, IDBAccessObject {
                                'ar_page_id'    => $id,
                                'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
                                'ar_sha1'       => $row->rev_sha1,
-                       ] + $commentStore->insert( $dbw, 'ar_comment', $comment );
+                       ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
+                               + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
                        if ( $wgContentHandlerUseDB ) {
                                $rowInsert['ar_content_model'] = $row->rev_content_model;
                                $rowInsert['ar_content_format'] = $row->rev_content_format;
@@ -2930,6 +2934,9 @@ class WikiPage implements Page, IDBAccessObject {
                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__ );
+               }
 
                // Also delete records from ip_changes as applicable.
                if ( count( $ipRevIds ) > 0 ) {
@@ -3161,16 +3168,31 @@ class WikiPage implements Page, IDBAccessObject {
 
                // Get the last edit not by this person...
                // Note: these may not be public values
-               $user = intval( $current->getUser( Revision::RAW ) );
-               $user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
-               $s = $dbw->selectRow( 'revision',
+               $userId = intval( $current->getUser( Revision::RAW ) );
+               $userName = $current->getUserText( Revision::RAW );
+               if ( $userId ) {
+                       $user = User::newFromId( $userId );
+                       $user->setName( $userName );
+               } else {
+                       $user = User::newFromName( $current->getUserText( Revision::RAW ), false );
+               }
+
+               $actorWhere = ActorMigration::newMigration()->getWhere( $dbw, 'rev_user', $user );
+
+               $s = $dbw->selectRow(
+                       [ 'revision' ] + $actorWhere['tables'],
                        [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
-                       [ 'rev_page' => $current->getPage(),
-                               "rev_user != {$user} OR rev_user_text != {$user_text}"
-                       ], __METHOD__,
-                       [ 'USE INDEX' => 'page_timestamp',
-                               'ORDER BY' => 'rev_timestamp DESC' ]
-                       );
+                       [
+                               'rev_page' => $current->getPage(),
+                               'NOT(' . $actorWhere['conds'] . ')',
+                       ],
+                       __METHOD__,
+                       [
+                               'USE INDEX' => [ 'revision' => 'page_timestamp' ],
+                               'ORDER BY' => 'rev_timestamp DESC'
+                       ],
+                       $actorWhere['joins']
+               );
                if ( $s === false ) {
                        // No one else ever edited this page
                        return [ [ 'cantrollback' ] ];
@@ -3249,11 +3271,12 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                if ( count( $set ) ) {
+                       $actorWhere = ActorMigration::newMigration()->getWhere( $dbw, 'rc_user', $user, false );
                        $dbw->update( 'recentchanges', $set,
                                [ /* WHERE */
                                        'rc_cur_id' => $current->getPage(),
-                                       'rc_user_text' => $current->getUserText(),
                                        'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
+                                       $actorWhere['conds'], // No tables/joins are needed for rc_user
                                ],
                                __METHOD__
                        );
index ab74dbd..679acc6 100644 (file)
@@ -45,6 +45,10 @@ class RevDelArchiveItem extends RevDelRevisionItem {
                return 'ar_user_text';
        }
 
+       public function getAuthorActorField() {
+               return 'ar_actor';
+       }
+
        public function getId() {
                # Convert DB timestamp to MW timestamp
                return $this->revision->getTimestamp();
index b098422..d36fac9 100644 (file)
@@ -50,6 +50,10 @@ class RevDelArchivedFileItem extends RevDelFileItem {
                return 'fa_user_text';
        }
 
+       public function getAuthorActorField() {
+               return 'fa_actor';
+       }
+
        public function getId() {
                return $this->row->fa_id;
        }
index 9beafc9..0ca84d7 100644 (file)
@@ -49,6 +49,10 @@ class RevDelFileItem extends RevDelItem {
                return 'oi_user_text';
        }
 
+       public function getAuthorActorField() {
+               return 'oi_actor';
+       }
+
        public function getId() {
                $parts = explode( '!', $this->row->oi_archive_name );
 
index 011c7b0..89025bc 100644 (file)
@@ -106,6 +106,8 @@ abstract class RevDelList extends RevisionListBase {
         * @since 1.23 Added 'perItemStatus' param
         */
        public function setVisibility( array $params ) {
+               global $wgActorTableSchemaMigrationStage;
+
                $status = Status::newGood();
 
                $bitPars = $params['value'];
@@ -134,7 +136,7 @@ abstract class RevDelList extends RevisionListBase {
                $missing = array_flip( $this->ids );
                $this->clearFileOps();
                $idsForLog = [];
-               $authorIds = $authorIPs = [];
+               $authorIds = $authorIPs = $authorActors = [];
 
                if ( $perItemStatus ) {
                        $status->itemStatuses = [];
@@ -216,10 +218,15 @@ abstract class RevDelList extends RevisionListBase {
                                $virtualOldBits |= $removedBits;
 
                                $status->successCount++;
-                               if ( $item->getAuthorId() > 0 ) {
-                                       $authorIds[] = $item->getAuthorId();
-                               } elseif ( IP::isIPAddress( $item->getAuthorName() ) ) {
-                                       $authorIPs[] = $item->getAuthorName();
+                               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                                       if ( $item->getAuthorId() > 0 ) {
+                                               $authorIds[] = $item->getAuthorId();
+                                       } elseif ( IP::isIPAddress( $item->getAuthorName() ) ) {
+                                               $authorIPs[] = $item->getAuthorName();
+                                       }
+                               }
+                               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                                       $authorActors[] = $item->getAuthorActor();
                                }
 
                                // Save the old and new bits in $visibilityChangeMap for
@@ -263,6 +270,14 @@ abstract class RevDelList extends RevisionListBase {
                }
 
                // Log it
+               $authorFields = [];
+               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                       $authorFields['authorIds'] = $authorIds;
+                       $authorFields['authorIPs'] = $authorIPs;
+               }
+               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                       $authorFields['authorActors'] = $authorActors;
+               }
                $this->updateLog(
                        $logType,
                        [
@@ -272,10 +287,8 @@ abstract class RevDelList extends RevisionListBase {
                                'oldBits' => $virtualOldBits,
                                'comment' => $comment,
                                'ids' => $idsForLog,
-                               'authorIds' => $authorIds,
-                               'authorIPs' => $authorIPs,
                                'tags' => isset( $params['tags'] ) ? $params['tags'] : [],
-                       ]
+                       ] + $authorFields
                );
 
                // Clear caches after commit
@@ -330,8 +343,9 @@ abstract class RevDelList extends RevisionListBase {
         *     title:           The target title
         *     ids:             The ID list
         *     comment:         The log comment
-        *     authorsIds:      The array of the user IDs of the offenders
-        *     authorsIPs:      The array of the IP/anon user offenders
+        *     authorIds:       The array of the user IDs of the offenders
+        *     authorIPs:       The array of the IP/anon user offenders
+        *     authorActors:    The array of the actor IDs of the offenders
         *     tags:            The array of change tags to apply to the log entry
         * @throws MWException
         */
@@ -350,11 +364,21 @@ abstract class RevDelList extends RevisionListBase {
                $logEntry->setParameters( $logParams );
                $logEntry->setPerformer( $this->getUser() );
                // Allow for easy searching of deletion log items for revision/log items
-               $logEntry->setRelations( [
+               $relations = [
                        $field => $params['ids'],
-                       'target_author_id' => $params['authorIds'],
-                       'target_author_ip' => $params['authorIPs'],
-               ] );
+               ];
+               if ( isset( $params['authorIds'] ) ) {
+                       $relations += [
+                               'target_author_id' => $params['authorIds'],
+                               'target_author_ip' => $params['authorIPs'],
+                       ];
+               }
+               if ( isset( $params['authorActors'] ) ) {
+                       $relations += [
+                               'target_author_actor' => $params['authorActors'],
+                       ];
+               }
+               $logEntry->setRelations( $relations );
                // Apply change tags to the log entry
                $logEntry->setTags( $params['tags'] );
                $logId = $logEntry->insert();
index b8b0c5c..198a28b 100644 (file)
@@ -39,6 +39,10 @@ class RevDelLogItem extends RevDelItem {
                return 'log_user_text';
        }
 
+       public function getAuthorActorField() {
+               return 'log_actor';
+       }
+
        public function canView() {
                return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED, $this->list->getUser() );
        }
index f4e4faf..b26fffd 100644 (file)
@@ -64,26 +64,25 @@ class RevDelLogList extends RevDelList {
                $ids = array_map( 'intval', $this->ids );
 
                $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
 
                return $db->select(
-                       [ 'logging' ] + $commentQuery['tables'],
+                       [ 'logging' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        [
                                'log_id',
                                'log_type',
                                'log_action',
                                'log_timestamp',
-                               'log_user',
-                               'log_user_text',
                                'log_namespace',
                                'log_title',
                                'log_page',
                                'log_params',
                                'log_deleted'
-                       ] + $commentQuery['fields'],
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
                        [ 'log_id' => $ids ],
                        __METHOD__,
                        [ 'ORDER BY' => 'log_id DESC' ],
-                       $commentQuery['joins']
+                       $commentQuery['joins'] + $actorQuery['joins']
                );
        }
 
index a9753b4..cb5ce48 100644 (file)
@@ -47,6 +47,10 @@ class RevDelRevisionItem extends RevDelItem {
                return 'rev_user_text';
        }
 
+       public function getAuthorActorField() {
+               return 'rev_actor';
+       }
+
        public function canView() {
                return $this->revision->userCan( Revision::DELETED_RESTRICTED, $this->list->getUser() );
        }
index 7812fb9..6291e8d 100644 (file)
@@ -42,6 +42,8 @@ class RevisionDeleteUser {
         * @return bool
         */
        private static function setUsernameBitfields( $name, $userId, $op, $dbw ) {
+               global $wgActorTableSchemaMigrationStage;
+
                if ( !$userId || ( $op !== '|' && $op !== '&' ) ) {
                        return false; // sanity check
                }
@@ -65,43 +67,120 @@ class RevisionDeleteUser {
                $userTitle = Title::makeTitleSafe( NS_USER, $name );
                $userDbKey = $userTitle->getDBkey();
 
-               # Hide name from live edits
-               $dbw->update(
-                       'revision',
-                       [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ],
-                       [ 'rev_user' => $userId ],
-                       __METHOD__ );
+               if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                       # Hide name from live edits
+                       $dbw->update(
+                               'revision',
+                               [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ],
+                               [ 'rev_user' => $userId ],
+                               __METHOD__ );
 
-               # Hide name from deleted edits
-               $dbw->update(
-                       'archive',
-                       [ self::buildSetBitDeletedField( 'ar_deleted', $op, $delUser, $dbw ) ],
-                       [ 'ar_user_text' => $name ],
-                       __METHOD__
-               );
+                       # Hide name from deleted edits
+                       $dbw->update(
+                               'archive',
+                               [ self::buildSetBitDeletedField( 'ar_deleted', $op, $delUser, $dbw ) ],
+                               [ 'ar_user_text' => $name ],
+                               __METHOD__
+                       );
 
-               # Hide name from logs
-               $dbw->update(
-                       'logging',
-                       [ self::buildSetBitDeletedField( 'log_deleted', $op, $delUser, $dbw ) ],
-                       [ 'log_user' => $userId, 'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
-                       __METHOD__
-               );
+                       # Hide name from logs
+                       $dbw->update(
+                               'logging',
+                               [ self::buildSetBitDeletedField( 'log_deleted', $op, $delUser, $dbw ) ],
+                               [ 'log_user' => $userId, 'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
+                               __METHOD__
+                       );
+
+                       # Hide name from RC
+                       $dbw->update(
+                               'recentchanges',
+                               [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delUser, $dbw ) ],
+                               [ 'rc_user_text' => $name ],
+                               __METHOD__
+                       );
+
+                       # Hide name from live images
+                       $dbw->update(
+                               'oldimage',
+                               [ self::buildSetBitDeletedField( 'oi_deleted', $op, $delUser, $dbw ) ],
+                               [ 'oi_user_text' => $name ],
+                               __METHOD__
+                       );
+
+                       # Hide name from deleted images
+                       $dbw->update(
+                               'filearchive',
+                               [ self::buildSetBitDeletedField( 'fa_deleted', $op, $delUser, $dbw ) ],
+                               [ 'fa_user_text' => $name ],
+                               __METHOD__
+                       );
+               }
+
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       $actorId = $dbw->selectField( 'actor', 'actor_id', [ 'actor_name' => $name ], __METHOD__ );
+                       if ( $actorId ) {
+                               # Hide name from live edits
+                               $subquery = $dbw->selectSQLText(
+                                       'revision_actor_temp', 'revactor_rev', [ 'revactor_actor' => $actorId ], __METHOD__
+                               );
+                               $dbw->update(
+                                       'revision',
+                                       [ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ],
+                                       [ "rev_id IN ($subquery)" ],
+                                       __METHOD__ );
+
+                               # Hide name from deleted edits
+                               $dbw->update(
+                                       'archive',
+                                       [ self::buildSetBitDeletedField( 'ar_deleted', $op, $delUser, $dbw ) ],
+                                       [ 'ar_actor' => $actorId ],
+                                       __METHOD__
+                               );
+
+                               # Hide name from logs
+                               $dbw->update(
+                                       'logging',
+                                       [ self::buildSetBitDeletedField( 'log_deleted', $op, $delUser, $dbw ) ],
+                                       [ 'log_actor' => $actorId, 'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
+                                       __METHOD__
+                               );
+
+                               # Hide name from RC
+                               $dbw->update(
+                                       'recentchanges',
+                                       [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delUser, $dbw ) ],
+                                       [ 'rc_actor' => $actorId ],
+                                       __METHOD__
+                               );
+
+                               # Hide name from live images
+                               $dbw->update(
+                                       'oldimage',
+                                       [ self::buildSetBitDeletedField( 'oi_deleted', $op, $delUser, $dbw ) ],
+                                       [ 'oi_actor' => $actorId ],
+                                       __METHOD__
+                               );
+
+                               # Hide name from deleted images
+                               $dbw->update(
+                                       'filearchive',
+                                       [ self::buildSetBitDeletedField( 'fa_deleted', $op, $delUser, $dbw ) ],
+                                       [ 'fa_actor' => $actorId ],
+                                       __METHOD__
+                               );
+                       }
+               }
+
+               # Hide log entries pointing to the user page
                $dbw->update(
                        'logging',
                        [ self::buildSetBitDeletedField( 'log_deleted', $op, $delAction, $dbw ) ],
                        [ 'log_namespace' => NS_USER, 'log_title' => $userDbKey,
-                               'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
+                       'log_type != ' . $dbw->addQuotes( 'suppress' ) ],
                        __METHOD__
                );
 
-               # Hide name from RC
-               $dbw->update(
-                       'recentchanges',
-                       [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delUser, $dbw ) ],
-                       [ 'rc_user_text' => $name ],
-                       __METHOD__
-               );
+               # Hide RC entries pointing to the user page
                $dbw->update(
                        'recentchanges',
                        [ self::buildSetBitDeletedField( 'rc_deleted', $op, $delAction, $dbw ) ],
@@ -109,21 +188,6 @@ class RevisionDeleteUser {
                        __METHOD__
                );
 
-               # Hide name from live images
-               $dbw->update(
-                       'oldimage',
-                       [ self::buildSetBitDeletedField( 'oi_deleted', $op, $delUser, $dbw ) ],
-                       [ 'oi_user_text' => $name ],
-                       __METHOD__
-               );
-
-               # Hide name from deleted images
-               $dbw->update(
-                       'filearchive',
-                       [ self::buildSetBitDeletedField( 'fa_deleted', $op, $delUser, $dbw ) ],
-                       [ 'fa_user_text' => $name ],
-                       __METHOD__
-               );
                # Done!
                return true;
        }
index 4204443..b8d7063 100644 (file)
@@ -121,7 +121,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                                'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
                                                        &$query_options, &$join_conds
                                                ) {
-                                                       $conds[] = 'rc_user = 0';
+                                                       $actorMigration = ActorMigration::newMigration();
+                                                       $actorQuery = $actorMigration->getJoin( 'rc_user' );
+                                                       $tables += $actorQuery['tables'];
+                                                       $join_conds += $actorQuery['joins'];
+                                                       $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
                                                },
                                                'isReplacedInStructuredUi' => true,
 
@@ -135,7 +139,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                                'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
                                                        &$query_options, &$join_conds
                                                ) {
-                                                       $conds[] = 'rc_user != 0';
+                                                       $actorMigration = ActorMigration::newMigration();
+                                                       $actorQuery = $actorMigration->getJoin( 'rc_user' );
+                                                       $tables += $actorQuery['tables'];
+                                                       $join_conds += $actorQuery['joins'];
+                                                       $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
                                                },
                                                'isReplacedInStructuredUi' => true,
                                        ]
@@ -220,8 +228,10 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                                'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
                                                        &$query_options, &$join_conds
                                                ) {
-                                                       $user = $ctx->getUser();
-                                                       $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
+                                                       $actorQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $ctx->getUser() );
+                                                       $tables += $actorQuery['tables'];
+                                                       $join_conds += $actorQuery['joins'];
+                                                       $conds[] = 'NOT(' . $actorQuery['conds'] . ')';
                                                },
                                                'cssClassSuffix' => 'self',
                                                'isRowApplicableCallable' => function ( $ctx, $rc ) {
@@ -236,8 +246,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                                'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
                                                        &$query_options, &$join_conds
                                                ) {
-                                                       $user = $ctx->getUser();
-                                                       $conds[] = 'rc_user_text = ' . $dbr->addQuotes( $user->getName() );
+                                                       $actorQuery = ActorMigration::newMigration()
+                                                               ->getWhere( $dbr, 'rc_user', $ctx->getUser(), false );
+                                                       $tables += $actorQuery['tables'];
+                                                       $join_conds += $actorQuery['joins'];
+                                                       $conds[] = $actorQuery['conds'];
                                                },
                                                'cssClassSuffix' => 'others',
                                                'isRowApplicableCallable' => function ( $ctx, $rc ) {
@@ -1702,22 +1715,27 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        return;
                }
 
+               $actorMigration = ActorMigration::newMigration();
+               $actorQuery = $actorMigration->getJoin( 'rc_user' );
+               $tables += $actorQuery['tables'];
+               $join_conds += $actorQuery['joins'];
+
                // 'registered' but not 'unregistered', experience levels, if any, are included in 'registered'
                if (
                        in_array( 'registered', $selectedExpLevels ) &&
                        !in_array( 'unregistered', $selectedExpLevels )
                ) {
-                       $conds[] = 'rc_user != 0';
+                       $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
                        return;
                }
 
                if ( $selectedExpLevels === [ 'unregistered' ] ) {
-                       $conds[] = 'rc_user = 0';
+                       $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
                        return;
                }
 
                $tables[] = 'user';
-               $join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ];
+               $join_conds['user'] = [ 'LEFT JOIN', $actorQuery['fields']['rc_user'] . ' = user_id' ];
 
                if ( $now === 0 ) {
                        $now = time();
@@ -1747,7 +1765,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
 
                if ( in_array( 'unregistered', $selectedExpLevels ) ) {
                        $selectedExpLevels = array_diff( $selectedExpLevels, [ 'unregistered' ] );
-                       $conditions[] = 'rc_user = 0';
+                       $conditions[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
                }
 
                if ( $selectedExpLevels === [ 'newcomer' ] ) {
@@ -1769,7 +1787,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                } elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) {
                        $conditions[] = $aboveNewcomer;
                } elseif ( $selectedExpLevels === [ 'experienced', 'learner', 'newcomer' ] ) {
-                       $conditions[] = 'rc_user != 0';
+                       $conditions[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
                }
 
                if ( count( $conditions ) > 1 ) {
index 8021bc2..7694a61 100644 (file)
@@ -85,15 +85,17 @@ class FileDuplicateSearchPage extends QueryPage {
        }
 
        public function getQueryInfo() {
+               $imgQuery = LocalFile::getQueryInfo();
                return [
-                       'tables' => [ 'image' ],
+                       'tables' => $imgQuery['tables'],
                        'fields' => [
                                'title' => 'img_name',
                                'value' => 'img_sha1',
-                               'img_user_text',
+                               'img_user_text' => $imgQuery['fields']['img_user_text'],
                                'img_timestamp'
                        ],
-                       'conds' => [ 'img_sha1' => $this->hash ]
+                       'conds' => [ 'img_sha1' => $this->hash ],
+                       'join_conds' => $imgQuery['joins'],
                ];
        }
 
index cc43580..6a11bf4 100644 (file)
@@ -32,6 +32,8 @@ class SpecialLog extends SpecialPage {
        }
 
        public function execute( $par ) {
+               global $wgActorTableSchemaMigrationStage;
+
                $this->setHeaders();
                $this->outputHeader();
                $this->getOutput()->addModules( 'mediawiki.userSuggest' );
@@ -78,12 +80,33 @@ class SpecialLog extends SpecialPage {
                # Handle type-specific inputs
                $qc = [];
                if ( $opts->getValue( 'type' ) == 'suppress' ) {
-                       $offender = User::newFromName( $opts->getValue( 'offender' ), false );
+                       $offenderName = $opts->getValue( 'offender' );
+                       $offender = empty( $offenderName ) ? null : User::newFromName( $offenderName, false );
                        if ( $offender ) {
-                               if ( $offender->getId() > 0 ) {
-                                       $qc = [ 'ls_field' => 'target_author_id', 'ls_value' => $offender->getId() ];
-                               } elseif ( !empty( $opts->getValue( 'offender' ) ) ) {
-                                       $qc = [ 'ls_field' => 'target_author_ip', 'ls_value' => $offender->getName() ];
+                               if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                                       $qc = [ 'ls_field' => 'target_author_actor', 'ls_value' => $offender->getActorId() ];
+                               } 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 ),
+                                               ];
+                                       }
                                }
                        }
                } else {
index 3290abd..a54d72d 100644 (file)
@@ -56,8 +56,9 @@ class MIMEsearchPage extends QueryPage {
                        // Allow wildcard searching
                        $minorType['img_minor_mime'] = $this->minor;
                }
+               $imgQuery = LocalFile::getQueryInfo();
                $qi = [
-                       'tables' => [ 'image' ],
+                       'tables' => $imgQuery['tables'],
                        'fields' => [
                                'namespace' => NS_FILE,
                                'title' => 'img_name',
@@ -67,7 +68,7 @@ class MIMEsearchPage extends QueryPage {
                                'img_size',
                                'img_width',
                                'img_height',
-                               'img_user_text',
+                               'img_user_text' => $imgQuery['fields']['img_user_text'],
                                'img_timestamp'
                        ],
                        'conds' => [
@@ -89,6 +90,7 @@ class MIMEsearchPage extends QueryPage {
                                        MEDIATYPE_3D,
                                ],
                        ] + $minorType,
+                       'join_conds' => $imgQuery['joins'],
                ];
 
                return $qi;
index ea0f0ed..46d5276 100644 (file)
@@ -299,6 +299,7 @@ class SpecialNewpages extends IncludableSpecialPage {
                        'deleted' => $result->rc_deleted,
                        'user_text' => $result->rc_user_text,
                        'user' => $result->rc_user,
+                       'actor' => $result->rc_actor,
                ], 0, $title );
        }
 
index 3273046..36e7779 100644 (file)
@@ -177,11 +177,13 @@ class SpecialRedirect extends FormSpecialPage {
                        return null;
                }
 
+               $logQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
+
                $logparams = [
-                       'log_id',
-                       'log_timestamp',
-                       'log_type',
-                       'log_user_text',
+                       'log_id' => 'log_id',
+                       'log_timestamp' => 'log_timestamp',
+                       'log_type' => 'log_type',
+                       'log_user_text' => $logQuery['fields']['log_user_text'],
                ];
 
                $dbr = wfGetDB( DB_REPLICA );
@@ -197,9 +199,12 @@ class SpecialRedirect extends FormSpecialPage {
                // Returns all fields mentioned in $logparams of the logs
                // with the same timestamp as the one returned by the statement above
                $logsSameTimestamps = $dbr->select(
-                       'logging',
+                       [ 'logging' ] + $logQuery['tables'],
                        $logparams,
-                       [ "log_timestamp = ($inner)" ]
+                       [ "log_timestamp = ($inner)" ],
+                       __METHOD__,
+                       [],
+                       $logQuery['joins']
                );
                if ( $logsSameTimestamps->numRows() === 0 ) {
                        return null;
@@ -217,10 +222,10 @@ class SpecialRedirect extends FormSpecialPage {
 
                // Stores all the rows with the same values in each column
                // as $rowMain
-               foreach ( $logparams as $cond ) {
+               foreach ( $logparams as $key => $dummy ) {
                        $matchedRows = [];
                        foreach ( $logsSameTimestamps as $row ) {
-                               if ( $row->$cond === $rowMain->$cond ) {
+                               if ( $row->$key === $rowMain->$key ) {
                                        $matchedRows[] = $row;
                                }
                        }
@@ -238,7 +243,7 @@ class SpecialRedirect extends FormSpecialPage {
                        'log_user_text' => 'user'
                ];
 
-               foreach ( $logparams as $logKey ) {
+               foreach ( $logparams as $logKey => $dummy ) {
                        $query[$keys[$logKey]] = $matchedRows[0]->$logKey;
                }
                $query['offset'] = $query['offset'] + 1;
index 64af71a..26ed499 100644 (file)
@@ -79,14 +79,17 @@ class ActiveUsersPager extends UsersPager {
        function getQueryInfo() {
                $dbr = $this->getDatabase();
 
+               $rcQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+
                $activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400;
                $timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds );
-               $tables = [ 'querycachetwo', 'user', 'recentchanges' ];
+               $tables = [ 'querycachetwo', 'user', 'recentchanges' ] + $rcQuery['tables'];
+               $jconds = $rcQuery['joins'];
                $conds = [
                        'qcc_type' => 'activeusers',
                        'qcc_namespace' => NS_USER,
                        'user_name = qcc_title',
-                       'rc_user_text = qcc_title',
+                       $rcQuery['fields']['rc_user_text'] . ' = qcc_title',
                        'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata.
                        'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ), // Don't count categorization changes.
                        'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ),
@@ -127,7 +130,8 @@ class ActiveUsersPager extends UsersPager {
                                'recentedits' => 'COUNT(*)'
                        ],
                        'options' => [ 'GROUP BY' => [ 'qcc_title' ] ],
-                       'conds' => $conds
+                       'conds' => $conds,
+                       'join_conds' => $jconds,
                ];
        }
 
index fe7cac0..cac0736 100644 (file)
@@ -210,15 +210,16 @@ class BlockListPager extends TablePager {
 
        function getQueryInfo() {
                $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
 
                $info = [
-                       'tables' => [ 'ipblocks', 'user' ] + $commentQuery['tables'],
+                       'tables' => array_merge(
+                               [ 'ipblocks' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ]
+                       ),
                        'fields' => [
                                'ipb_id',
                                'ipb_address',
                                'ipb_user',
-                               'ipb_by',
-                               'ipb_by_text',
                                'by_user_name' => 'user_name',
                                'ipb_timestamp',
                                'ipb_auto',
@@ -231,9 +232,11 @@ class BlockListPager extends TablePager {
                                'ipb_deleted',
                                'ipb_block_email',
                                'ipb_allow_usertalk',
-                       ] + $commentQuery['fields'],
+                       ] + $commentQuery['fields'], $actorQuery['fields'],
                        'conds' => $this->conds,
-                       'join_conds' => [ 'user' => [ 'LEFT JOIN', 'user_id = ipb_by' ] ] + $commentQuery['joins']
+                       'join_conds' => [
+                               'user' => [ 'LEFT JOIN', 'user_id = ' . $actorQuery['fields']['ipb_by'] ]
+                       ] + $commentQuery['joins'] + $actorQuery['joins']
                ];
 
                # Filter out any expired blocks
index e29467d..6fdf05f 100644 (file)
@@ -186,7 +186,7 @@ class ContribsPager extends RangeChronologicalPager {
 
                if ( $this->contribs == 'newbie' ) {
                        $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
-                       $queryInfo['conds'][] = 'rev_user >' . (int)( $max - $max / 100 );
+                       $queryInfo['conds'][] = $revQuery['fields']['rev_user'] . ' >' . (int)( $max - $max / 100 );
                        # ignore local groups with the bot right
                        # @todo FIXME: Global groups may have 'bot' rights
                        $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
@@ -195,7 +195,7 @@ class ContribsPager extends RangeChronologicalPager {
                                $queryInfo['conds'][] = 'ug_group IS NULL';
                                $queryInfo['join_conds']['user_groups'] = [
                                        'LEFT JOIN', [
-                                               'ug_user = rev_user',
+                                               'ug_user = ' . $revQuery['fields']['rev_user'],
                                                'ug_group' => $groupsWithBotPermission,
                                                'ug_expiry IS NULL OR ug_expiry >= ' .
                                                        $this->mDb->addQuotes( $this->mDb->timestamp() )
@@ -208,23 +208,18 @@ class ContribsPager extends RangeChronologicalPager {
                        $queryInfo['conds'][] = 'rev_timestamp > ' .
                                $this->mDb->addQuotes( $this->mDb->timestamp( wfTimestamp() - 30 * 24 * 60 * 60 ) );
                } else {
-                       $uid = User::idFromName( $this->target );
-                       if ( $uid ) {
-                               $queryInfo['conds']['rev_user'] = $uid;
-                               $queryInfo['options']['USE INDEX']['revision'] = 'user_timestamp';
+                       $user = User::newFromName( $this->target, false );
+                       $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null;
+                       if ( $ipRangeConds ) {
+                               $queryInfo['tables'][] = 'ip_changes';
+                               $queryInfo['join_conds']['ip_changes'] = [
+                                       'LEFT JOIN', [ 'ipc_rev_id = rev_id' ]
+                               ];
+                               $queryInfo['conds'][] = $ipRangeConds;
                        } else {
-                               $ipRangeConds = $this->getIpRangeConds( $this->mDb, $this->target );
-
-                               if ( $ipRangeConds ) {
-                                       $queryInfo['tables'][] = 'ip_changes';
-                                       $queryInfo['join_conds']['ip_changes'] = [
-                                               'LEFT JOIN', [ 'ipc_rev_id = rev_id' ]
-                                       ];
-                                       $queryInfo['conds'][] = $ipRangeConds;
-                               } else {
-                                       $queryInfo['conds']['rev_user_text'] = $this->target;
-                                       $queryInfo['options']['USE INDEX']['revision'] = 'usertext_timestamp';
-                               }
+                               // tables and joins are already handled by Revision::getQueryInfo()
+                               $queryInfo['conds'][] = ActorMigration::newMigration()
+                                       ->getWhere( $this->mDb, 'rev_user', $user )['conds'];
                        }
                }
 
index 1c31724..d642e66 100644 (file)
@@ -58,7 +58,12 @@ class DeletedContribsPager extends IndexPager {
        }
 
        function getQueryInfo() {
-               list( $index, $userCond ) = $this->getUserCond();
+               $userCond = [
+                       // ->getJoin() below takes care of any joins needed
+                       ActorMigration::newMigration()->getWhere(
+                               wfGetDB( DB_REPLICA ), 'ar_user', User::newFromName( $this->target, false ), false
+                       )['conds']
+               ];
                $conds = array_merge( $userCond, $this->getNamespaceCond() );
                $user = $this->getUser();
                // Paranoia: avoid brute force searches (T19792)
@@ -70,16 +75,17 @@ class DeletedContribsPager extends IndexPager {
                }
 
                $commentQuery = CommentStore::getStore()->getJoin( 'ar_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'ar_user' );
 
                return [
-                       'tables' => [ 'archive' ] + $commentQuery['tables'],
+                       'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        'fields' => [
                                'ar_rev_id', 'ar_namespace', 'ar_title', 'ar_timestamp',
-                               'ar_minor_edit', 'ar_user', 'ar_user_text', 'ar_deleted'
-                       ] + $commentQuery['fields'],
+                               'ar_minor_edit', 'ar_deleted'
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
                        'conds' => $conds,
-                       'options' => [ 'USE INDEX' => [ 'archive' => $index ] ],
-                       'join_conds' => $commentQuery['joins'],
+                       'options' => [],
+                       'join_conds' => $commentQuery['joins'] + $actorQuery['joins'],
                ];
        }
 
@@ -128,15 +134,6 @@ class DeletedContribsPager extends IndexPager {
                return new FakeResultWrapper( $result );
        }
 
-       function getUserCond() {
-               $condition = [];
-
-               $condition['ar_user_text'] = $this->target;
-               $index = 'ar_usertext_timestamp';
-
-               return [ $index, $condition ];
-       }
-
        function getIndexField() {
                return 'ar_timestamp';
        }
@@ -259,6 +256,7 @@ class DeletedContribsPager extends IndexPager {
                        'comment' => CommentStore::getStore()->getComment( 'ar_comment', $row )->text,
                        'user' => $row->ar_user,
                        'user_text' => $row->ar_user_text,
+                       'actor' => isset( $row->ar_actor ) ? $row->ar_actor : null,
                        'timestamp' => $row->ar_timestamp,
                        'minor_edit' => $row->ar_minor_edit,
                        'deleted' => $row->ar_deleted,
index ae19d59..75c2f77 100644 (file)
@@ -193,9 +193,9 @@ class ImageListPager extends TablePager {
                }
                $sortable = [ 'img_timestamp', 'img_name', 'img_size' ];
                /* For reference, the indicies we can use for sorting are:
-                * On the image table: img_user_timestamp, img_usertext_timestamp,
+                * On the image table: img_user_timestamp/img_usertext_timestamp/img_actor_timestamp,
                 * img_size, img_timestamp
-                * On oldimage: oi_usertext_timestamp, oi_name_timestamp
+                * On oldimage: oi_usertext_timestamp/oi_actor_timestamp, oi_name_timestamp
                 *
                 * In particular that means we cannot sort by timestamp when not filtering
                 * by user and including old images in the results. Which is sad.
@@ -246,6 +246,7 @@ class ImageListPager extends TablePager {
                $tables = [ $table ];
                $fields = $this->getFieldNames();
                unset( $fields['img_description'] );
+               unset( $fields['img_user_text'] );
                $fields = array_keys( $fields );
 
                if ( $table === 'oldimage' ) {
@@ -261,7 +262,6 @@ class ImageListPager extends TablePager {
                                $fields[array_search( 'top', $fields )] = "'yes' AS top";
                        }
                }
-               $fields[] = $prefix . '_user AS img_user';
                $fields[array_search( 'thumb', $fields )] = $prefix . '_name AS thumb';
 
                $options = $join_conds = [];
@@ -273,6 +273,14 @@ class ImageListPager extends TablePager {
                $join_conds += $commentQuery['joins'];
                $fields['description_field'] = "'{$prefix}_description'";
 
+               # User fields
+               $actorQuery = ActorMigration::newMigration()->getJoin( $prefix . '_user' );
+               $tables += $actorQuery['tables'];
+               $join_conds += $actorQuery['joins'];
+               $fields['img_user'] = $actorQuery['fields'][$prefix . '_user'];
+               $fields['img_user_text'] = $actorQuery['fields'][$prefix . '_user_text'];
+               $fields['img_actor'] = $actorQuery['fields'][$prefix . '_actor'];
+
                # Depends on $wgMiserMode
                # Will also not happen if mShowAll is true.
                if ( isset( $this->mFieldNames['count'] ) ) {
@@ -287,7 +295,7 @@ class ImageListPager extends TablePager {
                        unset( $field );
 
                        $columnlist = preg_grep( '/^img/', array_keys( $this->getFieldNames() ) );
-                       $options = [ 'GROUP BY' => array_merge( [ 'img_user' ], $columnlist ) ];
+                       $options = [ 'GROUP BY' => array_merge( [ $fields['img_user'] ], $columnlist ) ];
                        $join_conds['oldimage'] = [ 'LEFT JOIN', 'oi_name = img_name' ];
                }
 
index 001c296..4815189 100644 (file)
@@ -59,26 +59,24 @@ class NewFilesPager extends RangeChronologicalPager {
 
        function getQueryInfo() {
                $opts = $this->opts;
-               $conds = $jconds = [];
-               $tables = [ 'image' ];
-               $fields = [ 'img_name', 'img_user', 'img_timestamp' ];
+               $conds = [];
+               $imgQuery = LocalFile::getQueryInfo();
+               $tables = $imgQuery['tables'];
+               $fields = [ 'img_name', 'img_timestamp' ] + $imgQuery['fields'];
                $options = [];
+               $jconds = $imgQuery['joins'];
 
                $user = $opts->getValue( 'user' );
                if ( $user !== '' ) {
-                       $userId = User::idFromName( $user );
-                       if ( $userId ) {
-                               $conds['img_user'] = $userId;
-                       } else {
-                               $conds['img_user_text'] = $user;
-                       }
+                       $conds[] = ActorMigration::newMigration()
+                               ->getWhere( wfGetDB( DB_REPLICA ), 'img_user', User::newFromName( $user, false ) )['conds'];
                }
 
                if ( $opts->getValue( 'newbies' ) ) {
                        // newbie = most recent 1% of users
                        $dbr = wfGetDB( DB_REPLICA );
                        $max = $dbr->selectField( 'user', 'max(user_id)', false, __METHOD__ );
-                       $conds[] = 'img_user >' . (int)( $max - $max / 100 );
+                       $conds[] = $imgQuery['fields']['img_user'] . ' >' . (int)( $max - $max / 100 );
 
                        // there's no point in looking for new user activity in a far past;
                        // beyond a certain point, we'd just end up scanning the rest of the
@@ -99,7 +97,7 @@ class NewFilesPager extends RangeChronologicalPager {
                                        'LEFT JOIN',
                                        [
                                                'ug_group' => $groupsWithBotPermission,
-                                               'ug_user = img_user',
+                                               'ug_user = ' . $imgQuery['fields']['img_user'],
                                                'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
                                        ]
                                ];
@@ -107,16 +105,27 @@ class NewFilesPager extends RangeChronologicalPager {
                }
 
                if ( $opts->getValue( 'hidepatrolled' ) ) {
+                       global $wgActorTableSchemaMigrationStage;
+
                        $tables[] = 'recentchanges';
                        $conds['rc_type'] = RC_LOG;
                        $conds['rc_log_type'] = 'upload';
                        $conds['rc_patrolled'] = 0;
                        $conds['rc_namespace'] = NS_FILE;
+
+                       if ( $wgActorTableSchemaMigrationStage === MIGRATION_NEW ) {
+                               $jcond = 'rc_actor = ' . $imgQuery['fields']['img_actor'];
+                       } else {
+                               $rcQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+                               $tables += $rcQuery['tables'];
+                               $joins += $rcQuery['joins'];
+                               $jcond = $rcQuery['fields']['rc_user'] . ' = ' . $imgQuery['fields']['img_user'];
+                       }
                        $jconds['recentchanges'] = [
                                'INNER JOIN',
                                [
                                        'rc_title = img_name',
-                                       'rc_user = img_user',
+                                       $jcond,
                                        'rc_timestamp = img_timestamp'
                                ]
                        ];
index 61b648d..9746334 100644 (file)
@@ -39,6 +39,8 @@ class NewPagesPager extends ReverseChronologicalPager {
        }
 
        function getQueryInfo() {
+               $rcQuery = RecentChange::getQueryInfo();
+
                $conds = [];
                $conds['rc_new'] = 1;
 
@@ -68,13 +70,14 @@ class NewPagesPager extends ReverseChronologicalPager {
                }
 
                if ( $user ) {
-                       $conds['rc_user_text'] = $user->getText();
-                       $rcIndexes = 'rc_user_text';
+                       $conds[] = ActorMigration::newMigration()->getWhere(
+                               $this->mDb, 'rc_user', User::newFromName( $user->getText(), false ), false
+                       )['conds'];
                } elseif ( User::groupHasPermission( '*', 'createpage' ) &&
                        $this->opts->getValue( 'hideliu' )
                ) {
                        # If anons cannot make new pages, don't "exclude logged in users"!
-                       $conds['rc_user'] = 0;
+                       $conds[] = ActorMigration::newMigration()->isAnon( $rcQuery['fields']['rc_user'] );
                }
 
                # If this user cannot see patrolled edits or they are off, don't do dumb queries!
@@ -90,17 +93,12 @@ class NewPagesPager extends ReverseChronologicalPager {
                        $conds['page_is_redirect'] = 0;
                }
 
-               $commentQuery = CommentStore::getStore()->getJoin( 'rc_comment' );
-
                // Allow changes to the New Pages query
-               $tables = [ 'recentchanges', 'page' ] + $commentQuery['tables'];
+               $tables = array_merge( $rcQuery['tables'], [ 'page' ] );
                $fields = [
-                       'rc_namespace', 'rc_title', 'rc_cur_id', 'rc_user', 'rc_user_text',
-                       'rc_timestamp', 'rc_patrolled', 'rc_id', 'rc_deleted',
-                       'length' => 'page_len', 'rev_id' => 'page_latest', 'rc_this_oldid',
-                       'page_namespace', 'page_title'
-               ] + $commentQuery['fields'];
-               $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ] + $commentQuery['joins'];
+                       'length' => 'page_len', 'rev_id' => 'page_latest', 'page_namespace', 'page_title'
+               ] + $rcQuery['fields'];
+               $join_conds = [ 'page' => [ 'INNER JOIN', 'page_id=rc_cur_id' ] ] + $rcQuery['joins'];
 
                // Avoid PHP 7.1 warning from passing $this by reference
                $pager = $this;
index c4ea5f8..3b69698 100644 (file)
@@ -284,9 +284,13 @@ class ProtectedPagesPager extends TablePager {
                }
 
                $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
 
                return [
-                       'tables' => [ 'page', 'page_restrictions', 'log_search', 'logging' ] + $commentQuery['tables'],
+                       'tables' => [
+                               'page', 'page_restrictions', 'log_search',
+                               'logparen' => [ 'logging' ] + $commentQuery['tables'] + $actorQuery['tables'],
+                       ],
                        'fields' => [
                                'pr_id',
                                'page_namespace',
@@ -297,9 +301,8 @@ class ProtectedPagesPager extends TablePager {
                                'pr_expiry',
                                'pr_cascade',
                                'log_timestamp',
-                               'log_user',
                                'log_deleted',
-                       ] + $commentQuery['fields'],
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
                        'conds' => $conds,
                        'join_conds' => [
                                'log_search' => [
@@ -307,12 +310,12 @@ class ProtectedPagesPager extends TablePager {
                                                'ls_field' => 'pr_id', 'ls_value = ' . $this->mDb->buildStringCast( 'pr_id' )
                                        ]
                                ],
-                               'logging' => [
+                               'logparen' => [
                                        'LEFT JOIN', [
                                                'ls_log_id = log_id'
                                        ]
                                ]
-                       ] + $commentQuery['joins']
+                       ] + $commentQuery['joins'] + $actorQuery['joins']
                ];
        }
 
index eeade49..869adca 100644 (file)
@@ -31,6 +31,7 @@ use Wikimedia\IPSet;
 use Wikimedia\ScopedCallback;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\DBExpectedError;
+use Wikimedia\Rdbms\IDatabase;
 
 /**
  * String Some punctuation to prevent editing from broken text-mangling proxies.
@@ -70,7 +71,7 @@ class User implements IDBAccessObject, UserIdentity {
        /**
         * @const int Serialized record version.
         */
-       const VERSION = 11;
+       const VERSION = 12;
 
        /**
         * Exclude user options that are set to their default value.
@@ -111,6 +112,8 @@ class User implements IDBAccessObject, UserIdentity {
                'mGroupMemberships',
                // user_properties table
                'mOptionOverrides',
+               // actor table
+               'mActorId',
        ];
 
        /**
@@ -207,6 +210,8 @@ class User implements IDBAccessObject, UserIdentity {
        public $mId;
        /** @var string */
        public $mName;
+       /** @var int|null */
+       protected $mActorId;
        /** @var string */
        public $mRealName;
 
@@ -251,6 +256,7 @@ class User implements IDBAccessObject, UserIdentity {
         *  - 'defaults'   anonymous user initialised from class defaults
         *  - 'name'       initialise from mName
         *  - 'id'         initialise from mId
+        *  - 'actor'      initialise from mActorId
         *  - 'session'    log in from session if possible
         *
         * Use the User::newFrom*() family of functions to set this.
@@ -309,6 +315,7 @@ class User implements IDBAccessObject, UserIdentity {
         *
         * @see newFromName()
         * @see newFromId()
+        * @see newFromActorId()
         * @see newFromConfirmationCode()
         * @see newFromSession()
         * @see newFromRow()
@@ -401,6 +408,32 @@ class User implements IDBAccessObject, UserIdentity {
                        case 'id':
                                $this->loadFromId( $flags );
                                break;
+                       case 'actor':
+                               // Make sure this thread sees its own changes
+                               if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) {
+                                       $flags |= self::READ_LATEST;
+                                       $this->queryFlagsUsed = $flags;
+                               }
+
+                               list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
+                               $row = wfGetDB( $index )->selectRow(
+                                       'actor',
+                                       [ 'actor_user', 'actor_name' ],
+                                       [ 'actor_id' => $this->mActorId ],
+                                       __METHOD__,
+                                       $options
+                               );
+
+                               if ( !$row ) {
+                                       // Ugh.
+                                       $this->loadDefaults();
+                               } elseif ( $row->actor_user ) {
+                                       $this->mId = $row->actor_user;
+                                       $this->loadFromId( $flags );
+                               } else {
+                                       $this->loadDefaults( $row->actor_name );
+                               }
+                               break;
                        case 'session':
                                if ( !$this->loadFromSession() ) {
                                        // Loading from session failed. Load defaults.
@@ -575,6 +608,78 @@ class User implements IDBAccessObject, UserIdentity {
                return $u;
        }
 
+       /**
+        * Static factory method for creation from a given actor ID.
+        *
+        * @since 1.31
+        * @param int $id Valid actor ID
+        * @return User The corresponding User object
+        */
+       public static function newFromActorId( $id ) {
+               global $wgActorTableSchemaMigrationStage;
+
+               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_OLD ) {
+                       throw new BadMethodCallException(
+                               'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage is MIGRATION_OLD'
+                       );
+               }
+
+               $u = new User;
+               $u->mActorId = $id;
+               $u->mFrom = 'actor';
+               $u->setItemLoaded( 'actor' );
+               return $u;
+       }
+
+       /**
+        * Static factory method for creation from an ID, name, and/or actor ID
+        *
+        * This does not check that the ID, name, and actor ID all correspond to
+        * the same user.
+        *
+        * @since 1.31
+        * @param int|null $userId User ID, if known
+        * @param string|null $userName User name, if known
+        * @param int|null $actorId Actor ID, if known
+        * @return User
+        */
+       public static function newFromAnyId( $userId, $userName, $actorId ) {
+               global $wgActorTableSchemaMigrationStage;
+
+               $user = new User;
+               $user->mFrom = 'defaults';
+
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD && $actorId !== null ) {
+                       $user->mActorId = (int)$actorId;
+                       if ( $user->mActorId !== 0 ) {
+                               $user->mFrom = 'actor';
+                       }
+                       $user->setItemLoaded( 'actor' );
+               }
+
+               if ( $userName !== null && $userName !== '' ) {
+                       $user->mName = $userName;
+                       $user->mFrom = 'name';
+                       $user->setItemLoaded( 'name' );
+               }
+
+               if ( $userId !== null ) {
+                       $user->mId = (int)$userId;
+                       if ( $user->mId !== 0 ) {
+                               $user->mFrom = 'id';
+                       }
+                       $user->setItemLoaded( 'id' );
+               }
+
+               if ( $user->mFrom === 'defaults' ) {
+                       throw new InvalidArgumentException(
+                               'Cannot create a user with no name, no ID, and no actor ID'
+                       );
+               }
+
+               return $user;
+       }
+
        /**
         * Factory method to fetch whichever user has a given email confirmation code.
         * This code is generated when an account is created or its e-mail address
@@ -1164,6 +1269,7 @@ class User implements IDBAccessObject, UserIdentity {
        public function loadDefaults( $name = false ) {
                $this->mId = 0;
                $this->mName = $name;
+               $this->mActorId = null;
                $this->mRealName = '';
                $this->mEmail = '';
                $this->mOptionOverrides = null;
@@ -1320,11 +1426,29 @@ class User implements IDBAccessObject, UserIdentity {
         *  user_properties   Array with properties out of the user_properties table
         */
        protected function loadFromRow( $row, $data = null ) {
+               global $wgActorTableSchemaMigrationStage;
+
+               if ( !is_object( $row ) ) {
+                       throw new InvalidArgumentException( '$row must be an object' );
+               }
+
                $all = true;
 
                $this->mGroupMemberships = null; // deferred
 
-               if ( isset( $row->user_name ) ) {
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       if ( isset( $row->actor_id ) ) {
+                               $this->mActorId = (int)$row->actor_id;
+                               if ( $this->mActorId !== 0 ) {
+                                       $this->mFrom = 'actor';
+                               }
+                               $this->setItemLoaded( 'actor' );
+                       } else {
+                               $all = false;
+                       }
+               }
+
+               if ( isset( $row->user_name ) && $row->user_name !== '' ) {
                        $this->mName = $row->user_name;
                        $this->mFrom = 'name';
                        $this->setItemLoaded( 'name' );
@@ -1341,13 +1465,15 @@ class User implements IDBAccessObject, UserIdentity {
 
                if ( isset( $row->user_id ) ) {
                        $this->mId = intval( $row->user_id );
-                       $this->mFrom = 'id';
+                       if ( $this->mId !== 0 ) {
+                               $this->mFrom = 'id';
+                       }
                        $this->setItemLoaded( 'id' );
                } else {
                        $all = false;
                }
 
-               if ( isset( $row->user_id ) && isset( $row->user_name ) ) {
+               if ( isset( $row->user_id ) && isset( $row->user_name ) && $row->user_name !== '' ) {
                        self::$idCacheByName[$row->user_name] = $row->user_id;
                }
 
@@ -1555,7 +1681,7 @@ class User implements IDBAccessObject, UserIdentity {
         * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
         *
         * @param bool|string $reloadFrom Reload user and user_groups table data from a
-        *   given source. May be "name", "id", "defaults", "session", or false for no reload.
+        *   given source. May be "name", "id", "actor", "defaults", "session", or false for no reload.
         */
        public function clearInstanceCache( $reloadFrom = false ) {
                $this->mNewtalk = -1;
@@ -2284,6 +2410,67 @@ class User implements IDBAccessObject, UserIdentity {
                $this->mName = $str;
        }
 
+       /**
+        * Get the user's actor ID.
+        * @since 1.31
+        * @param IDatabase|null $dbw Assign a new actor ID, using this DB handle, if none exists
+        * @return int The actor's ID, or 0 if no actor ID exists and $dbw was null
+        */
+       public function getActorId( IDatabase $dbw = null ) {
+               global $wgActorTableSchemaMigrationStage;
+
+               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_OLD ) {
+                       return 0;
+               }
+
+               if ( !$this->isItemLoaded( 'actor' ) ) {
+                       $this->load();
+               }
+
+               // 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
+               // User::VERSION is incremented after $wgActorTableSchemaMigrationStage
+               // has been removed), that condition may be removed.
+               if ( $this->mActorId === null || !$this->mActorId && $dbw ) {
+                       $q = [
+                               'actor_user' => $this->getId() ?: null,
+                               'actor_name' => (string)$this->getName(),
+                       ];
+                       if ( $dbw ) {
+                               if ( $q['actor_user'] === null && self::isUsableName( $q['actor_name'] ) ) {
+                                       throw new CannotCreateActorException(
+                                               'Cannot create an actor for a usable name that is not an existing user'
+                                       );
+                               }
+                               if ( $q['actor_name'] === '' ) {
+                                       throw new CannotCreateActorException( 'Cannot create an actor for a user with no name' );
+                               }
+                               $dbw->insert( 'actor', $q, __METHOD__, [ 'IGNORE' ] );
+                               if ( $dbw->affectedRows() ) {
+                                       $this->mActorId = (int)$dbw->insertId();
+                               } else {
+                                       // Outdated cache?
+                                       list( , $options ) = DBAccessObjectUtils::getDBOptions( $this->queryFlagsUsed );
+                                       $this->mActorId = (int)$dbw->selectField( 'actor', 'actor_id', $q, __METHOD__, $options );
+                                       if ( !$this->mActorId ) {
+                                               throw new CannotCreateActorException(
+                                                       "Cannot create actor ID for user_id={$this->getId()} user_name={$this->getName()}"
+                                               );
+                                       }
+                               }
+                               $this->invalidateCache();
+                       } else {
+                               list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->queryFlagsUsed );
+                               $db = wfGetDB( $index );
+                               $this->mActorId = (int)$db->selectField( 'actor', 'actor_id', $q, __METHOD__, $options );
+                       }
+                       $this->setItemLoaded( 'actor' );
+               }
+
+               return (int)$this->mActorId;
+       }
+
        /**
         * Get the user's name escaped by underscores.
         * @return string Username escaped by underscores.
@@ -4040,31 +4227,40 @@ class User implements IDBAccessObject, UserIdentity {
                $newTouched = $this->newTouchedTimestamp();
 
                $dbw = wfGetDB( DB_MASTER );
-               $dbw->update( 'user',
-                       [ /* SET */
-                               'user_name' => $this->mName,
-                               'user_real_name' => $this->mRealName,
-                               'user_email' => $this->mEmail,
-                               'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
-                               'user_touched' => $dbw->timestamp( $newTouched ),
-                               'user_token' => strval( $this->mToken ),
-                               'user_email_token' => $this->mEmailToken,
-                               'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
-                       ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
-                               'user_id' => $this->mId,
-                       ] ), __METHOD__
-               );
+               $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $newTouched ) {
+                       $dbw->update( 'user',
+                               [ /* SET */
+                                       'user_name' => $this->mName,
+                                       'user_real_name' => $this->mRealName,
+                                       'user_email' => $this->mEmail,
+                                       'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
+                                       'user_touched' => $dbw->timestamp( $newTouched ),
+                                       'user_token' => strval( $this->mToken ),
+                                       'user_email_token' => $this->mEmailToken,
+                                       'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
+                               ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
+                                       'user_id' => $this->mId,
+                               ] ), $fname
+                       );
 
-               if ( !$dbw->affectedRows() ) {
-                       // Maybe the problem was a missed cache update; clear it to be safe
-                       $this->clearSharedCache( 'refresh' );
-                       // User was changed in the meantime or loaded with stale data
-                       $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
-                       throw new MWException(
-                               "CAS update failed on user_touched for user ID '{$this->mId}' (read from $from);" .
-                               " the version of the user to be saved is older than the current version."
+                       if ( !$dbw->affectedRows() ) {
+                               // Maybe the problem was a missed cache update; clear it to be safe
+                               $this->clearSharedCache( 'refresh' );
+                               // User was changed in the meantime or loaded with stale data
+                               $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'replica';
+                               throw new MWException(
+                                       "CAS update failed on user_touched for user ID '{$this->mId}' (read from $from);" .
+                                       " the version of the user to be saved is older than the current version."
+                               );
+                       }
+
+                       $dbw->update(
+                               'actor',
+                               [ 'actor_name' => $this->mName ],
+                               [ 'actor_user' => $this->mId ],
+                               $fname
                        );
-               }
+               } );
 
                $this->mTouched = $newTouched;
                $this->saveOptions();
@@ -4149,13 +4345,19 @@ class User implements IDBAccessObject, UserIdentity {
                foreach ( $params as $name => $value ) {
                        $fields["user_$name"] = $value;
                }
-               $dbw->insert( 'user', $fields, __METHOD__, [ 'IGNORE' ] );
-               if ( $dbw->affectedRows() ) {
-                       $newUser = self::newFromId( $dbw->insertId() );
-               } else {
-                       $newUser = null;
-               }
-               return $newUser;
+
+               return $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $fields ) {
+                       $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
+                       if ( $dbw->affectedRows() ) {
+                               $newUser = self::newFromId( $dbw->insertId() );
+                               $newUser->mName = $fields['user_name'];
+                               $newUser->setItemLoaded( 'name' );
+                               $newUser->updateActorId( $dbw );
+                       } else {
+                               $newUser = null;
+                       }
+                       return $newUser;
+               } );
        }
 
        /**
@@ -4196,55 +4398,79 @@ class User implements IDBAccessObject, UserIdentity {
 
                $this->mTouched = $this->newTouchedTimestamp();
 
-               $noPass = PasswordFactory::newInvalidPassword()->toString();
-
                $dbw = wfGetDB( DB_MASTER );
-               $dbw->insert( 'user',
-                       [
-                               'user_name' => $this->mName,
-                               'user_password' => $noPass,
-                               'user_newpassword' => $noPass,
-                               'user_email' => $this->mEmail,
-                               'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
-                               'user_real_name' => $this->mRealName,
-                               'user_token' => strval( $this->mToken ),
-                               'user_registration' => $dbw->timestamp( $this->mRegistration ),
-                               'user_editcount' => 0,
-                               'user_touched' => $dbw->timestamp( $this->mTouched ),
-                       ], __METHOD__,
-                       [ 'IGNORE' ]
-               );
-               if ( !$dbw->affectedRows() ) {
-                       // Use locking reads to bypass any REPEATABLE-READ snapshot.
-                       $this->mId = $dbw->selectField(
-                               'user',
-                               'user_id',
-                               [ 'user_name' => $this->mName ],
-                               __METHOD__,
-                               [ 'LOCK IN SHARE MODE' ]
+               $status = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
+                       $noPass = PasswordFactory::newInvalidPassword()->toString();
+                       $dbw->insert( 'user',
+                               [
+                                       'user_name' => $this->mName,
+                                       'user_password' => $noPass,
+                                       'user_newpassword' => $noPass,
+                                       'user_email' => $this->mEmail,
+                                       'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
+                                       'user_real_name' => $this->mRealName,
+                                       'user_token' => strval( $this->mToken ),
+                                       'user_registration' => $dbw->timestamp( $this->mRegistration ),
+                                       'user_editcount' => 0,
+                                       'user_touched' => $dbw->timestamp( $this->mTouched ),
+                               ], $fname,
+                               [ 'IGNORE' ]
                        );
-                       $loaded = false;
-                       if ( $this->mId ) {
-                               if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
-                                       $loaded = true;
+                       if ( !$dbw->affectedRows() ) {
+                               // Use locking reads to bypass any REPEATABLE-READ snapshot.
+                               $this->mId = $dbw->selectField(
+                                       'user',
+                                       'user_id',
+                                       [ 'user_name' => $this->mName ],
+                                       __METHOD__,
+                                       [ 'LOCK IN SHARE MODE' ]
+                               );
+                               $loaded = false;
+                               if ( $this->mId ) {
+                                       if ( $this->loadFromDatabase( self::READ_LOCKING ) ) {
+                                               $loaded = true;
+                                       }
                                }
+                               if ( !$loaded ) {
+                                       throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
+                                               "to insert user '{$this->mName}' row, but it was not present in select!" );
+                               }
+                               return Status::newFatal( 'userexists' );
                        }
-                       if ( !$loaded ) {
-                               throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
-                                       "to insert user '{$this->mName}' row, but it was not present in select!" );
-                       }
-                       return Status::newFatal( 'userexists' );
+                       $this->mId = $dbw->insertId();
+                       self::$idCacheByName[$this->mName] = $this->mId;
+                       $this->updateActorId( $dbw );
+
+                       return Status::newGood();
+               } );
+               if ( !$status->isGood() ) {
+                       return $status;
                }
-               $this->mId = $dbw->insertId();
-               self::$idCacheByName[$this->mName] = $this->mId;
 
-               // Clear instance cache other than user table data, which is already accurate
+               // Clear instance cache other than user table data and actor, which is already accurate
                $this->clearInstanceCache();
 
                $this->saveOptions();
                return Status::newGood();
        }
 
+       /**
+        * Update the actor ID after an insert
+        * @param IDatabase $dbw Writable database handle
+        */
+       private function updateActorId( IDatabase $dbw ) {
+               global $wgActorTableSchemaMigrationStage;
+
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       $dbw->insert(
+                               'actor',
+                               [ 'actor_user' => $this->mId, 'actor_name' => $this->mName ],
+                               __METHOD__
+                       );
+                       $this->mActorId = (int)$dbw->insertId();
+               }
+       }
+
        /**
         * If this user is logged-in and blocked,
         * block any IP address they've successfully logged in from.
@@ -4733,10 +4959,14 @@ class User implements IDBAccessObject, UserIdentity {
                        return false; // anons
                }
                $dbr = wfGetDB( DB_REPLICA );
-               $time = $dbr->selectField( 'revision', 'rev_timestamp',
-                       [ 'rev_user' => $this->getId() ],
+               $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
+               $time = $dbr->selectField(
+                       [ 'revision' ] + $actorWhere['tables'],
+                       'rev_timestamp',
+                       [ $actorWhere['conds'] ],
                        __METHOD__,
-                       [ 'ORDER BY' => 'rev_timestamp ASC' ]
+                       [ 'ORDER BY' => 'rev_timestamp ASC' ],
+                       $actorWhere['joins']
                );
                if ( !$time ) {
                        return false; // no edits
@@ -5184,11 +5414,14 @@ class User implements IDBAccessObject, UserIdentity {
                // Pull from a replica DB to be less cruel to servers
                // Accuracy isn't the point anyway here
                $dbr = wfGetDB( DB_REPLICA );
+               $actorWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $this );
                $count = (int)$dbr->selectField(
-                       'revision',
-                       'COUNT(rev_user)',
-                       [ 'rev_user' => $this->getId() ],
-                       __METHOD__
+                       [ 'revision' ] + $actorWhere['tables'],
+                       'COUNT(*)',
+                       [ $actorWhere['conds'] ],
+                       __METHOD__,
+                       [],
+                       $actorWhere['joins']
                );
                $count = $count + $add;
 
@@ -5537,7 +5770,9 @@ class User implements IDBAccessObject, UserIdentity {
         *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
         */
        public static function getQueryInfo() {
-               return [
+               global $wgActorTableSchemaMigrationStage;
+
+               $ret = [
                        'tables' => [ 'user' ],
                        'fields' => [
                                'user_id',
@@ -5554,6 +5789,15 @@ class User implements IDBAccessObject, UserIdentity {
                        ],
                        'joins' => [],
                ];
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       $ret['tables']['user_actor'] = 'actor';
+                       $ret['fields'][] = 'user_actor.actor_id';
+                       $ret['joins']['user_actor'] = [
+                               $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+                               [ 'user_actor.actor_user = user_id' ]
+                       ];
+               }
+               return $ret;
        }
 
        /**
index 57a0408..d02a678 100644 (file)
@@ -45,7 +45,13 @@ interface UserIdentity {
         */
        public function getName();
 
-       // TODO: in the future, we should also provide access to the actor ID here.
+       /**
+        * @since 1.31
+        *
+        * @return int The user's actor ID. May be 0 if no actor ID is set.
+        */
+       public function getActorId();
+
        // TODO: we may want to (optionally?) provide a global ID, see CentralIdLookup.
 
 }
index e728264..120f31f 100644 (file)
@@ -41,16 +41,24 @@ class UserIdentityValue implements UserIdentity {
         */
        private $name;
 
+       /**
+        * @var int
+        */
+       private $actor;
+
        /**
         * @param int $id
         * @param string $name
+        * @param int $actor
         */
-       public function __construct( $id, $name ) {
+       public function __construct( $id, $name, $actor ) {
                Assert::parameterType( 'integer', $id, '$id' );
                Assert::parameterType( 'string', $name, '$name' );
+               Assert::parameterType( 'integer', $actor, '$actor' );
 
                $this->id = $id;
                $this->name = $name;
+               $this->actor = $actor;
        }
 
        /**
@@ -67,4 +75,11 @@ class UserIdentityValue implements UserIdentity {
                return $this->name;
        }
 
+       /**
+        * @return int The user's actor ID. May be 0 if no actor ID has been assigned.
+        */
+       public function getActorId() {
+               return $this->actor;
+       }
+
 }
index abe9b89..412fdf5 100644 (file)
@@ -59,12 +59,17 @@ class WatchedItemQueryService {
        /** @var CommentStore */
        private $commentStore;
 
+       /** @var ActorMigration */
+       private $actorMigration;
+
        public function __construct(
                LoadBalancer $loadBalancer,
-               CommentStore $commentStore
+               CommentStore $commentStore,
+               ActorMigration $actorMigration
        ) {
                $this->loadBalancer = $loadBalancer;
                $this->commentStore = $commentStore;
+               $this->actorMigration = $actorMigration;
        }
 
        /**
@@ -335,6 +340,14 @@ class WatchedItemQueryService {
                if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
                        $tables[] = 'tag_summary';
                }
+               if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
+                       in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
+                       in_array( self::FILTER_ANON, $options['filters'] ) ||
+                       in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
+                       array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
+               ) {
+                       $tables += $this->actorMigration->getJoin( 'rc_user' )['tables'];
+               }
                return $tables;
        }
 
@@ -367,10 +380,10 @@ class WatchedItemQueryService {
                        $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
                }
                if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
-                       $fields[] = 'rc_user_text';
+                       $fields['rc_user_text'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user_text'];
                }
                if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
-                       $fields[] = 'rc_user';
+                       $fields['rc_user'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user'];
                }
                if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
                        $fields += $this->commentStore->getJoin( 'rc_comment' )['fields'];
@@ -469,9 +482,13 @@ class WatchedItemQueryService {
                }
 
                if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
-                       $conds[] = 'rc_user = 0';
+                       $conds[] = $this->actorMigration->isAnon(
+                               $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
+                       );
                } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
-                       $conds[] = 'rc_user != 0';
+                       $conds[] = $this->actorMigration->isNotAnon(
+                               $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
+                       );
                }
 
                if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
@@ -523,9 +540,11 @@ class WatchedItemQueryService {
                $conds = [];
 
                if ( array_key_exists( 'onlyByUser', $options ) ) {
-                       $conds['rc_user_text'] = $options['onlyByUser'];
+                       $byUser = User::newFromName( $options['onlyByUser'], false );
+                       $conds[] = $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'];
                } elseif ( array_key_exists( 'notByUser', $options ) ) {
-                       $conds[] = 'rc_user_text != ' . $db->addQuotes( $options['notByUser'] );
+                       $byUser = User::newFromName( $options['notByUser'], false );
+                       $conds[] = 'NOT(' . $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'] . ')';
                }
 
                // Avoid brute force searches (T19342)
@@ -685,6 +704,14 @@ class WatchedItemQueryService {
                if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
                        $joinConds['tag_summary'] = [ 'LEFT JOIN', [ 'rc_id=ts_rc_id' ] ];
                }
+               if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
+                       in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
+                       in_array( self::FILTER_ANON, $options['filters'] ) ||
+                       in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
+                       array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
+               ) {
+                       $joinConds += $this->actorMigration->getJoin( 'rc_user' )['joins'];
+               }
                return $joinConds;
        }
 
diff --git a/maintenance/archives/patch-actor-table.sql b/maintenance/archives/patch-actor-table.sql
new file mode 100644 (file)
index 0000000..fdd95e8
--- /dev/null
@@ -0,0 +1,57 @@
+--
+-- patch-actor-table.sql
+--
+-- T167246. Add an `actor` table and various columns (and temporary tables) to reference it.
+
+CREATE TABLE /*_*/actor (
+  actor_id bigint unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  actor_user int unsigned,
+  actor_name varchar(255) binary NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/actor_user ON /*_*/actor (actor_user);
+CREATE UNIQUE INDEX /*i*/actor_name ON /*_*/actor (actor_name);
+
+CREATE TABLE /*_*/revision_actor_temp (
+  revactor_rev int unsigned NOT NULL,
+  revactor_actor bigint unsigned NOT NULL,
+  revactor_timestamp binary(14) NOT NULL default '',
+  revactor_page int unsigned NOT NULL,
+  PRIMARY KEY (revactor_rev, revactor_actor)
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/revactor_rev ON /*_*/revision_actor_temp (revactor_rev);
+CREATE INDEX /*i*/actor_timestamp ON /*_*/revision_actor_temp (revactor_actor,revactor_timestamp);
+CREATE INDEX /*i*/page_actor_timestamp ON /*_*/revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp);
+
+ALTER TABLE /*_*/archive
+  ALTER COLUMN ar_user_text SET DEFAULT '',
+  ADD COLUMN ar_actor bigint unsigned NOT NULL DEFAULT 0 AFTER ar_user_text;
+CREATE INDEX /*i*/ar_actor_timestamp ON /*_*/archive (ar_actor,ar_timestamp);
+
+ALTER TABLE /*_*/ipblocks
+  ADD COLUMN ipb_by_actor bigint unsigned NOT NULL DEFAULT 0 AFTER ipb_by_text;
+
+ALTER TABLE /*_*/image
+  ALTER COLUMN img_user_text SET DEFAULT '',
+  ADD COLUMN img_actor bigint unsigned NOT NULL DEFAULT 0 AFTER img_user_text;
+CREATE INDEX /*i*/img_actor_timestamp ON /*_*/image (img_actor, img_timestamp);
+
+ALTER TABLE /*_*/oldimage
+  ALTER COLUMN oi_user_text SET DEFAULT '',
+  ADD COLUMN oi_actor bigint unsigned NOT NULL DEFAULT 0 AFTER oi_user_text;
+CREATE INDEX /*i*/oi_actor_timestamp ON /*_*/oldimage (oi_actor,oi_timestamp);
+
+ALTER TABLE /*_*/filearchive
+  ALTER COLUMN fa_user_text SET DEFAULT '',
+  ADD COLUMN fa_actor bigint unsigned NOT NULL DEFAULT 0 AFTER fa_user_text;
+CREATE INDEX /*i*/fa_actor_timestamp ON /*_*/filearchive (fa_actor,fa_timestamp);
+
+ALTER TABLE /*_*/recentchanges
+  ALTER COLUMN rc_user_text SET DEFAULT '',
+  ADD COLUMN rc_actor bigint unsigned NOT NULL DEFAULT 0 AFTER rc_user_text;
+CREATE INDEX /*i*/rc_ns_actor ON /*_*/recentchanges (rc_namespace, rc_actor);
+CREATE INDEX /*i*/rc_actor ON /*_*/recentchanges (rc_actor, rc_timestamp);
+
+ALTER TABLE /*_*/logging
+  ADD COLUMN log_actor bigint unsigned NOT NULL DEFAULT 0 AFTER log_user_text;
+CREATE INDEX /*i*/actor_time ON /*_*/logging (log_actor, log_timestamp);
+CREATE INDEX /*i*/log_actor_type_time ON /*_*/logging (log_actor, log_type, log_timestamp);
index c52013b..326073f 100644 (file)
@@ -43,13 +43,19 @@ class DeleteDefaultMessages extends Maintenance {
 
                $this->output( "Checking existence of old default messages..." );
                $dbr = $this->getDB( DB_REPLICA );
-               $res = $dbr->select( [ 'page', 'revision' ],
+
+               $actorQuery = ActorMigration::newMigration()
+                       ->getWhere( $dbr, 'rev_user', User::newFromName( 'MediaWiki default' ) );
+               $res = $dbr->select(
+                       [ 'page', 'revision' ] + $actorQuery['tables'],
                        [ 'page_namespace', 'page_title' ],
                        [
                                'page_namespace' => NS_MEDIAWIKI,
-                               'page_latest=rev_id',
-                               'rev_user_text' => 'MediaWiki default',
-                       ]
+                               $actorQuery['conds'],
+                       ],
+                       __METHOD__,
+                       [],
+                       [ 'revision' => [ 'JOIN', 'page_latest=rev_id' ] ] + $actorQuery['joins']
                );
 
                if ( $dbr->numRows( $res ) == 0 ) {
index 57fd91b..e1b1682 100644 (file)
@@ -59,11 +59,15 @@ class FixUserRegistration extends Maintenance {
                                $id = $row->user_id;
                                $lastId = $id;
                                // Get first edit time
+                               $actorQuery = ActorMigration::newMigration()
+                                       ->getWhere( $dbw, 'rev_user', User::newFromId( $id ) );
                                $timestamp = $dbw->selectField(
-                                       'revision',
+                                       [ 'revision' ] + $actorQuery['tables'],
                                        'MIN(rev_timestamp)',
-                                       [ 'rev_user' => $id ],
-                                       __METHOD__
+                                       $actorQuery['conds'],
+                                       __METHOD__,
+                                       [],
+                                       $actorQuery['joins']
                                );
                                // Update
                                if ( $timestamp !== null ) {
index 3c4336f..f7ef7a2 100644 (file)
@@ -38,9 +38,9 @@ in the load balancer, usually indicating a replication environment.' );
        }
 
        public function execute() {
+               global $wgActorTableSchemaMigrationStage;
+
                $dbw = $this->getDB( DB_MASTER );
-               $user = $dbw->tableName( 'user' );
-               $revision = $dbw->tableName( 'revision' );
 
                // Autodetect mode...
                if ( $this->hasOption( 'background' ) ) {
@@ -51,6 +51,17 @@ in the load balancer, usually indicating a replication environment.' );
                        $backgroundMode = wfGetLB()->getServerCount() > 1;
                }
 
+               $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" );
 
@@ -62,15 +73,55 @@ in the load balancer, usually indicating a replication environment.' );
                        $migrated = 0;
                        for ( $min = 0; $min <= $lastUser; $min += $chunkSize ) {
                                $max = $min + $chunkSize;
-                               $result = $dbr->query(
-                                       "SELECT
-                                               user_id,
-                                               COUNT(rev_user) AS user_editcount
-                                       FROM $user
-                                       LEFT OUTER JOIN $revision ON user_id=rev_user
-                                       WHERE user_id > $min AND user_id <= $max
-                                       GROUP BY user_id",
-                                       __METHOD__ );
+
+                               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']
+                                       );
+                               }
 
                                foreach ( $result as $row ) {
                                        $dbw->update( 'user',
@@ -93,8 +144,43 @@ in the load balancer, usually indicating a replication environment.' );
                        }
                } else {
                        $this->output( "Using single-query mode...\n" );
-                       $sql = "UPDATE $user SET user_editcount=(SELECT COUNT(*) FROM $revision WHERE rev_user=user_id)";
-                       $dbw->query( $sql );
+
+                       $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__ );
+                       }
                }
 
                $this->output( "Done!\n" );
diff --git a/maintenance/migrateActors.php b/maintenance/migrateActors.php
new file mode 100644 (file)
index 0000000..1657f14
--- /dev/null
@@ -0,0 +1,551 @@
+<?php
+/**
+ * Migrate actors from pre-1.31 columns to the 'actor' 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
+ * @ingroup Maintenance
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script that migrates actors from pre-1.31 columns to the
+ * 'actor' table
+ *
+ * @ingroup Maintenance
+ */
+class MigrateActors extends LoggedUpdateMaintenance {
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription( 'Migrates actors from pre-1.31 columns to the \'actor\' table' );
+               $this->setBatchSize( 100 );
+       }
+
+       protected function getUpdateKey() {
+               return __CLASS__;
+       }
+
+       protected function doDBUpdates() {
+               global $wgActorTableSchemaMigrationStage;
+
+               if ( $wgActorTableSchemaMigrationStage < MIGRATION_WRITE_NEW ) {
+                       $this->output(
+                               "...cannot update while \$wgActorTableSchemaMigrationStage < MIGRATION_WRITE_NEW\n"
+                       );
+                       return false;
+               }
+
+               $this->output( "Creating actor entries for all registered users\n" );
+               $end = 0;
+               $dbw = $this->getDB( DB_MASTER );
+               $max = $dbw->selectField( 'user', 'MAX(user_id)', false, __METHOD__ );
+               $count = 0;
+               while ( $end < $max ) {
+                       $start = $end + 1;
+                       $end = min( $start + $this->mBatchSize, $max );
+                       $this->output( "... $start - $end\n" );
+                       $dbw->insertSelect(
+                               'actor',
+                               'user',
+                               [ 'actor_user' => 'user_id', 'actor_name' => 'user_name' ],
+                               [ "user_id >= $start", "user_id <= $end" ],
+                               __METHOD__,
+                               [ 'IGNORE' ],
+                               [ 'ORDER BY' => [ 'user_id' ] ]
+                       );
+                       $count += $dbw->affectedRows();
+                       wfWaitForSlaves();
+               }
+               $this->output( "Completed actor creation, added $count new actor(s)\n" );
+
+               $errors = 0;
+               $errors += $this->migrateToTemp(
+                       'revision', 'rev_id', [ 'revactor_timestamp' => 'rev_timestamp', 'revactor_page' => 'rev_page' ],
+                       'rev_user', 'rev_user_text', 'revactor_rev', 'revactor_actor'
+               );
+               $errors += $this->migrate( 'archive', 'ar_id', 'ar_user', 'ar_user_text', 'ar_actor' );
+               $errors += $this->migrate( 'ipblocks', 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_by_actor' );
+               $errors += $this->migrate( 'image', 'img_name', 'img_user', 'img_user_text', 'img_actor' );
+               $errors += $this->migrate(
+                       'oldimage', [ 'oi_name', 'oi_timestamp' ], 'oi_user', 'oi_user_text', 'oi_actor'
+               );
+               $errors += $this->migrate( 'filearchive', 'fa_id', 'fa_user', 'fa_user_text', 'fa_actor' );
+               $errors += $this->migrate( 'recentchanges', 'rc_id', 'rc_user', 'rc_user_text', 'rc_actor' );
+               $errors += $this->migrate( 'logging', 'log_id', 'log_user', 'log_user_text', 'log_actor' );
+
+               $errors += $this->migrateLogSearch();
+
+               return $errors === 0;
+       }
+
+       /**
+        * Calculate a "next" condition and a display string
+        * @param IDatabase $dbw
+        * @param string[] $primaryKey Primary key of the table.
+        * @param object $row Database row
+        * @return array [ string $next, string $display ]
+        */
+       private function makeNextCond( $dbw, $primaryKey, $row ) {
+               $next = '';
+               $display = [];
+               for ( $i = count( $primaryKey ) - 1; $i >= 0; $i-- ) {
+                       $field = $primaryKey[$i];
+                       $display[] = $field . '=' . $row->$field;
+                       $value = $dbw->addQuotes( $row->$field );
+                       if ( $next === '' ) {
+                               $next = "$field > $value";
+                       } else {
+                               $next = "$field > $value OR $field = $value AND ($next)";
+                       }
+               }
+               $display = implode( ' ', array_reverse( $display ) );
+               return [ $next, $display ];
+       }
+
+       /**
+        * Add actors for anons in a set of rows
+        * @param IDatabase $dbw
+        * @param string $nameField
+        * @param object[] &$rows
+        * @param array &$complainedAboutUsers
+        * @param int &$countErrors
+        * @return int Count of actors inserted
+        */
+       private function addActorsForRows(
+               IDatabase $dbw, $nameField, array &$rows, array &$complainedAboutUsers, &$countErrors
+       ) {
+               $needActors = [];
+               $countActors = 0;
+
+               $keep = [];
+               foreach ( $rows as $index => $row ) {
+                       $keep[$index] = true;
+                       if ( $row->actor_id === null ) {
+                               // All registered users should have an actor_id already. So
+                               // if we have a usable name here, it means they didn't run
+                               // maintenance/cleanupUsersWithNoId.php
+                               $name = $row->$nameField;
+                               if ( User::isUsableName( $name ) ) {
+                                       if ( !isset( $complainedAboutUsers[$name] ) ) {
+                                               $complainedAboutUsers[$name] = true;
+                                               $this->error(
+                                                       "User name \"$name\" is usable, cannot create an anonymous actor for it."
+                                                       . " Run maintenance/cleanupUsersWithNoId.php to fix this situation.\n"
+                                               );
+                                       }
+                                       unset( $keep[$index] );
+                                       $countErrors++;
+                               } else {
+                                       $needActors[$name] = 0;
+                               }
+                       }
+               }
+               $rows = array_intersect_key( $rows, $keep );
+
+               if ( $needActors ) {
+                       $dbw->insert(
+                               'actor',
+                               array_map( function ( $v ) {
+                                       return [
+                                               'actor_name' => $v,
+                                       ];
+                               }, array_keys( $needActors ) ),
+                               __METHOD__
+                       );
+                       $countActors += $dbw->affectedRows();
+
+                       $res = $dbw->select(
+                               'actor',
+                               [ 'actor_id', 'actor_name' ],
+                               [ 'actor_name' => array_keys( $needActors ) ],
+                               __METHOD__
+                       );
+                       foreach ( $res as $row ) {
+                               $needActors[$row->actor_name] = $row->actor_id;
+                       }
+                       foreach ( $rows as $row ) {
+                               if ( $row->actor_id === null ) {
+                                       $row->actor_id = $needActors[$row->$nameField];
+                               }
+                       }
+               }
+
+               return $countActors;
+       }
+
+       /**
+        * Migrate actors in a table.
+        *
+        * Assumes any row with the actor field non-zero have already been migrated.
+        * Blanks the name field when migrating.
+        *
+        * @param string $table Table to migrate
+        * @param string|string[] $primaryKey Primary key of the table.
+        * @param string $userField User ID field name
+        * @param string $nameField User name field name
+        * @param string $actorField Actor field name
+        * @return int Number of errors
+        */
+       protected function migrate( $table, $primaryKey, $userField, $nameField, $actorField ) {
+               $complainedAboutUsers = [];
+
+               $primaryKey = (array)$primaryKey;
+               $pkFilter = array_flip( $primaryKey );
+               $this->output(
+                       "Beginning migration of $table.$userField and $table.$nameField to $table.$actorField\n"
+               );
+               wfWaitForSlaves();
+
+               $dbw = $this->getDB( DB_MASTER );
+               $next = '1=1';
+               $countUpdated = 0;
+               $countActors = 0;
+               $countErrors = 0;
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               [ $table, 'actor' ],
+                               array_merge( $primaryKey, [ $userField, $nameField, 'actor_id' ] ),
+                               [
+                                       $actorField => 0,
+                                       $next,
+                               ],
+                               __METHOD__,
+                               [
+                                       'ORDER BY' => $primaryKey,
+                                       'LIMIT' => $this->mBatchSize,
+                               ],
+                               [
+                                       'actor' => [
+                                               'LEFT JOIN',
+                                               "$userField != 0 AND actor_user = $userField OR "
+                                               . "($userField = 0 OR $userField IS NULL) AND actor_name = $nameField"
+                                       ]
+                               ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Insert new actors for rows that need one
+                       $rows = iterator_to_array( $res );
+                       $lastRow = end( $rows );
+                       $countActors += $this->addActorsForRows(
+                               $dbw, $nameField, $rows, $complainedAboutUsers, $countErrors
+                       );
+
+                       // Update the existing rows
+                       foreach ( $rows as $row ) {
+                               if ( !$row->actor_id ) {
+                                       list( , $display ) = $this->makeNextCond( $dbw, $primaryKey, $row );
+                                       $this->error(
+                                               "Could not make actor for row with $display "
+                                               . "$userField={$row->$userField} $nameField={$row->$nameField}\n"
+                                       );
+                                       $countErrors++;
+                                       continue;
+                               }
+                               $dbw->update(
+                                       $table,
+                                       [
+                                               $actorField => $row->actor_id,
+                                               $nameField => '',
+                                       ],
+                                       array_intersect_key( (array)$row, $pkFilter ) + [
+                                               $actorField => 0
+                                       ],
+                                       __METHOD__
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                       }
+
+                       list( $next, $display ) = $this->makeNextCond( $dbw, $primaryKey, $lastRow );
+                       $this->output( "... $display\n" );
+                       wfWaitForSlaves();
+               }
+
+               $this->output(
+                       "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), "
+                       . "$countErrors error(s)\n"
+               );
+               return $countErrors;
+       }
+
+       /**
+        * Migrate actors in a table to a temporary table.
+        *
+        * Assumes the new table is named "{$table}_actor_temp", and it has two
+        * columns, in order, being the primary key of the original table and the
+        * actor ID field.
+        * Blanks the name field when migrating.
+        *
+        * @param string $table Table to migrate
+        * @param string $primaryKey Primary key of the table.
+        * @param array $extra Extra fields to copy
+        * @param string $userField User ID field name
+        * @param string $nameField User name field name
+        * @param string $newPrimaryKey Primary key of the new table.
+        * @param string $actorField Actor field name
+        */
+       protected function migrateToTemp(
+               $table, $primaryKey, $extra, $userField, $nameField, $newPrimaryKey, $actorField
+       ) {
+               $complainedAboutUsers = [];
+
+               $newTable = $table . '_actor_temp';
+               $this->output(
+                       "Beginning migration of $table.$userField and $table.$nameField to $newTable.$actorField\n"
+               );
+               wfWaitForSlaves();
+
+               $dbw = $this->getDB( DB_MASTER );
+               $next = [];
+               $countUpdated = 0;
+               $countActors = 0;
+               $countErrors = 0;
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               [ $table, $newTable, 'actor' ],
+                               [ $primaryKey, $userField, $nameField, 'actor_id' ] + $extra,
+                               [ $newPrimaryKey => null ] + $next,
+                               __METHOD__,
+                               [
+                                       'ORDER BY' => $primaryKey,
+                                       'LIMIT' => $this->mBatchSize,
+                               ],
+                               [
+                                       $newTable => [ 'LEFT JOIN', "{$primaryKey}={$newPrimaryKey}" ],
+                                       'actor' => [
+                                               'LEFT JOIN',
+                                               "$userField != 0 AND actor_user = $userField OR "
+                                               . "($userField = 0 OR $userField IS NULL) AND actor_name = $nameField"
+                                       ]
+                               ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Insert new actors for rows that need one
+                       $rows = iterator_to_array( $res );
+                       $lastRow = end( $rows );
+                       $countActors += $this->addActorsForRows(
+                               $dbw, $nameField, $rows, $complainedAboutUsers, $countErrors
+                       );
+
+                       // Update rows
+                       if ( $rows ) {
+                               $inserts = [];
+                               $updates = [];
+                               foreach ( $rows as $row ) {
+                                       if ( !$row->actor_id ) {
+                                               list( , $display ) = $this->makeNextCond( $dbw, [ $primaryKey ], $row );
+                                               $this->error(
+                                                       "Could not make actor for row with $display "
+                                                       . "$userField={$row->$userField} $nameField={$row->$nameField}\n"
+                                               );
+                                               $countErrors++;
+                                               continue;
+                                       }
+                                       $ins = [
+                                               $newPrimaryKey => $row->$primaryKey,
+                                               $actorField => $row->actor_id,
+                                       ];
+                                       foreach ( $extra as $to => $from ) {
+                                               $ins[$to] = $row->$to; // It's aliased
+                                       }
+                                       $inserts[] = $ins;
+                                       $updates[] = $row->$primaryKey;
+                               }
+                               $this->beginTransaction( $dbw, __METHOD__ );
+                               $dbw->insert( $newTable, $inserts, __METHOD__ );
+                               $dbw->update( $table, [ $nameField => '' ], [ $primaryKey => $updates ], __METHOD__ );
+                               $countUpdated += $dbw->affectedRows();
+                               $this->commitTransaction( $dbw, __METHOD__ );
+                       }
+
+                       // Calculate the "next" condition
+                       list( $n, $display ) = $this->makeNextCond( $dbw, [ $primaryKey ], $lastRow );
+                       $next = [ $n ];
+                       $this->output( "... $display\n" );
+                       wfWaitForSlaves();
+               }
+
+               $this->output(
+                       "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), "
+                       . "$countErrors error(s)\n"
+               );
+               return $countErrors;
+       }
+
+       /**
+        * Migrate actors in the log_search table.
+        * @return int Number of errors
+        */
+       protected function migrateLogSearch() {
+               $complainedAboutUsers = [];
+
+               $primaryKey = [ 'ls_field', 'ls_value' ];
+               $pkFilter = array_flip( $primaryKey );
+               $this->output( "Beginning migration of log_search\n" );
+               wfWaitForSlaves();
+
+               $dbw = $this->getDB( DB_MASTER );
+               $countUpdated = 0;
+               $countActors = 0;
+               $countErrors = 0;
+
+               $next = '1=1';
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               [ 'log_search', 'actor' ],
+                               [ 'ls_field', 'ls_value', 'actor_id' ],
+                               [
+                                       'ls_field' => 'target_author_id',
+                                       $next,
+                               ],
+                               __METHOD__,
+                               [
+                                       'DISTINCT',
+                                       'ORDER BY' => [ 'ls_value' ],
+                                       'LIMIT' => $this->mBatchSize,
+                               ],
+                               [ 'actor' => [ 'LEFT JOIN', 'ls_value = ' . $dbw->buildStringCast( 'actor_user' ) ] ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Update the rows
+                       $del = [];
+                       foreach ( $res as $row ) {
+                               $lastRow = $row;
+                               if ( !$row->actor_id ) {
+                                       list( , $display ) = $this->makeNextCond( $dbw, $primaryKey, $row );
+                                       $this->error( "No actor for row with $display\n" );
+                                       $countErrors++;
+                                       continue;
+                               }
+                               $dbw->update(
+                                       'log_search',
+                                       [
+                                               'ls_field' => 'target_author_actor',
+                                               'ls_value' => $row->actor_id,
+                                       ],
+                                       [
+                                               'ls_field' => $row->ls_field,
+                                               'ls_value' => $row->ls_value,
+                                       ],
+                                       __METHOD__,
+                                       [ 'IGNORE' ]
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                               $del[] = $row->ls_value;
+                       }
+                       if ( $del ) {
+                               $dbw->delete(
+                                       'log_search', [ 'ls_field' => 'target_author_id', 'ls_value' => $del ], __METHOD__
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                       }
+
+                       list( $next, $display ) = $this->makeNextCond( $dbw, $primaryKey, $lastRow );
+                       $this->output( "... $display\n" );
+                       wfWaitForSlaves();
+               }
+
+               $next = '1=1';
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               [ 'log_search', 'actor' ],
+                               [ 'ls_field', 'ls_value', 'actor_id' ],
+                               [
+                                       'ls_field' => 'target_author_ip',
+                                       $next,
+                               ],
+                               __METHOD__,
+                               [
+                                       'DISTINCT',
+                                       'ORDER BY' => [ 'ls_value' ],
+                                       'LIMIT' => $this->mBatchSize,
+                               ],
+                               [ 'actor' => [ 'LEFT JOIN', 'ls_value = actor_name' ] ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Insert new actors for rows that need one
+                       $rows = iterator_to_array( $res );
+                       $lastRow = end( $rows );
+                       $countActors += $this->addActorsForRows(
+                               $dbw, 'ls_value', $rows, $complainedAboutUsers, $countErrors
+                       );
+
+                       // Update the rows
+                       $del = [];
+                       foreach ( $rows as $row ) {
+                               if ( !$row->actor_id ) {
+                                       list( , $display ) = $this->makeNextCond( $dbw, $primaryKey, $row );
+                                       $this->error( "Could not make actor for row with $display\n" );
+                                       $countErrors++;
+                                       continue;
+                               }
+                               $dbw->update(
+                                       'log_search',
+                                       [
+                                               'ls_field' => 'target_author_actor',
+                                               'ls_value' => $row->actor_id,
+                                       ],
+                                       [
+                                               'ls_field' => $row->ls_field,
+                                               'ls_value' => $row->ls_value,
+                                       ],
+                                       __METHOD__,
+                                       [ 'IGNORE' ]
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                               $del[] = $row->ls_value;
+                       }
+                       if ( $del ) {
+                               $dbw->delete(
+                                       'log_search', [ 'ls_field' => 'target_author_ip', 'ls_value' => $del ], __METHOD__
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                       }
+
+                       list( $next, $display ) = $this->makeNextCond( $dbw, $primaryKey, $lastRow );
+                       $this->output( "... $display\n" );
+                       wfWaitForSlaves();
+               }
+
+               $this->output(
+                       "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), "
+                       . "$countErrors error(s)\n"
+               );
+               return $countErrors;
+       }
+}
+
+$maintClass = "MigrateActors";
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/maintenance/mssql/archives/patch-actor-table.sql b/maintenance/mssql/archives/patch-actor-table.sql
new file mode 100644 (file)
index 0000000..b26ad44
--- /dev/null
@@ -0,0 +1,53 @@
+--
+-- patch-actor-table.sql
+--
+-- T167246. Add an `actor` table and various columns (and temporary tables) to reference it.
+
+CREATE TABLE /*_*/actor (
+  actor_id bigint unsigned NOT NULL CONSTRAINT PK_actor PRIMARY KEY IDENTITY(0,1),
+  actor_user int unsigned,
+  actor_name nvarchar(255) NOT NULL
+);
+CREATE UNIQUE INDEX /*i*/actor_user ON /*_*/actor (actor_user);
+CREATE UNIQUE INDEX /*i*/actor_name ON /*_*/actor (actor_name);
+
+-- Dummy
+INSERT INTO /*_*/actor (actor_name) VALUES ('##Anonymous##');
+
+CREATE TABLE /*_*/revision_actor_temp (
+  revactor_rev int unsigned NOT NULL CONSTRAINT FK_revactor_rev FOREIGN KEY REFERENCES /*_*/revision(rev_id) ON DELETE CASCADE,
+  revactor_actor bigint unsigned NOT NULL,
+  revactor_timestamp varchar(14) NOT NULL CONSTRAINT DF_revactor_timestamp DEFAULT '',
+  revactor_page int unsigned NOT NULL,
+  CONSTRAINT PK_revision_actor_temp PRIMARY KEY (revactor_rev, revactor_actor)
+);
+CREATE UNIQUE INDEX /*i*/revactor_rev ON /*_*/revision_actor_temp (revactor_rev);
+CREATE INDEX /*i*/actor_timestamp ON /*_*/revision_actor_temp (revactor_actor,revactor_timestamp);
+CREATE INDEX /*i*/page_actor_timestamp ON /*_*/revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp);
+
+ALTER TABLE /*_*/archive ADD CONSTRAINT DF_ar_user_text DEFAULT '' FOR ar_user_text;
+ALTER TABLE /*_*/archive ADD ar_actor bigint unsigned NOT NULL CONSTRAINT DF_ar_actor DEFAULT 0;
+CREATE INDEX /*i*/ar_actor_timestamp ON /*_*/archive (ar_actor,ar_timestamp);
+
+ALTER TABLE /*_*/ipblocks ADD ipb_by_actor bigint unsigned NOT NULL CONSTRAINT DF_ipb_by_actor DEFAULT 0;
+
+ALTER TABLE /*_*/image ADD CONSTRAINT DF_img_user_text DEFAULT '' FOR img_user_text;
+ALTER TABLE /*_*/image ADD img_actor bigint unsigned NOT NULL CONSTRAINT DF_img_actor DEFAULT 0;
+CREATE INDEX /*i*/img_actor_timestamp ON /*_*/image (img_actor, img_timestamp);
+
+ALTER TABLE /*_*/oldimage ADD CONSTRAINT DF_oi_user_text DEFAULT '' FOR oi_user_text;
+ALTER TABLE /*_*/oldimage ADD oi_actor bigint unsigned NOT NULL CONSTRAINT DF_oi_actor DEFAULT 0;
+CREATE INDEX /*i*/oi_actor_timestamp ON /*_*/oldimage (oi_actor,oi_timestamp);
+
+ALTER TABLE /*_*/filearchive ADD CONSTRAINT DF_fa_user_text DEFAULT '' FOR fa_user_text;
+ALTER TABLE /*_*/filearchive ADD fa_actor bigint unsigned NOT NULL CONSTRAINT DF_fa_actor DEFAULT 0;
+CREATE INDEX /*i*/fa_actor_timestamp ON /*_*/filearchive (fa_actor,fa_timestamp);
+
+ALTER TABLE /*_*/recentchanges ADD CONSTRAINT DF_rc_user_text DEFAULT '' FOR rc_user_text;
+ALTER TABLE /*_*/recentchanges ADD rc_actor bigint unsigned NOT NULL CONSTRAINT DF_rc_actor DEFAULT 0;
+CREATE INDEX /*i*/rc_ns_actor ON /*_*/recentchanges (rc_namespace, rc_actor);
+CREATE INDEX /*i*/rc_actor ON /*_*/recentchanges (rc_actor, rc_timestamp);
+
+ALTER TABLE /*_*/logging ADD log_actor bigint unsigned NOT NULL CONSTRAINT DF_log_actor DEFAULT 0;
+CREATE INDEX /*i*/actor_time ON /*_*/logging (log_actor, log_timestamp);
+CREATE INDEX /*i*/log_actor_type_time ON /*_*/logging (log_actor, log_type, log_timestamp);
index 4673264..5348c47 100644 (file)
@@ -54,6 +54,23 @@ CREATE INDEX /*i*/user_email ON /*_*/mwuser (user_email);
 -- Insert a dummy user to represent anons
 INSERT INTO /*_*/mwuser (user_name) VALUES ('##Anonymous##');
 
+--
+-- The "actor" table associates user names or IP addresses with integers for
+-- the benefit of other tables that need to refer to either logged-in or
+-- logged-out users. If something can only ever be done by logged-in users, it
+-- can refer to the user table directly.
+--
+CREATE TABLE /*_*/actor (
+  actor_id bigint unsigned NOT NULL CONSTRAINT PK_actor PRIMARY KEY IDENTITY(0,1),
+  actor_user int unsigned,
+  actor_name nvarchar(255) NOT NULL
+);
+CREATE UNIQUE INDEX /*i*/actor_user ON /*_*/actor (actor_user);
+CREATE UNIQUE INDEX /*i*/actor_name ON /*_*/actor (actor_name);
+
+-- Insert a dummy actor to represent no actor
+INSERT INTO /*_*/actor (actor_name) VALUES ('##Anonymous##');
+
 --
 -- User permissions have been broken out to a separate table;
 -- this allows sites with a shared user table to have different
@@ -200,7 +217,7 @@ INSERT INTO /*_*/revision (rev_page,rev_text_id,rev_comment,rev_user,rev_len) VA
 ALTER TABLE /*_*/page ADD CONSTRAINT FK_page_latest_page_id FOREIGN KEY (page_latest) REFERENCES /*_*/revision(rev_id);
 
 --
--- Temporary table to avoid blocking on an alter of revision.
+-- Temporary tables to avoid blocking on an alter of revision.
 --
 -- On large wikis like the English Wikipedia, altering the revision table is a
 -- months-long process. This table is being created to avoid such an alter, and
@@ -213,6 +230,17 @@ CREATE TABLE /*_*/revision_comment_temp (
 );
 CREATE UNIQUE INDEX /*i*/revcomment_rev ON /*_*/revision_comment_temp (revcomment_rev);
 
+CREATE TABLE /*_*/revision_actor_temp (
+  revactor_rev int unsigned NOT NULL CONSTRAINT FK_revactor_rev FOREIGN KEY REFERENCES /*_*/revision(rev_id) ON DELETE CASCADE,
+  revactor_actor bigint unsigned NOT NULL,
+  revactor_timestamp varchar(14) NOT NULL CONSTRAINT DF_revactor_timestamp DEFAULT '',
+  revactor_page int unsigned NOT NULL,
+  CONSTRAINT PK_revision_actor_temp PRIMARY KEY (revactor_rev, revactor_actor)
+);
+CREATE UNIQUE INDEX /*i*/revactor_rev ON /*_*/revision_actor_temp (revactor_rev);
+CREATE INDEX /*i*/actor_timestamp ON /*_*/revision_actor_temp (revactor_actor,revactor_timestamp);
+CREATE INDEX /*i*/page_actor_timestamp ON /*_*/revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp);
+
 --
 -- Holds TEXT of individual page revisions.
 --
@@ -246,7 +274,8 @@ CREATE TABLE /*_*/archive (
    ar_comment NVARCHAR(255) NOT NULL CONSTRAINT DF_ar_comment DEFAULT '',
    ar_comment_id bigint unsigned NOT NULL CONSTRAINT DF_ar_comment_id DEFAULT 0 CONSTRAINT FK_ar_comment_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
    ar_user INT CONSTRAINT ar_user__user_id__fk FOREIGN KEY REFERENCES /*_*/mwuser(user_id),
-   ar_user_text NVARCHAR(255) NOT NULL,
+   ar_user_text NVARCHAR(255) NOT NULL CONSTRAINT DF_ar_user_text DEFAULT '',
+   ar_actor bigint unsigned NOT NULL CONSTRAINT DF_ar_actor DEFAULT 0,
    ar_timestamp varchar(14) NOT NULL default '',
    ar_minor_edit BIT NOT NULL DEFAULT 0,
    ar_flags NVARCHAR(255) NOT NULL,
@@ -262,6 +291,7 @@ CREATE TABLE /*_*/archive (
 );
 CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
 CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp);
+CREATE INDEX /*i*/ar_actor_timestamp ON /*_*/archive (ar_actor,ar_timestamp);
 CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id);
 
 
@@ -600,6 +630,9 @@ CREATE TABLE /*_*/ipblocks (
   -- User ID who made the block.
   ipb_by int REFERENCES /*_*/mwuser(user_id) ON DELETE CASCADE,
 
+  -- Actor ID who made the block.
+  ipb_by_actor bigint unsigned NOT NULL CONSTRAINT DF_ipb_by_actor DEFAULT 0,
+
   -- User name of blocker
   ipb_by_text nvarchar(255) NOT NULL default '',
 
@@ -709,7 +742,8 @@ CREATE TABLE /*_*/image (
 
   -- user_id and user_name of uploader.
   img_user int REFERENCES /*_*/mwuser(user_id) ON DELETE SET NULL,
-  img_user_text nvarchar(255) NOT NULL,
+  img_user_text nvarchar(255) NOT NULL CONSTRAINT DF_img_user_text DEFAULT '',
+  img_actor bigint unsigned NOT NULL CONSTRAINT DF_img_actor DEFAULT 0,
 
   -- Time of the upload.
   img_timestamp nvarchar(14) NOT NULL default '',
@@ -722,6 +756,7 @@ CREATE TABLE /*_*/image (
 );
 
 CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+CREATE INDEX /*i*/img_actor_timestamp ON /*_*/image (img_actor, img_timestamp);
 -- Used by Special:ListFiles for sort-by-size
 CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
 -- Used by Special:Newimages and Special:ListFiles
@@ -768,7 +803,8 @@ CREATE TABLE /*_*/oldimage (
   oi_description nvarchar(255) NOT NULL CONSTRAINT DF_oi_description DEFAULT '',
   oi_description_id bigint unsigned NOT NULL CONSTRAINT DF_oi_description_id DEFAULT 0 CONSTRAINT FK_oi_description_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
   oi_user int REFERENCES /*_*/mwuser(user_id),
-  oi_user_text nvarchar(255) NOT NULL,
+  oi_user_text nvarchar(255) NOT NULL CONSTRAINT DF_oi_user_text DEFAULT '',
+  oi_actor bigint unsigned NOT NULL CONSTRAINT DF_oi_actor DEFAULT 0,
   oi_timestamp varchar(14) NOT NULL default '',
 
   oi_metadata varbinary(max) NOT NULL,
@@ -783,6 +819,7 @@ CREATE TABLE /*_*/oldimage (
 );
 
 CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_actor_timestamp ON /*_*/oldimage (oi_actor,oi_timestamp);
 CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
 CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1);
 
@@ -830,7 +867,8 @@ CREATE TABLE /*_*/filearchive (
   fa_description nvarchar(255) CONSTRAINT DF_fa_description DEFAULT '',
   fa_description_id bigint unsigned NOT NULL CONSTRAINT DF_fa_description DEFAULT 0 CONSTRAINT FK_fa_description FOREIGN KEY REFERENCES /*_*/comment(comment_id),
   fa_user int default 0 REFERENCES /*_*/mwuser(user_id) ON DELETE SET NULL,
-  fa_user_text nvarchar(255),
+  fa_user_text nvarchar(255) CONSTRAINT DF_fa_user_text DEFAULT '',
+  fa_actor bigint unsigned NOT NULL CONSTRAINT DF_fa_actor DEFAULT 0,
   fa_timestamp varchar(14) default '',
 
   -- Visibility of deleted revisions, bitfield
@@ -851,6 +889,7 @@ CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_sto
 CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
 -- sort by uploader
 CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+CREATE INDEX /*i*/fa_actor_timestamp ON /*_*/filearchive (fa_actor,fa_timestamp);
 -- find file by sha1, 10 bytes will be enough for hashes to be indexed
 CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1);
 
@@ -923,7 +962,8 @@ CREATE TABLE /*_*/recentchanges (
 
   -- As in revision
   rc_user int NOT NULL default 0 CONSTRAINT rc_user__user_id__fk FOREIGN KEY REFERENCES /*_*/mwuser(user_id),
-  rc_user_text nvarchar(255) NOT NULL,
+  rc_user_text nvarchar(255) NOT NULL CONSTRAINT DF_rc_user_text DEFAULT '',
+  rc_actor bigint unsigned NOT NULL CONSTRAINT DF_rc_actor DEFAULT 0,
 
   -- When pages are renamed, their RC entries do _not_ change.
   rc_namespace int NOT NULL default 0,
@@ -995,6 +1035,8 @@ CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,
 CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip);
 CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text);
 CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp);
+CREATE INDEX /*i*/rc_ns_actor ON /*_*/recentchanges (rc_namespace, rc_actor);
+CREATE INDEX /*i*/rc_actor ON /*_*/recentchanges (rc_actor, rc_timestamp);
 CREATE INDEX /*i*/rc_name_type_patrolled_timestamp ON /*_*/recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
 
 
@@ -1126,6 +1168,9 @@ CREATE TABLE /*_*/logging (
   -- Name of the user who performed this action
   log_user_text nvarchar(255) NOT NULL default '',
 
+  -- The actor who performed this action
+  log_actor bigint unsigned NOT NULL CONSTRAINT DF_log_actor DEFAULT 0,
+
   -- Key to the page affected. Where a user is the target,
   -- this will point to the user page.
   log_namespace int NOT NULL default 0,
@@ -1156,6 +1201,8 @@ CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp);
 CREATE INDEX /*i*/type_action ON /*_*/logging (log_type, log_action, log_timestamp);
 CREATE INDEX /*i*/log_user_text_type_time ON /*_*/logging (log_user_text, log_type, log_timestamp);
 CREATE INDEX /*i*/log_user_text_time ON /*_*/logging (log_user_text, log_timestamp);
+CREATE INDEX /*i*/actor_time ON /*_*/logging (log_actor, log_timestamp);
+CREATE INDEX /*i*/log_actor_type_time ON /*_*/logging (log_actor, log_type, log_timestamp);
 
 INSERT INTO /*_*/logging (log_user,log_page,log_params) VALUES(0,0,'');
 
diff --git a/maintenance/oracle/archives/patch-actor-table.sql b/maintenance/oracle/archives/patch-actor-table.sql
new file mode 100644 (file)
index 0000000..93c7531
--- /dev/null
@@ -0,0 +1,64 @@
+--
+-- patch-actor-table.sql
+--
+-- T167246. Add an `actor` table and various columns (and temporary tables) to reference it.
+
+define mw_prefix='{$wgDBprefix}';
+
+CREATE SEQUENCE actor_actor_id_seq;
+CREATE TABLE &mw_prefix.actor (
+  actor_id NUMBER NOT NULL,
+  actor_user NUMBER,
+  actor_name VARCHAR2(255) NOT NULL
+);
+
+ALTER TABLE &mw_prefix.actor ADD CONSTRAINT &mw_prefix.actor_pk PRIMARY KEY (actor_id);
+
+/*$mw$*/
+CREATE TRIGGER &mw_prefix.actor_seq_trg BEFORE INSERT ON &mw_prefix.actor
+       FOR EACH ROW WHEN (new.actor_id IS NULL)
+BEGIN
+       &mw_prefix.lastval_pkg.setLastval(actor_actor_id_seq.nextval, :new.actor_id);
+END;
+/*$mw$*/
+
+-- Create a dummy actor to satisfy fk contraints
+INSERT INTO &mw_prefix.actor (actor_id, actor_name) VALUES (0,'##Anonymous##');
+
+CREATE TABLE &mw_prefix.revision_actor_temp (
+  revactor_rev NUMBER NOT NULL,
+  revactor_actor NUMBER NOT NULL,
+  revactor_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
+  revactor_page NUMBER NOT NULL
+);
+ALTER TABLE &mw_prefix.revision_actor_temp ADD CONSTRAINT &mw_prefix.revision_actor_temp_pk PRIMARY KEY (revactor_rev, revactor_actor);
+CREATE UNIQUE INDEX &mw_prefix.revactor_rev ON &mw_prefix.revision_actor_temp (revactor_rev);
+CREATE INDEX &mw_prefix.actor_timestamp ON &mw_prefix.revision_actor_temp (revactor_actor,revactor_timestamp);
+CREATE INDEX &mw_prefix.page_actor_timestamp ON &mw_prefix.revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp);
+
+ALTER TABLE &mw_prefix.archive ALTER COLUMN ar_user_text VARCHAR2(255) NULL;
+ALTER TABLE &mw_prefix.archive ADD COLUMN ar_actor NUMBER DEFAULT 0 NOT NULL;
+CREATE INDEX &mw_prefix.ar_actor_timestamp ON &mw_prefix.archive (ar_actor,ar_timestamp);
+
+ALTER TABLE &mw_prefix.ipblocks ADD COLUMN ipb_by_actor NUMBER DEFUALT 0 NOT NULL;
+
+ALTER TABLE &mw_prefix.image ALTER COLUMN img_user_text VARCHAR2(255) NULL;
+ALTER TABLE &mw_prefix.image ADD COLUMN img_actor NUMBER DEFAULT 0 NOT NULL;
+CREATE INDEX &mw_prefix.img_actor_timestamp ON &mw_prefix.image (img_actor, img_timestamp);
+
+ALTER TABLE &mw_prefix.oldimage ALTER COLUMN oi_user_text VARCHAR2(255) NULL;
+ALTER TABLE &mw_prefix.oldimage ADD COLUMN oi_actor NUMBER DEFAULT 0 NOT NULL;
+CREATE INDEX &mw_prefix.oi_actor_timestamp ON &mw_prefix.oldimage (oi_actor,oi_timestamp);
+
+ALTER TABLE &mw_prefix.filearchive ALTER COLUMN fa_user_text VARCHAR2(255) NULL;
+ALTER TABLE &mw_prefix.filearchive ADD COLUMN fa_actor NUMBER DEFAULT 0 NOT NULL;
+CREATE INDEX &mw_prefix.fa_actor_timestamp ON &mw_prefix.filearchive (fa_actor,fa_timestamp);
+
+ALTER TABLE &mw_prefix.recentchanges ALTER COLUMN rc_user_text VARCHAR2(255) NULL;
+ALTER TABLE &mw_prefix.recentchanges ADD COLUMN rc_actor NUMBER DEFAULT 0 NOT NULL;
+CREATE INDEX &mw_prefix.rc_ns_actor ON &mw_prefix.recentchanges (rc_namespace, rc_actor);
+CREATE INDEX &mw_prefix.rc_actor ON &mw_prefix.recentchanges (rc_actor, rc_timestamp);
+
+ALTER TABLE &mw_prefix.logging ADD COLUMN log_actor NUMBER DEFAULT 0 NOT NULL;
+CREATE INDEX &mw_prefix.actor_time ON &mw_prefix.logging (log_actor, log_timestamp);
+CREATE INDEX &mw_prefix.log_actor_type_time ON &mw_prefix.logging (log_actor, log_type, log_timestamp);
index 7195a5e..110188d 100644 (file)
@@ -60,6 +60,26 @@ INSERT INTO &mw_prefix.mwuser
   (user_id, user_name, user_options, user_touched, user_registration, user_editcount)
   VALUES (0,'Anonymous','', current_timestamp, current_timestamp,0);
 
+CREATE SEQUENCE actor_actor_id_seq;
+CREATE TABLE &mw_prefix.actor (
+  actor_id NUMBER NOT NULL,
+  actor_user NUMBER,
+  actor_name VARCHAR2(255) NOT NULL
+);
+
+ALTER TABLE &mw_prefix.actor ADD CONSTRAINT &mw_prefix.actor_pk PRIMARY KEY (actor_id);
+
+/*$mw$*/
+CREATE TRIGGER &mw_prefix.actor_seq_trg BEFORE INSERT ON &mw_prefix.actor
+       FOR EACH ROW WHEN (new.actor_id IS NULL)
+BEGIN
+       &mw_prefix.lastval_pkg.setLastval(actor_actor_id_seq.nextval, :new.actor_id);
+END;
+/*$mw$*/
+
+-- Create a dummy actor to satisfy fk contraints
+INSERT INTO &mw_prefix.actor (actor_id, actor_name) VALUES (0,'##Anonymous##');
+
 CREATE TABLE &mw_prefix.user_groups (
   ug_user   NUMBER      DEFAULT 0 NOT NULL,
   ug_group  VARCHAR2(255)     NOT NULL,
@@ -197,6 +217,17 @@ ALTER TABLE &mw_prefix.revision_comment_temp ADD CONSTRAINT &mw_prefix.revision_
 ALTER TABLE &mw_prefix.revision_comment_temp ADD CONSTRAINT &mw_prefix.revision_comment_temp_fk2 FOREIGN KEY (revcomment_comment_id) REFERENCES &mw_prefix."COMMENT"(comment_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
 CREATE UNIQUE INDEX &mw_prefix.revcomment_rev ON &mw_prefix.revision_comment_temp (revcomment_rev);
 
+CREATE TABLE &mw_prefix.revision_actor_temp (
+  revactor_rev NUMBER NOT NULL,
+  revactor_actor NUMBER NOT NULL,
+  revactor_timestamp TIMESTAMP(6) WITH TIME ZONE NOT NULL,
+  revactor_page NUMBER NOT NULL
+);
+ALTER TABLE &mw_prefix.revision_actor_temp ADD CONSTRAINT &mw_prefix.revision_actor_temp_pk PRIMARY KEY (revactor_rev, revactor_actor);
+CREATE UNIQUE INDEX &mw_prefix.revactor_rev ON &mw_prefix.revision_actor_temp (revactor_rev);
+CREATE INDEX &mw_prefix.actor_timestamp ON &mw_prefix.revision_actor_temp (revactor_actor,revactor_timestamp);
+CREATE INDEX &mw_prefix.page_actor_timestamp ON &mw_prefix.revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp);
+
 CREATE SEQUENCE text_old_id_seq;
 CREATE TABLE &mw_prefix.pagecontent ( -- replaces reserved word 'text'
   old_id     NUMBER  NOT NULL,
@@ -221,7 +252,8 @@ CREATE TABLE &mw_prefix.archive (
   ar_comment     VARCHAR2(255),
   ar_comment_id  NUMBER DEFAULT 0 NOT NULL,
   ar_user        NUMBER          DEFAULT 0 NOT NULL,
-  ar_user_text   VARCHAR2(255)         NOT NULL,
+  ar_user_text   VARCHAR2(255)         NULL,
+  ar_actor       NUMBER          DEFAULT 0 NOT NULL,
   ar_timestamp   TIMESTAMP(6) WITH TIME ZONE  NOT NULL,
   ar_minor_edit  CHAR(1)         DEFAULT '0' NOT NULL,
   ar_flags       VARCHAR2(255),
@@ -240,6 +272,7 @@ ALTER TABLE &mw_prefix.archive ADD CONSTRAINT &mw_prefix.archive_fk1 FOREIGN KEY
 ALTER TABLE &mw_prefix.archive ADD CONSTRAINT &mw_prefix.archive_fk2 FOREIGN KEY (ar_comment_id) REFERENCES &mw_prefix."COMMENT"(comment_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
 CREATE INDEX &mw_prefix.archive_i01 ON &mw_prefix.archive (ar_namespace,ar_title,ar_timestamp);
 CREATE INDEX &mw_prefix.archive_i02 ON &mw_prefix.archive (ar_user_text,ar_timestamp);
+CREATE INDEX &mw_prefix.ar_actor_timestamp ON &mw_prefix.archive (ar_actor,ar_timestamp);
 CREATE INDEX &mw_prefix.archive_i03 ON &mw_prefix.archive (ar_rev_id);
 /*$mw$*/
 CREATE TRIGGER &mw_prefix.archive_seq_trg BEFORE INSERT ON &mw_prefix.archive
@@ -439,6 +472,7 @@ CREATE TABLE &mw_prefix.ipblocks (
   ipb_user              NUMBER      DEFAULT 0 NOT  NULL,
   ipb_by                NUMBER      DEFAULT 0 NOT NULL,
   ipb_by_text           VARCHAR2(255)      NULL,
+  ipb_by_actor          NUMBER      DEFUALT 0 NOT NULL,
   ipb_reason            VARCHAR2(255)      NULL,
   ipb_reason_id         NUMBER DEFAULT 0 NOT NULL,
   ipb_timestamp         TIMESTAMP(6) WITH TIME ZONE  NOT NULL,
@@ -484,7 +518,8 @@ CREATE TABLE &mw_prefix.image (
   img_minor_mime   VARCHAR2(100) DEFAULT 'unknown',
   img_description  VARCHAR2(255),
   img_user         NUMBER       DEFAULT 0 NOT NULL,
-  img_user_text    VARCHAR2(255)      NOT NULL,
+  img_user_text    VARCHAR2(255)      NULL,
+  img_actor        NUMBER       DEFAULT 0 NOT NULL,
   img_timestamp    TIMESTAMP(6) WITH TIME ZONE,
   img_sha1         VARCHAR2(32)
 );
@@ -494,6 +529,7 @@ CREATE INDEX &mw_prefix.image_i01 ON &mw_prefix.image (img_user_text,img_timesta
 CREATE INDEX &mw_prefix.image_i02 ON &mw_prefix.image (img_size);
 CREATE INDEX &mw_prefix.image_i03 ON &mw_prefix.image (img_timestamp);
 CREATE INDEX &mw_prefix.image_i04 ON &mw_prefix.image (img_sha1);
+CREATE INDEX &mw_prefix.img_actor_timestamp ON &mw_prefix.image (img_actor, img_timestamp);
 
 CREATE TABLE &mw_prefix.image_comment_temp (
   imgcomment_name VARCHAR2(255) NOT NULL,
@@ -515,7 +551,8 @@ CREATE TABLE &mw_prefix.oldimage (
   oi_description   VARCHAR2(255),
   oi_description_id  NUMBER DEFAULT 0 NOT NULL,
   oi_user          NUMBER          DEFAULT 0 NOT NULL,
-  oi_user_text     VARCHAR2(255)         NOT NULL,
+  oi_user_text     VARCHAR2(255)         NULL,
+  oi_actor         NUMBER          DEFAULT 0 NOT NULL,
   oi_timestamp     TIMESTAMP(6) WITH TIME ZONE  NOT NULL,
   oi_metadata      CLOB,
   oi_media_type    VARCHAR2(32) DEFAULT NULL,
@@ -528,6 +565,7 @@ ALTER TABLE &mw_prefix.oldimage ADD CONSTRAINT &mw_prefix.oldimage_fk1 FOREIGN K
 ALTER TABLE &mw_prefix.oldimage ADD CONSTRAINT &mw_prefix.oldimage_fk2 FOREIGN KEY (oi_user) REFERENCES &mw_prefix.mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;
 ALTER TABLE &mw_prefix.oldimage ADD CONSTRAINT &mw_prefix.oldimage_fk3 FOREIGN KEY (oi_description_id) REFERENCES &mw_prefix."COMMENT"(comment_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
 CREATE INDEX &mw_prefix.oldimage_i01 ON &mw_prefix.oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX &mw_prefix.oi_actor_timestamp ON &mw_prefix.oldimage (oi_actor,oi_timestamp);
 CREATE INDEX &mw_prefix.oldimage_i02 ON &mw_prefix.oldimage (oi_name,oi_timestamp);
 CREATE INDEX &mw_prefix.oldimage_i03 ON &mw_prefix.oldimage (oi_name,oi_archive_name);
 CREATE INDEX &mw_prefix.oldimage_i04 ON &mw_prefix.oldimage (oi_sha1);
@@ -555,7 +593,8 @@ CREATE TABLE &mw_prefix.filearchive (
   fa_description        VARCHAR2(255),
   fa_description_id     NUMBER DEFAULT 0 NOT NULL,
   fa_user               NUMBER          DEFAULT 0 NOT NULL,
-  fa_user_text          VARCHAR2(255)         NOT NULL,
+  fa_user_text          VARCHAR2(255)         NULL,
+  fa_actor              NUMBER          DEFAULT 0 NOT NULL,
   fa_timestamp          TIMESTAMP(6) WITH TIME ZONE,
   fa_deleted            NUMBER      DEFAULT 0 NOT NULL,
   fa_sha1              VARCHAR2(32)
@@ -569,6 +608,7 @@ CREATE INDEX &mw_prefix.filearchive_i01 ON &mw_prefix.filearchive (fa_name, fa_t
 CREATE INDEX &mw_prefix.filearchive_i02 ON &mw_prefix.filearchive (fa_storage_group, fa_storage_key);
 CREATE INDEX &mw_prefix.filearchive_i03 ON &mw_prefix.filearchive (fa_deleted_timestamp);
 CREATE INDEX &mw_prefix.filearchive_i04 ON &mw_prefix.filearchive (fa_user_text,fa_timestamp);
+CREATE INDEX &mw_prefix.fa_actor_timestamp ON &mw_prefix.filearchive (fa_actor,fa_timestamp);
 CREATE INDEX &mw_prefix.filearchive_i05 ON &mw_prefix.filearchive (fa_sha1);
 /*$mw$*/
 CREATE TRIGGER &mw_prefix.filearchive_seq_trg BEFORE INSERT ON &mw_prefix.filearchive
@@ -617,7 +657,8 @@ CREATE TABLE &mw_prefix.recentchanges (
   rc_timestamp       TIMESTAMP(6) WITH TIME ZONE  NOT NULL,
   rc_cur_time        TIMESTAMP(6) WITH TIME ZONE,
   rc_user            NUMBER          DEFAULT 0 NOT NULL,
-  rc_user_text       VARCHAR2(255)         NOT NULL,
+  rc_user_text       VARCHAR2(255)         NULL,
+  rc_actor           NUMBER          DEFAULT 0 NOT NULL,
   rc_namespace       NUMBER     DEFAULT 0 NOT NULL,
   rc_title           VARCHAR2(255)         NOT NULL,
   rc_comment         VARCHAR2(255),
@@ -651,6 +692,8 @@ CREATE INDEX &mw_prefix.recentchanges_i04 ON &mw_prefix.recentchanges (rc_new,rc
 CREATE INDEX &mw_prefix.recentchanges_i05 ON &mw_prefix.recentchanges (rc_ip);
 CREATE INDEX &mw_prefix.recentchanges_i06 ON &mw_prefix.recentchanges (rc_namespace, rc_user_text);
 CREATE INDEX &mw_prefix.recentchanges_i07 ON &mw_prefix.recentchanges (rc_user_text, rc_timestamp);
+CREATE INDEX &mw_prefix.rc_ns_actor ON &mw_prefix.recentchanges (rc_namespace, rc_actor);
+CREATE INDEX &mw_prefix.rc_actor ON &mw_prefix.recentchanges (rc_actor, rc_timestamp);
 CREATE INDEX &mw_prefix.recentchanges_i08 ON &mw_prefix.recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
 /*$mw$*/
 CREATE TRIGGER &mw_prefix.recentchanges_seq_trg BEFORE INSERT ON &mw_prefix.recentchanges
@@ -721,6 +764,7 @@ CREATE TABLE &mw_prefix.logging (
   log_timestamp   TIMESTAMP(6) WITH TIME ZONE  NOT NULL,
   log_user        NUMBER                DEFAULT 0 NOT NULL,
   log_user_text        VARCHAR2(255),
+  log_actor       NUMBER                DEFAULT 0 NOT NULL,
   log_namespace   NUMBER     DEFAULT 0 NOT NULL,
   log_title       VARCHAR2(255)         NOT NULL,
   log_page                             NUMBER,
@@ -739,6 +783,8 @@ CREATE INDEX &mw_prefix.logging_i04 ON &mw_prefix.logging (log_timestamp);
 CREATE INDEX &mw_prefix.logging_i05 ON &mw_prefix.logging (log_type, log_action, log_timestamp);
 CREATE INDEX &mw_prefix.logging_i06 ON &mw_prefix.logging (log_user_text, log_type, log_timestamp);
 CREATE INDEX &mw_prefix.logging_i07 ON &mw_prefix.logging (log_user_text, log_timestamp);
+CREATE INDEX &mw_prefix.actor_time ON &mw_prefix.logging (log_actor, log_timestamp);
+CREATE INDEX &mw_prefix.log_actor_type_time ON &mw_prefix.logging (log_actor, log_type, log_timestamp);
 /*$mw$*/
 CREATE TRIGGER &mw_prefix.logging_seq_trg BEFORE INSERT ON &mw_prefix.logging
        FOR EACH ROW WHEN (new.log_id IS NULL)
index 945df32..7acf6d8 100644 (file)
@@ -82,16 +82,18 @@ class Orphans extends Maintenance {
                }
 
                $commentQuery = $commentStore->getJoin( 'rev_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'rev_user' );
 
                $this->output( "Checking for orphan revision table entries... "
                        . "(this may take a while on a large wiki)\n" );
                $result = $dbw->select(
-                       [ 'revision', 'page' ] + $commentQuery['tables'],
-                       [ 'rev_id', 'rev_page', 'rev_timestamp', 'rev_user_text' ] + $commentQuery['fields'],
+                       [ 'revision', 'page' ] + $commentQuery['tables'] + $actorQuery['tables'],
+                       [ 'rev_id', 'rev_page', 'rev_timestamp' ] + $commentQuery['fields'] + $actorQuery['fields'],
                        [ 'page_id' => null ],
                        __METHOD__,
                        [],
                        [ 'page' => [ 'LEFT JOIN', [ 'rev_page=page_id' ] ] ] + $commentQuery['joins']
+                               + $actorQuery['joins']
                );
                $orphans = $result->numRows();
                if ( $orphans > 0 ) {
index fbe16de..5174758 100644 (file)
@@ -82,13 +82,19 @@ TEXT
 
                $this->output( "Copying IP revisions to ip_changes, from rev_id $start to rev_id $end\n" );
 
+               $actorMigration = ActorMigration::newMigration();
+               $actorQuery = $actorMigration->getJoin( 'rev_user' );
+               $revUserIsAnon = $actorMigration->isAnon( $actorQuery['fields']['rev_user'] );
+
                while ( $blockStart <= $end ) {
                        $blockEnd = min( $blockStart + $this->getBatchSize(), $end );
                        $rows = $dbr->select(
-                               'revision',
-                               [ 'rev_id', 'rev_timestamp', 'rev_user_text' ],
-                               [ "rev_id BETWEEN " . (int)$blockStart . " AND " . (int)$blockEnd, 'rev_user' => 0 ],
-                               __METHOD__
+                               [ 'revision' ] + $actorQuery['tables'],
+                               [ 'rev_id', 'rev_timestamp', 'rev_user_text' => $actorQuery['fields']['rev_user_text'] ],
+                               [ "rev_id BETWEEN " . (int)$blockStart . " AND " . (int)$blockEnd, $revUserIsAnon ],
+                               __METHOD__,
+                               [],
+                               $actorQuery['joins']
                        );
 
                        $numRows = $rows->numRows();
index f960753..332d7c5 100644 (file)
@@ -53,6 +53,8 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
        }
 
        protected function doDBUpdates() {
+               global $wgActorTableSchemaMigrationStage;
+
                $batchSize = $this->getBatchSize();
                $db = $this->getDB( DB_MASTER );
                if ( !$db->tableExists( 'log_search' ) ) {
@@ -81,8 +83,8 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                'logging', [ 'log_id', 'log_type', 'log_action', 'log_params' ], $cond, __FUNCTION__
                        );
                        foreach ( $res as $row ) {
-                               // RevisionDelete logs - revisions
                                if ( LogEventsList::typeAction( $row, $delTypes, 'revision' ) ) {
+                                       // RevisionDelete logs - revisions
                                        $params = LogPage::extractParams( $row->log_params );
                                        // Param format: <urlparam> <item CSV> [<ofield> <nfield>]
                                        if ( count( $params ) < 2 ) {
@@ -107,30 +109,31 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                        $log = new LogPage( $row->log_type );
                                        // Add item relations...
                                        $log->addRelations( $field, $items, $row->log_id );
-                                       // Determine what table to query...
+                                       // Query item author relations...
                                        $prefix = substr( $field, 0, strpos( $field, '_' ) ); // db prefix
                                        if ( !isset( self::$tableMap[$prefix] ) ) {
                                                continue; // bad row?
                                        }
-                                       $table = self::$tableMap[$prefix];
-                                       $userField = $prefix . '_user';
-                                       $userTextField = $prefix . '_user_text';
-                                       // Add item author relations...
-                                       $userIds = $userIPs = [];
-                                       $sres = $db->select( $table,
-                                               [ $userField, $userTextField ],
-                                               [ $field => $items ]
-                                       );
-                                       foreach ( $sres as $srow ) {
-                                               if ( $srow->$userField > 0 ) {
-                                                       $userIds[] = intval( $srow->$userField );
-                                               } elseif ( $srow->$userTextField != '' ) {
-                                                       $userIPs[] = $srow->$userTextField;
+                                       $tables = [ self::$tableMap[$prefix] ];
+                                       $fields = [];
+                                       $joins = [];
+                                       if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                                               $fields['userid'] = $prefix . '_user';
+                                               $fields['username'] = $prefix . '_user_text';
+                                       }
+                                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                                               if ( $prefix === 'rev' ) {
+                                                       $tables[] = 'revision_actor_temp';
+                                                       $joins['revision_actor_temp'] = [
+                                                               $wgActorTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
+                                                               'rev_id = revactor_rev',
+                                                       ];
+                                                       $fields['actorid'] = 'revactor_actor';
+                                               } else {
+                                                       $fields['actorid'] = $prefix . '_actor';
                                                }
                                        }
-                                       // Add item author relations...
-                                       $log->addRelations( 'target_author_id', $userIds, $row->log_id );
-                                       $log->addRelations( 'target_author_ip', $userIPs, $row->log_id );
+                                       $sres = $db->select( $tables, $fields, [ $field => $items ], __METHOD__, [], $joins );
                                } elseif ( LogEventsList::typeAction( $row, $delTypes, 'event' ) ) {
                                        // RevisionDelete logs - log events
                                        $params = LogPage::extractParams( $row->log_params );
@@ -142,22 +145,49 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                        $log = new LogPage( $row->log_type );
                                        // Add item relations...
                                        $log->addRelations( 'log_id', $items, $row->log_id );
-                                       // Add item author relations...
-                                       $userIds = $userIPs = [];
-                                       $sres = $db->select( 'logging',
-                                               [ 'log_user', 'log_user_text' ],
-                                               [ 'log_id' => $items ]
-                                       );
-                                       foreach ( $sres as $srow ) {
-                                               if ( $srow->log_user > 0 ) {
-                                                       $userIds[] = intval( $srow->log_user );
-                                               } elseif ( IP::isIPAddress( $srow->log_user_text ) ) {
-                                                       $userIPs[] = $srow->log_user_text;
+                                       // Query item author relations...
+                                       $fields = [];
+                                       if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                                               $fields['userid'] = 'log_user';
+                                               $fields['username'] = 'log_user_text';
+                                       }
+                                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                                               $fields['actorid'] = 'log_actor';
+                                       }
+
+                                       $sres = $db->select( 'logging', $fields, [ 'log_id' => $items ], __METHOD__ );
+                               } else {
+                                       continue;
+                               }
+
+                               // Add item author relations...
+                               $userIds = $userIPs = $userActors = [];
+                               foreach ( $sres as $srow ) {
+                                       if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                                               if ( $srow->userid > 0 ) {
+                                                       $userIds[] = intval( $srow->userid );
+                                               } elseif ( $srow->username != '' ) {
+                                                       $userIPs[] = $srow->username;
                                                }
                                        }
+                                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                                               if ( $srow->actorid ) {
+                                                       $userActors[] = intval( $srow->actorid );
+                                               } elseif ( $srow->userid > 0 ) {
+                                                       $userActors[] = User::newFromId( $srow->userid )->getActorId( $db );
+                                               } else {
+                                                       $userActors[] = User::newFromName( $srow->username, false )->getActorId( $db );
+                                               }
+                                       }
+                               }
+                               // Add item author relations...
+                               if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
                                        $log->addRelations( 'target_author_id', $userIds, $row->log_id );
                                        $log->addRelations( 'target_author_ip', $userIPs, $row->log_id );
                                }
+                               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                                       $log->addRelations( 'target_author_actor', $userActors, $row->log_id );
+                               }
                        }
                        $blockStart += $batchSize;
                        $blockEnd += $batchSize;
index 2fe7ea6..55ad536 100644 (file)
@@ -58,6 +58,14 @@ class PopulateLogUsertext extends LoggedUpdateMaintenance {
                }
                $end = $db->selectField( 'logging', 'MAX(log_id)', false, __METHOD__ );
 
+               // If this is being run during an upgrade from 1.16 or earlier, this
+               // will be run before the actor table change and should continue. But
+               // if it's being run on a new installation, the field won't exist to be populated.
+               if ( !$db->fieldInfo( 'logging', 'log_user_text' ) ) {
+                       $this->output( "No log_user_text field, nothing to do.\n" );
+                       return true;
+               }
+
                # Do remaining chunk
                $end += $batchSize - 1;
                $blockStart = $start;
diff --git a/maintenance/postgres/archives/patch-actor-table.sql b/maintenance/postgres/archives/patch-actor-table.sql
new file mode 100644 (file)
index 0000000..68e5d26
--- /dev/null
@@ -0,0 +1,24 @@
+--
+-- patch-actor-table.sql
+--
+-- T167246. Add an `actor` table and various columns (and temporary tables) to reference it.
+
+CREATE SEQUENCE actor_actor_id_seq;
+CREATE TABLE actor (
+  actor_id INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('actor_actor_id_seq'),
+  actor_user INTEGER,
+  actor_name TEXT NOT NULL
+);
+CREATE UNIQUE INDEX actor_user ON actor (actor_user);
+CREATE UNIQUE INDEX actor_name ON actor (actor_name);
+
+CREATE TABLE revision_actor_temp (
+  revactor_rev INTEGER NOT NULL,
+  revactor_actor INTEGER NOT NULL,
+  revactor_timestamp TIMESTAMPTZ  NOT NULL,
+  revactor_page INTEGER          NULL  REFERENCES page (page_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+  PRIMARY KEY (revactor_rev, revactor_actor)
+);
+CREATE UNIQUE INDEX revactor_rev ON revision_actor_temp (revactor_rev);
+CREATE INDEX rev_actor_timestamp ON revision_actor_temp (revactor_actor,revactor_timestamp);
+CREATE INDEX rev_page_actor_timestamp ON revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp);
index 34de2cb..01177d8 100644 (file)
@@ -10,6 +10,7 @@ BEGIN;
 SET client_min_messages = 'ERROR';
 
 DROP SEQUENCE IF EXISTS user_user_id_seq CASCADE;
+DROP SEQUENCE IF EXISTS actor_actor_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS page_page_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS revision_rev_id_seq CASCADE;
 DROP SEQUENCE IF EXISTS comment_comment_id_seq CASCADE;
@@ -58,6 +59,15 @@ CREATE INDEX user_email_token_idx ON mwuser (user_email_token);
 INSERT INTO mwuser
   VALUES (DEFAULT,'Anonymous','',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,now(),now());
 
+CREATE SEQUENCE actor_actor_id_seq;
+CREATE TABLE actor (
+  actor_id      INTEGER  NOT NULL  PRIMARY KEY DEFAULT nextval('actor_actor_id_seq'),
+  actor_user INTEGER,
+  actor_name    TEXT     NOT NULL
+);
+CREATE UNIQUE INDEX actor_user ON actor (actor_user);
+CREATE UNIQUE INDEX actor_name ON actor (actor_name);
+
 CREATE TABLE user_groups (
   ug_user    INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
   ug_group   TEXT         NOT NULL,
@@ -134,8 +144,8 @@ CREATE TABLE revision (
   rev_page           INTEGER          NULL  REFERENCES page (page_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
   rev_text_id        INTEGER          NULL, -- FK
   rev_comment        TEXT         NOT NULL DEFAULT '',
-  rev_user           INTEGER      NOT NULL  REFERENCES mwuser(user_id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
-  rev_user_text      TEXT         NOT NULL,
+  rev_user           INTEGER      NOT NULL DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
+  rev_user_text      TEXT         NOT NULL DEFAULT '',
   rev_timestamp      TIMESTAMPTZ  NOT NULL,
   rev_minor_edit     SMALLINT     NOT NULL  DEFAULT 0,
   rev_deleted        SMALLINT     NOT NULL  DEFAULT 0,
@@ -158,6 +168,17 @@ CREATE TABLE revision_comment_temp (
 );
 CREATE UNIQUE INDEX revcomment_rev ON revision_comment_temp (revcomment_rev);
 
+CREATE TABLE revision_actor_temp (
+  revactor_rev       INTEGER NOT NULL,
+  revactor_actor     INTEGER NOT NULL,
+  revactor_timestamp TIMESTAMPTZ  NOT NULL,
+  revactor_page      INTEGER          NULL  REFERENCES page (page_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+  PRIMARY KEY (revactor_rev, revactor_actor)
+);
+CREATE UNIQUE INDEX revactor_rev ON revision_actor_temp (revactor_rev);
+CREATE INDEX rev_actor_timestamp ON revision_actor_temp (revactor_actor,revactor_timestamp);
+CREATE INDEX rev_page_actor_timestamp ON revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp);
+
 CREATE SEQUENCE ip_changes_ipc_rev_id_seq;
 
 CREATE TABLE ip_changes (
@@ -221,8 +242,9 @@ CREATE TABLE archive (
   ar_sha1           TEXT         NOT NULL DEFAULT '',
   ar_comment        TEXT         NOT NULL DEFAULT '',
   ar_comment_id     INTEGER      NOT NULL DEFAULT 0,
-  ar_user           INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
-  ar_user_text      TEXT         NOT NULL,
+  ar_user           INTEGER      NOT NULL DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+  ar_user_text      TEXT         NOT NULL DEFAULT '',
+  ar_actor          INTEGER      NOT NULL DEFAULT 0,
   ar_timestamp      TIMESTAMPTZ  NOT NULL,
   ar_minor_edit     SMALLINT     NOT NULL  DEFAULT 0,
   ar_flags          TEXT,
@@ -235,6 +257,7 @@ CREATE TABLE archive (
 );
 CREATE INDEX archive_name_title_timestamp ON archive (ar_namespace,ar_title,ar_timestamp);
 CREATE INDEX archive_user_text            ON archive (ar_user_text);
+CREATE INDEX archive_actor                ON archive (ar_actor);
 
 
 CREATE TABLE slots (
@@ -362,8 +385,9 @@ CREATE TABLE ipblocks (
   ipb_id                INTEGER      NOT NULL  PRIMARY KEY DEFAULT nextval('ipblocks_ipb_id_seq'),
   ipb_address           TEXT             NULL,
   ipb_user              INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
-  ipb_by                INTEGER      NOT NULL  REFERENCES mwuser(user_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+  ipb_by                INTEGER      NOT NULL  DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
   ipb_by_text           TEXT         NOT NULL  DEFAULT '',
+  ipb_by_actor          INTEGER      NOT NULL  DEFAULT 0,
   ipb_reason            TEXT         NOT NULL  DEFAULT '',
   ipb_reason_id         INTEGER      NOT NULL  DEFAULT 0,
   ipb_timestamp         TIMESTAMPTZ  NOT NULL,
@@ -397,8 +421,9 @@ CREATE TABLE image (
   img_major_mime   TEXT                DEFAULT 'unknown',
   img_minor_mime   TEXT                DEFAULT 'unknown',
   img_description  TEXT      NOT NULL  DEFAULT '',
-  img_user         INTEGER       NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
-  img_user_text    TEXT      NOT NULL,
+  img_user         INTEGER   NOT NULL  DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+  img_user_text    TEXT      NOT NULL  DEFAULT '',
+  img_actor        INTEGER   NOT NULL  DEFAULT 0,
   img_timestamp    TIMESTAMPTZ,
   img_sha1         TEXT      NOT NULL  DEFAULT ''
 );
@@ -422,8 +447,9 @@ CREATE TABLE oldimage (
   oi_bits          SMALLINT         NULL,
   oi_description   TEXT         NOT NULL DEFAULT '',
   oi_description_id INTEGER     NOT NULL DEFAULT 0,
-  oi_user          INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
-  oi_user_text     TEXT         NOT NULL,
+  oi_user          INTEGER      NOT NULL DEFAULT 0  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+  oi_user_text     TEXT         NOT NULL DEFAULT '',
+  oi_actor         INTEGER      NOT NULL DEFAULT 0,
   oi_timestamp     TIMESTAMPTZ      NULL,
   oi_metadata      BYTEA        NOT NULL DEFAULT '',
   oi_media_type    TEXT             NULL,
@@ -459,8 +485,9 @@ CREATE TABLE filearchive (
   fa_minor_mime         TEXT                   DEFAULT 'unknown',
   fa_description        TEXT         NOT NULL DEFAULT '',
   fa_description_id     INTEGER      NOT NULL DEFAULT 0,
-  fa_user               INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
-  fa_user_text          TEXT         NOT NULL,
+  fa_user               INTEGER      NOT NULL DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+  fa_user_text          TEXT         NOT NULL DEFAULT '',
+  fa_actor              INTEGER      NOT NULL DEFAULT 0,
   fa_timestamp          TIMESTAMPTZ,
   fa_deleted            SMALLINT     NOT NULL DEFAULT 0,
   fa_sha1               TEXT         NOT NULL DEFAULT ''
@@ -504,8 +531,9 @@ CREATE TABLE recentchanges (
   rc_id              INTEGER      NOT NULL  PRIMARY KEY DEFAULT nextval('recentchanges_rc_id_seq'),
   rc_timestamp       TIMESTAMPTZ  NOT NULL,
   rc_cur_time        TIMESTAMPTZ      NULL,
-  rc_user            INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
-  rc_user_text       TEXT         NOT NULL,
+  rc_user            INTEGER      NOT NULL  DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+  rc_user_text       TEXT         NOT NULL  DEFAULT '',
+  rc_actor           INTEGER      NOT NULL  DEFAULT 0,
   rc_namespace       SMALLINT     NOT NULL,
   rc_title           TEXT         NOT NULL,
   rc_comment         TEXT         NOT NULL  DEFAULT '',
@@ -605,7 +633,8 @@ CREATE TABLE logging (
   log_type        TEXT         NOT NULL,
   log_action      TEXT         NOT NULL,
   log_timestamp   TIMESTAMPTZ  NOT NULL,
-  log_user        INTEGER                REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+  log_user        INTEGER      NOT NULL DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+  log_actor       INTEGER      NOT NULL DEFAULT 0,
   log_namespace   SMALLINT     NOT NULL,
   log_title       TEXT         NOT NULL,
   log_comment     TEXT         NOT NULL DEFAULT '',
@@ -617,12 +646,15 @@ CREATE TABLE logging (
 );
 CREATE INDEX logging_type_name ON logging (log_type, log_timestamp);
 CREATE INDEX logging_user_time ON logging (log_timestamp, log_user);
+CREATE INDEX logging_actor_time_backwards ON logging (log_timestamp, log_actor);
 CREATE INDEX logging_page_time ON logging (log_namespace, log_title, log_timestamp);
 CREATE INDEX logging_times ON logging (log_timestamp);
 CREATE INDEX logging_user_type_time ON logging (log_user, log_type, log_timestamp);
+CREATE INDEX logging_actor_type_time ON logging (log_actor, log_type, log_timestamp);
 CREATE INDEX logging_page_id_time ON logging (log_page, log_timestamp);
 CREATE INDEX logging_user_text_type_time ON logging (log_user_text, log_type, log_timestamp);
 CREATE INDEX logging_user_text_time ON logging (log_user_text, log_timestamp);
+CREATE INDEX logging_actor_time ON logging (log_actor, log_timestamp);
 
 CREATE TABLE log_search (
   ls_field   TEXT     NOT NULL,
index 6f88f3d..c91d4ed 100644 (file)
@@ -23,6 +23,8 @@
  * @licence GNU General Public Licence 2.0 or later
  */
 
+use Wikimedia\Rdbms\IDatabase;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -74,27 +76,35 @@ class ReassignEdits extends Maintenance {
         * @return int Number of entries changed, or that would be changed
         */
        private function doReassignEdits( &$from, &$to, $rc = false, $report = false ) {
+               global $wgActorTableSchemaMigrationStage;
+
                $dbw = $this->getDB( DB_MASTER );
                $this->beginTransaction( $dbw, __METHOD__ );
 
                # Count things
                $this->output( "Checking current edits..." );
+               $revQueryInfo = ActorMigration::newMigration()->getWhere( $dbw, 'rev_user', $from );
                $res = $dbw->select(
-                       'revision',
+                       [ 'revision' ] + $revQueryInfo['tables'],
                        'COUNT(*) AS count',
-                       $this->userConditions( $from, 'rev_user', 'rev_user_text' ),
-                       __METHOD__
+                       $revQueryInfo['conds'],
+                       __METHOD__,
+                       [],
+                       $revQueryInfo['joins']
                );
                $row = $dbw->fetchObject( $res );
                $cur = $row->count;
                $this->output( "found {$cur}.\n" );
 
                $this->output( "Checking deleted edits..." );
+               $arQueryInfo = ActorMigration::newMigration()->getWhere( $dbw, 'ar_user', $from, false );
                $res = $dbw->select(
-                       'archive',
+                       [ 'archive' ] + $arQueryInfo['tables'],
                        'COUNT(*) AS count',
-                       $this->userConditions( $from, 'ar_user', 'ar_user_text' ),
-                       __METHOD__
+                       $arQueryInfo['conds'],
+                       __METHOD__,
+                       [],
+                       $arQueryInfo['joins']
                );
                $row = $dbw->fetchObject( $res );
                $del = $row->count;
@@ -103,11 +113,14 @@ class ReassignEdits extends Maintenance {
                # Don't count recent changes if we're not supposed to
                if ( $rc ) {
                        $this->output( "Checking recent changes..." );
+                       $rcQueryInfo = ActorMigration::newMigration()->getWhere( $dbw, 'rc_user', $from, false );
                        $res = $dbw->select(
-                               'recentchanges',
+                               [ 'recentchanges' ] + $rcQueryInfo['tables'],
                                'COUNT(*) AS count',
-                               $this->userConditions( $from, 'rc_user', 'rc_user_text' ),
-                               __METHOD__
+                               $rcQueryInfo['conds'],
+                               __METHOD__,
+                               [],
+                               $rcQueryInfo['joins']
                        );
                        $row = $dbw->fetchObject( $res );
                        $rec = $row->count;
@@ -123,17 +136,38 @@ class ReassignEdits extends Maintenance {
                        if ( $total ) {
                                # Reassign edits
                                $this->output( "\nReassigning current edits..." );
-                               $dbw->update( 'revision', $this->userSpecification( $to, 'rev_user', 'rev_user_text' ),
-                                       $this->userConditions( $from, 'rev_user', 'rev_user_text' ), __METHOD__ );
+                               if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                                       $dbw->update(
+                                               'revision',
+                                               [
+                                                       'rev_user' => $to->getId(),
+                                                       'rev_user_text' =>
+                                                               $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ? $to->getName() : ''
+                                               ],
+                                               $from->isLoggedIn()
+                                                       ? [ 'rev_user' => $from->getId() ] : [ 'rev_user_text' => $from->getName() ],
+                                               __METHOD__
+                                       );
+                               }
+                               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                                       $dbw->update(
+                                               'revision_actor_temp',
+                                               [ 'revactor_actor' => $to->getActorId( $dbw ) ],
+                                               [ 'revactor_actor' => $from->getActorId() ],
+                                               __METHOD__
+                                       );
+                               }
                                $this->output( "done.\nReassigning deleted edits..." );
-                               $dbw->update( 'archive', $this->userSpecification( $to, 'ar_user', 'ar_user_text' ),
-                                       $this->userConditions( $from, 'ar_user', 'ar_user_text' ), __METHOD__ );
+                               $dbw->update( 'archive',
+                                       $this->userSpecification( $dbw, $to, 'ar_user', 'ar_user_text', 'ar_actor' ),
+                                       [ $arQueryInfo['conds'] ], __METHOD__ );
                                $this->output( "done.\n" );
                                # Update recent changes if required
                                if ( $rc ) {
                                        $this->output( "Updating recent changes..." );
-                                       $dbw->update( 'recentchanges', $this->userSpecification( $to, 'rc_user', 'rc_user_text' ),
-                                               $this->userConditions( $from, 'rc_user', 'rc_user_text' ), __METHOD__ );
+                                       $dbw->update( 'recentchanges',
+                                               $this->userSpecification( $dbw, $to, 'rc_user', 'rc_user_text', 'rc_actor' ),
+                                               [ $rcQueryInfo['conds'] ], __METHOD__ );
                                        $this->output( "done.\n" );
                                }
                        }
@@ -144,32 +178,31 @@ class ReassignEdits extends Maintenance {
                return (int)$total;
        }
 
-       /**
-        * Return the most efficient set of user conditions
-        * i.e. a user => id mapping, or a user_text => text mapping
-        *
-        * @param User $user User for the condition
-        * @param string $idfield Field name containing the identifier
-        * @param string $utfield Field name containing the user text
-        * @return array
-        */
-       private function userConditions( &$user, $idfield, $utfield ) {
-               return $user->getId()
-                       ? [ $idfield => $user->getId() ]
-                       : [ $utfield => $user->getName() ];
-       }
-
        /**
         * Return user specifications
         * i.e. user => id, user_text => text
         *
+        * @param IDatabase $dbw Database handle
         * @param User $user User for the spec
         * @param string $idfield Field name containing the identifier
         * @param string $utfield Field name containing the user text
+        * @param string $acfield Field name containing the actor ID
         * @return array
         */
-       private function userSpecification( &$user, $idfield, $utfield ) {
-               return [ $idfield => $user->getId(), $utfield => $user->getName() ];
+       private function userSpecification( IDatabase $dbw, &$user, $idfield, $utfield, $acfield ) {
+               global $wgActorTableSchemaMigrationStage;
+
+               $ret = [];
+               if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                       $ret += [
+                               $idfield => $user->getId(),
+                               $utfield => $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ? $user->getName() : '',
+                       ];
+               }
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       $ret += [ $acfield => $user->getActorId( $dbw ) ];
+               }
+               return $ret;
        }
 
        /**
index 672bae8..6c7dbff 100644 (file)
@@ -116,12 +116,11 @@ class RebuildRecentchanges extends Maintenance {
                $this->output( "Loading from page and revision tables...\n" );
 
                $commentQuery = $commentStore->getJoin( 'rev_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'rev_user' );
                $res = $dbw->select(
-                       [ 'revision', 'page' ] + $commentQuery['tables'],
+                       [ 'revision', 'page' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        [
                                'rev_timestamp',
-                               'rev_user',
-                               'rev_user_text',
                                'rev_minor_edit',
                                'rev_id',
                                'rev_deleted',
@@ -129,7 +128,7 @@ class RebuildRecentchanges extends Maintenance {
                                'page_title',
                                'page_is_new',
                                'page_id'
-                       ] + $commentQuery['fields'],
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
                        [
                                'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
                                'rev_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
@@ -138,19 +137,19 @@ class RebuildRecentchanges extends Maintenance {
                        [ 'ORDER BY' => 'rev_timestamp DESC' ],
                        [
                                'page' => [ 'JOIN', 'rev_page=page_id' ],
-                       ] + $commentQuery['joins']
+                       ] + $commentQuery['joins'] + $actorQuery['joins']
                );
 
                $this->output( "Inserting from page and revision tables...\n" );
                $inserted = 0;
+               $actorMigration = ActorMigration::newMigration();
                foreach ( $res as $row ) {
                        $comment = $commentStore->getComment( 'rev_comment', $row );
+                       $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
                        $dbw->insert(
                                'recentchanges',
                                [
                                        'rc_timestamp' => $row->rev_timestamp,
-                                       'rc_user' => $row->rev_user,
-                                       'rc_user_text' => $row->rev_user_text,
                                        'rc_namespace' => $row->page_namespace,
                                        'rc_title' => $row->page_title,
                                        'rc_minor' => $row->rev_minor_edit,
@@ -162,7 +161,8 @@ class RebuildRecentchanges extends Maintenance {
                                        'rc_type' => $row->page_is_new ? RC_NEW : RC_EDIT,
                                        'rc_source' => $row->page_is_new ? RecentChange::SRC_NEW : RecentChange::SRC_EDIT,
                                        'rc_deleted' => $row->rev_deleted
-                               ] + $commentStore->insert( $dbw, 'rc_comment', $comment ),
+                               ] + $commentStore->insert( $dbw, 'rc_comment', $comment )
+                                       + $actorMigration->getInsertValues( $dbw, 'rc_user', $user ),
                                __METHOD__
                        );
                        if ( ( ++$inserted % $this->getBatchSize() ) == 0 ) {
@@ -274,12 +274,11 @@ class RebuildRecentchanges extends Maintenance {
                $this->output( "Loading from user, page, and logging tables...\n" );
 
                $commentQuery = $commentStore->getJoin( 'log_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
                $res = $dbw->select(
-                       [ 'user', 'logging', 'page' ] + $commentQuery['tables'],
+                       [ 'logging', 'page' ] + $commentQuery['tables'] + $actorQuery['tables'],
                        [
                                'log_timestamp',
-                               'log_user',
-                               'user_name',
                                'log_namespace',
                                'log_title',
                                'page_id',
@@ -288,11 +287,10 @@ class RebuildRecentchanges extends Maintenance {
                                'log_id',
                                'log_params',
                                'log_deleted'
-                       ] + $commentQuery['fields'],
+                       ] + $commentQuery['fields'] + $actorQuery['fields'],
                        [
                                'log_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
                                'log_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
-                               'log_user=user_id',
                                // Some logs don't go in RC since they are private.
                                // @FIXME: core/extensions also have spammy logs that don't go in RC.
                                'log_type' => array_diff( $wgLogTypes, array_keys( $wgLogRestrictions ) ),
@@ -302,20 +300,20 @@ class RebuildRecentchanges extends Maintenance {
                        [
                                'page' =>
                                        [ 'LEFT JOIN', [ 'log_namespace=page_namespace', 'log_title=page_title' ] ]
-                       ] + $commentQuery['joins']
+                       ] + $commentQuery['joins'] + $actorQuery['joins']
                );
 
                $field = $dbw->fieldInfo( 'recentchanges', 'rc_cur_id' );
 
                $inserted = 0;
+               $actorMigration = ActorMigration::newMigration();
                foreach ( $res as $row ) {
                        $comment = $commentStore->getComment( 'log_comment', $row );
+                       $user = User::newFromAnyId( $row->log_user, $row->log_user_text, $row->log_actor );
                        $dbw->insert(
                                'recentchanges',
                                [
                                        'rc_timestamp' => $row->log_timestamp,
-                                       'rc_user' => $row->log_user,
-                                       'rc_user_text' => $row->user_name,
                                        'rc_namespace' => $row->log_namespace,
                                        'rc_title' => $row->log_title,
                                        'rc_minor' => 0,
@@ -334,7 +332,8 @@ class RebuildRecentchanges extends Maintenance {
                                        'rc_logid' => $row->log_id,
                                        'rc_params' => $row->log_params,
                                        'rc_deleted' => $row->log_deleted
-                               ] + $commentStore->insert( $dbw, 'rc_comment', $comment ),
+                               ] + $commentStore->insert( $dbw, 'rc_comment', $comment )
+                                       + $actorMigration->getInsertValues( $dbw, 'rc_user', $user ),
                                __METHOD__
                        );
 
@@ -352,8 +351,7 @@ class RebuildRecentchanges extends Maintenance {
 
                $dbw = $this->getDB( DB_MASTER );
 
-               list( $recentchanges, $usergroups, $user ) =
-                       $dbw->tableNamesN( 'recentchanges', 'user_groups', 'user' );
+               $userQuery = User::getQueryInfo();
 
                # @FIXME: recognize other bot account groups (not the same as users with 'bot' rights)
                # @NOTE: users with 'bot' rights choose when edits are bot edits or not. That information
@@ -363,32 +361,42 @@ class RebuildRecentchanges extends Maintenance {
 
                # Flag our recent bot edits
                if ( $botgroups ) {
-                       $botwhere = $dbw->makeList( $botgroups );
-
                        $this->output( "Flagging bot account edits...\n" );
 
                        # Find all users that are bots
-                       $sql = "SELECT DISTINCT user_name FROM $usergroups, $user " .
-                               "WHERE ug_group IN($botwhere) AND user_id = ug_user";
-                       $res = $dbw->query( $sql, __METHOD__ );
+                       $res = $dbw->select(
+                               array_merge( [ 'user_groups' ], $userQuery['tables'] ),
+                               $userQuery['fields'],
+                               [ 'ug_group' => $botgroups ],
+                               __METHOD__,
+                               [ 'DISTINCT' ],
+                               [ 'user_group' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
+                       );
 
                        $botusers = [];
                        foreach ( $res as $obj ) {
-                               $botusers[] = $obj->user_name;
+                               $botusers[] = User::newFromRow( $obj );
                        }
 
                        # Fill in the rc_bot field
                        if ( $botusers ) {
-                               $rcids = $dbw->selectFieldValues(
-                                       'recentchanges',
-                                       'rc_id',
-                                       [
-                                               'rc_user_text' => $botusers,
-                                               "rc_timestamp > " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
-                                               "rc_timestamp < " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
-                                       ],
-                                       __METHOD__
-                               );
+                               $actorQuery = ActorMigration::newMigration()->getWhere( $dbw, 'rc_user', $botusers, false );
+                               $rcids = [];
+                               foreach ( $actorQuery['orconds'] as $cond ) {
+                                       $rcids = array_merge( $rcids, $dbw->selectFieldValues(
+                                               [ 'recentchanges' ] + $actorQuery['tables'],
+                                               'rc_id',
+                                               [
+                                                       "rc_timestamp > " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
+                                                       "rc_timestamp < " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
+                                                       $cond,
+                                               ],
+                                               __METHOD__,
+                                               [],
+                                               $actorQuery['joins']
+                                       ) );
+                               }
+                               $rcids = array_values( array_unique( $rcids ) );
 
                                foreach ( array_chunk( $rcids, $this->getBatchSize() ) as $rcidBatch ) {
                                        $dbw->update(
@@ -404,28 +412,40 @@ class RebuildRecentchanges extends Maintenance {
 
                # Flag our recent autopatrolled edits
                if ( !$wgMiserMode && $autopatrolgroups ) {
-                       $patrolwhere = $dbw->makeList( $autopatrolgroups );
                        $patrolusers = [];
 
                        $this->output( "Flagging auto-patrolled edits...\n" );
 
                        # Find all users in RC with autopatrol rights
-                       $sql = "SELECT DISTINCT user_name FROM $usergroups, $user " .
-                               "WHERE ug_group IN($patrolwhere) AND user_id = ug_user";
-                       $res = $dbw->query( $sql, __METHOD__ );
+                       $res = $dbw->select(
+                               array_merge( [ 'user_groups' ], $userQuery['tables'] ),
+                               $userQuery['fields'],
+                               [ 'ug_group' => $autopatrolgroups ],
+                               __METHOD__,
+                               [ 'DISTINCT' ],
+                               [ 'user_group' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
+                       );
 
                        foreach ( $res as $obj ) {
-                               $patrolusers[] = $dbw->addQuotes( $obj->user_name );
+                               $patrolusers[] = User::newFromRow( $obj );
                        }
 
                        # Fill in the rc_patrolled field
                        if ( $patrolusers ) {
-                               $patrolwhere = implode( ',', $patrolusers );
-                               $sql2 = "UPDATE $recentchanges SET rc_patrolled=1 " .
-                                       "WHERE rc_user_text IN($patrolwhere) " .
-                                       "AND rc_timestamp > " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ) . ' ' .
-                                       "AND rc_timestamp < " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) );
-                               $dbw->query( $sql2 );
+                               $actorQuery = ActorMigration::newMigration()->getWhere( $dbw, 'rc_user', $patrolusers, false );
+                               foreach ( $actorQuery['orconds'] as $cond ) {
+                                       $dbw->update(
+                                               'recentchanges',
+                                               [ 'rc_patrolled' => 1 ],
+                                               [
+                                                       $cond,
+                                                       'rc_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
+                                                       'rc_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
+                                               ],
+                                               __METHOD__
+                                       );
+                                       wfGetLBFactory()->waitForReplication();
+                               }
                        }
                }
        }
index 0910c9c..e066300 100644 (file)
@@ -39,13 +39,27 @@ class RemoveUnusedAccounts extends Maintenance {
        }
 
        public function execute() {
+               global $wgActorTableSchemaMigrationStage;
+
                $this->output( "Remove unused accounts\n\n" );
 
                # Do an initial scan for inactive accounts and report the result
                $this->output( "Checking for unused user accounts...\n" );
-               $del = [];
+               $delUser = [];
+               $delActor = [];
                $dbr = $this->getDB( DB_REPLICA );
-               $res = $dbr->select( 'user', [ 'user_id', 'user_name', 'user_touched' ], '', __METHOD__ );
+               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                       $res = $dbr->select(
+                               [ 'user', 'actor' ],
+                               [ 'user_id', 'user_name', 'user_touched', 'actor_id' ],
+                               '',
+                               __METHOD__,
+                               [],
+                               [ 'actor' => [ 'LEFT JOIN', 'user_id = actor_user' ] ]
+                       );
+               } else {
+                       $res = $dbr->select( 'user', [ 'user_id', 'user_name', 'user_touched' ], '', __METHOD__ );
+               }
                if ( $this->hasOption( 'ignore-groups' ) ) {
                        $excludedGroups = explode( ',', $this->getOption( 'ignore-groups' ) );
                } else {
@@ -61,27 +75,49 @@ class RemoveUnusedAccounts extends Maintenance {
                        # group or if it's touched within the $touchedSeconds seconds.
                        $instance = User::newFromId( $row->user_id );
                        if ( count( array_intersect( $instance->getEffectiveGroups(), $excludedGroups ) ) == 0
-                               && $this->isInactiveAccount( $row->user_id, true )
+                               && $this->isInactiveAccount( $row->user_id, $row->actor_id, true )
                                && wfTimestamp( TS_UNIX, $row->user_touched ) < wfTimestamp( TS_UNIX, time() - $touchedSeconds )
                        ) {
                                # Inactive; print out the name and flag it
-                               $del[] = $row->user_id;
+                               $delUser[] = $row->user_id;
+                               if ( $row->actor_id ) {
+                                       $delActor[] = $row->actor_id;
+                               }
                                $this->output( $row->user_name . "\n" );
                        }
                }
-               $count = count( $del );
+               $count = count( $delUser );
                $this->output( "...found {$count}.\n" );
 
                # If required, go back and delete each marked account
                if ( $count > 0 && $this->hasOption( 'delete' ) ) {
                        $this->output( "\nDeleting unused accounts..." );
                        $dbw = $this->getDB( DB_MASTER );
-                       $dbw->delete( 'user', [ 'user_id' => $del ], __METHOD__ );
-                       $dbw->delete( 'user_groups', [ 'ug_user' => $del ], __METHOD__ );
-                       $dbw->delete( 'user_former_groups', [ 'ufg_user' => $del ], __METHOD__ );
-                       $dbw->delete( 'user_properties', [ 'up_user' => $del ], __METHOD__ );
-                       $dbw->delete( 'logging', [ 'log_user' => $del ], __METHOD__ );
-                       $dbw->delete( 'recentchanges', [ 'rc_user' => $del ], __METHOD__ );
+                       $dbw->delete( 'user', [ 'user_id' => $delUser ], __METHOD__ );
+                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                               # Keep actor rows referenced from ipblocks
+                               $keep = $dbw->selectFieldValues(
+                                       'ipblocks', 'ipb_by_actor', [ 'ipb_by_actor' => $delActor ], __METHOD__
+                               );
+                               $del = array_diff( $delActor, $keep );
+                               if ( $del ) {
+                                       $dbw->delete( 'actor', [ 'actor_id' => $del ], __METHOD__ );
+                               }
+                               if ( $keep ) {
+                                       $dbw->update( 'actor', [ 'actor_user' => 0 ], [ 'actor_id' => $keep ], __METHOD__ );
+                               }
+                       }
+                       $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 ) {
+                               $dbw->delete( 'logging', [ 'log_actor' => $delActor ], __METHOD__ );
+                               $dbw->delete( 'recentchanges', [ 'rc_actor' => $delActor ], __METHOD__ );
+                       }
+                       if ( $wgActorTableSchemaMigrationStage < MIGRATION_NEW ) {
+                               $dbw->delete( 'logging', [ 'log_user' => $delUser ], __METHOD__ );
+                               $dbw->delete( 'recentchanges', [ 'rc_user' => $delUser ], __METHOD__ );
+                       }
                        $this->output( "done.\n" );
                        # Update the site_stats.ss_users field
                        $users = $dbw->selectField( 'user', 'COUNT(*)', [], __METHOD__ );
@@ -102,10 +138,11 @@ class RemoveUnusedAccounts extends Maintenance {
         * (No edits, no deleted edits, no log entries, no current/old uploads)
         *
         * @param int $id User's ID
+        * @param int $actor User's actor ID
         * @param bool $master Perform checking on the master
         * @return bool
         */
-       private function isInactiveAccount( $id, $master = false ) {
+       private function isInactiveAccount( $id, $actor, $master = false ) {
                $dbo = $this->getDB( $master ? DB_MASTER : DB_REPLICA );
                $checks = [
                        'revision' => 'rev',
@@ -116,14 +153,37 @@ class RemoveUnusedAccounts extends Maintenance {
                ];
                $count = 0;
 
+               $migration = ActorMigration::newMigration();
+
+               $user = User::newFromAnyId( $id, null, $actor );
+
                $this->beginTransaction( $dbo, __METHOD__ );
-               foreach ( $checks as $table => $fprefix ) {
-                       $conds = [ $fprefix . '_user' => $id ];
-                       $count += (int)$dbo->selectField( $table, 'COUNT(*)', $conds, __METHOD__ );
+               foreach ( $checks as $table => $prefix ) {
+                       $actorQuery = $migration->getWhere(
+                               $dbo, $prefix . '_user', $user, $prefix !== 'oi' && $prefix !== 'fa'
+                       );
+                       $count += (int)$dbo->selectField(
+                               [ $table ] + $actorQuery['tables'],
+                               'COUNT(*)',
+                               $actorQuery['conds'],
+                               __METHOD__,
+                               [],
+                               $actorQuery['joins']
+                       );
                }
 
-               $conds = [ 'log_user' => $id, 'log_type != ' . $dbo->addQuotes( 'newusers' ) ];
-               $count += (int)$dbo->selectField( 'logging', 'COUNT(*)', $conds, __METHOD__ );
+               $actorQuery = $migration->getWhere( $dbo, 'log_user', $user, false );
+               $count += (int)$dbo->selectField(
+                       [ 'logging' ] + $actorQuery['tables'],
+                       'COUNT(*)',
+                       [
+                               $actorQuery['conds'],
+                               'log_type != ' . $dbo->addQuotes( 'newusers' )
+                       ],
+                       __METHOD__,
+                       [],
+                       $actorQuery['joins']
+               );
 
                $this->commitTransaction( $dbo, __METHOD__ );
 
index 34bc62b..878eb9b 100644 (file)
@@ -91,20 +91,26 @@ class RollbackEdits extends Maintenance {
 
        /**
         * Get all pages that should be rolled back for a given user
-        * @param string $user A name to check against rev_user_text
+        * @param string $user A name to check against
         * @return array
         */
        private function getRollbackTitles( $user ) {
                $dbr = $this->getDB( DB_REPLICA );
                $titles = [];
-               $results = $dbr->select(
-                       [ 'page', 'revision' ],
-                       [ 'page_namespace', 'page_title' ],
-                       [ 'page_latest = rev_id', 'rev_user_text' => $user ],
-                       __METHOD__
-               );
-               foreach ( $results as $row ) {
-                       $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
+               $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 );
+                       }
                }
 
                return $titles;
diff --git a/maintenance/sqlite/archives/patch-actor-table.sql b/maintenance/sqlite/archives/patch-actor-table.sql
new file mode 100644 (file)
index 0000000..bf15a04
--- /dev/null
@@ -0,0 +1,371 @@
+--
+-- patch-actor-table.sql
+--
+-- T167246. Add an `actor` table and various columns (and temporary tables) to reference it.
+-- Sigh, sqlite, such trouble just to change the default value of a column.
+
+CREATE TABLE /*_*/actor (
+  actor_id bigint unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  actor_user int unsigned,
+  actor_name varchar(255) binary NOT NULL
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/actor_user ON /*_*/actor (actor_user);
+CREATE UNIQUE INDEX /*i*/actor_name ON /*_*/actor (actor_name);
+
+CREATE TABLE /*_*/revision_actor_temp (
+  revactor_rev int unsigned NOT NULL,
+  revactor_actor bigint unsigned NOT NULL,
+  revactor_timestamp binary(14) NOT NULL default '',
+  revactor_page int unsigned NOT NULL,
+  PRIMARY KEY (revactor_rev, revactor_actor)
+) /*$wgDBTableOptions*/;
+CREATE UNIQUE INDEX /*i*/revactor_rev ON /*_*/revision_actor_temp (revactor_rev);
+CREATE INDEX /*i*/actor_timestamp ON /*_*/revision_actor_temp (revactor_actor,revactor_timestamp);
+CREATE INDEX /*i*/page_actor_timestamp ON /*_*/revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp);
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/archive_tmp;
+CREATE TABLE /*_*/archive_tmp (
+  ar_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  ar_namespace int NOT NULL default 0,
+  ar_title varchar(255) binary NOT NULL default '',
+  ar_text mediumblob NOT NULL,
+  ar_comment varbinary(767) NOT NULL default '',
+  ar_comment_id bigint unsigned NOT NULL DEFAULT 0,
+  ar_user int unsigned NOT NULL default 0,
+  ar_user_text varchar(255) binary NOT NULL DEFAULT '',
+  ar_actor bigint unsigned NOT NULL DEFAULT 0,
+  ar_timestamp binary(14) NOT NULL default '',
+  ar_minor_edit tinyint NOT NULL default 0,
+  ar_flags tinyblob NOT NULL,
+  ar_rev_id int unsigned,
+  ar_text_id int unsigned,
+  ar_deleted tinyint unsigned NOT NULL default 0,
+  ar_len int unsigned,
+  ar_page_id int unsigned,
+  ar_parent_id int unsigned default NULL,
+  ar_sha1 varbinary(32) NOT NULL default '',
+  ar_content_model varbinary(32) DEFAULT NULL,
+  ar_content_format varbinary(64) DEFAULT NULL
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/archive_tmp (
+       ar_id, ar_namespace, ar_title, ar_text, ar_comment, ar_user, ar_user_text,
+       ar_timestamp, ar_minor_edit, ar_flags, ar_rev_id, ar_text_id, ar_deleted,
+       ar_len, ar_page_id, ar_parent_id, ar_sha1, ar_content_model,
+       ar_content_format)
+  SELECT
+       ar_id, ar_namespace, ar_title, ar_text, ar_comment, ar_user, ar_user_text,
+       ar_timestamp, ar_minor_edit, ar_flags, ar_rev_id, ar_text_id, ar_deleted,
+       ar_len, ar_page_id, ar_parent_id, ar_sha1, ar_content_model,
+       ar_content_format
+  FROM /*_*/archive;
+
+DROP TABLE /*_*/archive;
+ALTER TABLE /*_*/archive_tmp RENAME TO /*_*/archive;
+CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
+CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp);
+CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id);
+CREATE INDEX /*i*/ar_actor_timestamp ON /*_*/archive (ar_actor,ar_timestamp);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS ipblocks_tmp;
+CREATE TABLE /*_*/ipblocks_tmp (
+  ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  ipb_address tinyblob NOT NULL,
+  ipb_user int unsigned NOT NULL default 0,
+  ipb_by int unsigned NOT NULL default 0,
+  ipb_by_text varchar(255) binary NOT NULL default '',
+  ipb_by_actor bigint unsigned NOT NULL DEFAULT 0,
+  ipb_reason varbinary(767) NOT NULL default '',
+  ipb_reason_id bigint unsigned NOT NULL DEFAULT 0,
+  ipb_timestamp binary(14) NOT NULL default '',
+  ipb_auto bool NOT NULL default 0,
+  ipb_anon_only bool NOT NULL default 0,
+  ipb_create_account bool NOT NULL default 1,
+  ipb_enable_autoblock bool NOT NULL default '1',
+  ipb_expiry varbinary(14) NOT NULL default '',
+  ipb_range_start tinyblob NOT NULL,
+  ipb_range_end tinyblob NOT NULL,
+  ipb_deleted bool NOT NULL default 0,
+  ipb_block_email bool NOT NULL default 0,
+  ipb_allow_usertalk bool NOT NULL default 0,
+  ipb_parent_block_id int default NULL
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/ipblocks_tmp (
+       ipb_id, ipb_address, ipb_user, ipb_by, ipb_by_text, ipb_reason,
+       ipb_timestamp, ipb_auto, ipb_anon_only, ipb_create_account,
+       ipb_enable_autoblock, ipb_expiry, ipb_range_start, ipb_range_end,
+       ipb_deleted, ipb_block_email, ipb_allow_usertalk, ipb_parent_block_id)
+  SELECT
+       ipb_id, ipb_address, ipb_user, ipb_by, ipb_by_text, ipb_reason,
+       ipb_timestamp, ipb_auto, ipb_anon_only, ipb_create_account,
+       ipb_enable_autoblock, ipb_expiry, ipb_range_start, ipb_range_end,
+       ipb_deleted, ipb_block_email, ipb_allow_usertalk, ipb_parent_block_id
+  FROM /*_*/ipblocks;
+
+DROP TABLE /*_*/ipblocks;
+ALTER TABLE /*_*/ipblocks_tmp RENAME TO /*_*/ipblocks;
+CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only);
+CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user);
+CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8));
+CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp);
+CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry);
+CREATE INDEX /*i*/ipb_parent_block_id ON /*_*/ipblocks (ipb_parent_block_id);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/image_tmp;
+CREATE TABLE /*_*/image_tmp (
+  img_name varchar(255) binary NOT NULL default '' PRIMARY KEY,
+  img_size int unsigned NOT NULL default 0,
+  img_width int NOT NULL default 0,
+  img_height int NOT NULL default 0,
+  img_metadata mediumblob NOT NULL,
+  img_bits int NOT NULL default 0,
+  img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+  img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+  img_minor_mime varbinary(100) NOT NULL default "unknown",
+  img_description varbinary(767) NOT NULL default '',
+  img_user int unsigned NOT NULL default 0,
+  img_user_text varchar(255) binary NOT NULL DEFAULT '',
+  img_actor bigint unsigned NOT NULL DEFAULT 0,
+  img_timestamp varbinary(14) NOT NULL default '',
+  img_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/image_tmp (
+       img_name, img_size, img_width, img_height, img_metadata, img_bits,
+       img_media_type, img_major_mime, img_minor_mime, img_description, img_user,
+       img_user_text, img_timestamp, img_sha1)
+  SELECT
+       img_name, img_size, img_width, img_height, img_metadata, img_bits,
+       img_media_type, img_major_mime, img_minor_mime, img_description, img_user,
+       img_user_text, img_timestamp, img_sha1
+  FROM /*_*/image;
+
+DROP TABLE /*_*/image;
+ALTER TABLE /*_*/image_tmp RENAME TO /*_*/image;
+CREATE INDEX /*i*/img_user_timestamp ON /*_*/image (img_user,img_timestamp);
+CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+CREATE INDEX /*i*/img_actor_timestamp ON /*_*/image (img_actor,img_timestamp);
+CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
+CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
+CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10));
+CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/oldimage_tmp;
+CREATE TABLE /*_*/oldimage_tmp (
+  oi_name varchar(255) binary NOT NULL default '',
+  oi_archive_name varchar(255) binary NOT NULL default '',
+  oi_size int unsigned NOT NULL default 0,
+  oi_width int NOT NULL default 0,
+  oi_height int NOT NULL default 0,
+  oi_bits int NOT NULL default 0,
+  oi_description varbinary(767) NOT NULL default '',
+  oi_description_id bigint unsigned NOT NULL DEFAULT 0,
+  oi_user int unsigned NOT NULL default 0,
+  oi_user_text varchar(255) binary NOT NULL DEFAULT '',
+  oi_actor bigint unsigned NOT NULL DEFAULT 0,
+  oi_timestamp binary(14) NOT NULL default '',
+  oi_metadata mediumblob NOT NULL,
+  oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+  oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+  oi_minor_mime varbinary(100) NOT NULL default "unknown",
+  oi_deleted tinyint unsigned NOT NULL default 0,
+  oi_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/oldimage_tmp (
+       oi_name, oi_archive_name, oi_size, oi_width, oi_height, oi_bits,
+       oi_description, oi_user, oi_user_text, oi_timestamp, oi_metadata,
+       oi_media_type, oi_major_mime, oi_minor_mime, oi_deleted, oi_sha1)
+  SELECT
+       oi_name, oi_archive_name, oi_size, oi_width, oi_height, oi_bits,
+       oi_description, oi_user, oi_user_text, oi_timestamp, oi_metadata,
+       oi_media_type, oi_major_mime, oi_minor_mime, oi_deleted, oi_sha1
+  FROM /*_*/oldimage;
+
+DROP TABLE /*_*/oldimage;
+ALTER TABLE /*_*/oldimage_tmp RENAME TO /*_*/oldimage;
+CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
+CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
+CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1(10));
+CREATE INDEX /*i*/oi_actor_timestamp ON /*_*/oldimage (oi_actor,oi_timestamp);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/filearchive_tmp;
+CREATE TABLE /*_*/filearchive_tmp (
+  fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  fa_name varchar(255) binary NOT NULL default '',
+  fa_archive_name varchar(255) binary default '',
+  fa_storage_group varbinary(16),
+  fa_storage_key varbinary(64) default '',
+  fa_deleted_user int,
+  fa_deleted_timestamp binary(14) default '',
+  fa_deleted_reason varbinary(767) default '',
+  fa_deleted_reason_id bigint unsigned NOT NULL DEFAULT 0,
+  fa_size int unsigned default 0,
+  fa_width int default 0,
+  fa_height int default 0,
+  fa_metadata mediumblob,
+  fa_bits int default 0,
+  fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
+  fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") default "unknown",
+  fa_minor_mime varbinary(100) default "unknown",
+  fa_description varbinary(767) default '',
+  fa_description_id bigint unsigned NOT NULL DEFAULT 0,
+  fa_user int unsigned default 0,
+  fa_user_text varchar(255) binary DEFAULT '',
+  fa_actor bigint unsigned NOT NULL DEFAULT 0,
+  fa_timestamp binary(14) default '',
+  fa_deleted tinyint unsigned NOT NULL default 0,
+  fa_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/filearchive_tmp (
+       fa_id, fa_name, fa_archive_name, fa_storage_group, fa_storage_key,
+       fa_deleted_user, fa_deleted_timestamp, fa_deleted_reason, fa_size,
+       fa_width, fa_height, fa_metadata, fa_bits, fa_media_type, fa_major_mime,
+       fa_minor_mime, fa_description, fa_user, fa_user_text, fa_timestamp,
+       fa_deleted, fa_sha1)
+  SELECT
+       fa_id, fa_name, fa_archive_name, fa_storage_group, fa_storage_key,
+       fa_deleted_user, fa_deleted_timestamp, fa_deleted_reason, fa_size,
+       fa_width, fa_height, fa_metadata, fa_bits, fa_media_type, fa_major_mime,
+       fa_minor_mime, fa_description, fa_user, fa_user_text, fa_timestamp,
+       fa_deleted, fa_sha1
+  FROM /*_*/filearchive;
+
+DROP TABLE /*_*/filearchive;
+ALTER TABLE /*_*/filearchive_tmp RENAME TO /*_*/filearchive;
+CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp);
+CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key);
+CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
+CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1(10));
+CREATE INDEX /*i*/fa_actor_timestamp ON /*_*/filearchive (fa_actor,fa_timestamp);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/logging_tmp;
+CREATE TABLE /*_*/logging_tmp (
+  log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  log_type varbinary(32) NOT NULL default '',
+  log_action varbinary(32) NOT NULL default '',
+  log_timestamp binary(14) NOT NULL default '19700101000000',
+  log_user int unsigned NOT NULL default 0,
+  log_user_text varchar(255) binary NOT NULL default '',
+  log_actor bigint unsigned NOT NULL DEFAULT 0,
+  log_namespace int NOT NULL default 0,
+  log_title varchar(255) binary NOT NULL default '',
+  log_page int unsigned NULL,
+  log_comment varbinary(767) NOT NULL default '',
+  log_comment_id bigint unsigned NOT NULL DEFAULT 0,
+  log_params blob NOT NULL,
+  log_deleted tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/logging_tmp (
+       log_id, log_type, log_action, log_timestamp, log_user, log_user_text,
+       log_namespace, log_title, log_page, log_comment, log_comment_id,
+       log_params, log_deleted)
+  SELECT
+       log_id, log_type, log_action, log_timestamp, log_user, log_user_text,
+       log_namespace, log_title, log_page, log_comment, log_comment_id,
+       log_params, log_deleted
+  FROM /*_*/logging;
+
+DROP TABLE /*_*/logging;
+ALTER TABLE /*_*/logging_tmp RENAME TO /*_*/logging;
+CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp);
+CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp);
+CREATE INDEX /*i*/actor_time ON /*_*/logging (log_actor, log_timestamp);
+CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp);
+CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp);
+CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp);
+CREATE INDEX /*i*/log_actor_type_time ON /*_*/logging (log_actor, log_type, log_timestamp);
+CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp);
+CREATE INDEX /*i*/type_action ON /*_*/logging (log_type, log_action, log_timestamp);
+CREATE INDEX /*i*/log_user_text_type_time ON /*_*/logging (log_user_text, log_type, log_timestamp);
+CREATE INDEX /*i*/log_user_text_time ON /*_*/logging (log_user_text, log_timestamp);
+
+COMMIT;
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/recentchanges_tmp;
+CREATE TABLE /*_*/recentchanges_tmp (
+  rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  rc_timestamp varbinary(14) NOT NULL default '',
+  rc_user int unsigned NOT NULL default 0,
+  rc_user_text varchar(255) binary NOT NULL DEFAULT '',
+  rc_actor bigint unsigned NOT NULL DEFAULT 0,
+  rc_namespace int NOT NULL default 0,
+  rc_title varchar(255) binary NOT NULL default '',
+  rc_comment varbinary(767) NOT NULL default '',
+  rc_comment_id bigint unsigned NOT NULL DEFAULT 0,
+  rc_minor tinyint unsigned NOT NULL default 0,
+  rc_bot tinyint unsigned NOT NULL default 0,
+  rc_new tinyint unsigned NOT NULL default 0,
+  rc_cur_id int unsigned NOT NULL default 0,
+  rc_this_oldid int unsigned NOT NULL default 0,
+  rc_last_oldid int unsigned NOT NULL default 0,
+  rc_type tinyint unsigned NOT NULL default 0,
+  rc_source varchar(16) binary not null default '',
+  rc_patrolled tinyint unsigned NOT NULL default 0,
+  rc_ip varbinary(40) NOT NULL default '',
+  rc_old_len int,
+  rc_new_len int,
+  rc_deleted tinyint unsigned NOT NULL default 0,
+  rc_logid int unsigned NOT NULL default 0,
+  rc_log_type varbinary(255) NULL default NULL,
+  rc_log_action varbinary(255) NULL default NULL,
+  rc_params blob NULL
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/recentchanges_tmp (
+       rc_id, rc_timestamp, rc_user, rc_user_text, rc_namespace, rc_title,
+       rc_comment, rc_comment_id, rc_minor, rc_bot, rc_new, rc_cur_id,
+       rc_this_oldid, rc_last_oldid, rc_type, rc_source, rc_patrolled, rc_ip,
+       rc_old_len, rc_new_len, rc_deleted, rc_logid, rc_log_type, rc_log_action,
+       rc_params)
+  SELECT
+       rc_id, rc_timestamp, rc_user, rc_user_text, rc_namespace, rc_title,
+       rc_comment, rc_comment_id, rc_minor, rc_bot, rc_new, rc_cur_id,
+       rc_this_oldid, rc_last_oldid, rc_type, rc_source, rc_patrolled, rc_ip,
+       rc_old_len, rc_new_len, rc_deleted, rc_logid, rc_log_type, rc_log_action,
+       rc_params
+  FROM /*_*/recentchanges;
+
+DROP TABLE /*_*/recentchanges;
+ALTER TABLE /*_*/recentchanges_tmp RENAME TO /*_*/recentchanges;
+CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp);
+CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title);
+CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id);
+CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp);
+CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip);
+CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text);
+CREATE INDEX /*i*/rc_ns_actor ON /*_*/recentchanges (rc_namespace, rc_actor);
+CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp);
+CREATE INDEX /*i*/rc_actor ON /*_*/recentchanges (rc_actor, rc_timestamp);
+CREATE INDEX /*i*/rc_name_type_patrolled_timestamp ON /*_*/recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
+
+COMMIT;
index b881d7e..d633a9c 100644 (file)
@@ -140,6 +140,28 @@ CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token);
 CREATE INDEX /*i*/user_email ON /*_*/user (user_email(50));
 
 
+--
+-- The "actor" table associates user names or IP addresses with integers for
+-- the benefit of other tables that need to refer to either logged-in or
+-- logged-out users. If something can only ever be done by logged-in users, it
+-- can refer to the user table directly.
+--
+CREATE TABLE /*_*/actor (
+  -- Unique ID to identify each actor
+  actor_id bigint unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+
+  -- Key to user.user_id, or NULL for anonymous edits.
+  actor_user int unsigned,
+
+  -- Text username or IP address
+  actor_name varchar(255) binary NOT NULL
+) /*$wgDBTableOptions*/;
+
+-- User IDs and names must be unique.
+CREATE UNIQUE INDEX /*i*/actor_user ON /*_*/actor (actor_user);
+CREATE UNIQUE INDEX /*i*/actor_name ON /*_*/actor (actor_name);
+
+
 --
 -- User permissions have been broken out to a separate table;
 -- this allows sites with a shared user table to have different
@@ -351,9 +373,11 @@ CREATE TABLE /*_*/revision (
 
   -- Key to user.user_id of the user who made this edit.
   -- Stores 0 for anonymous edits and for some mass imports.
+  -- Deprecated in favor of revision_actor_temp.revactor_actor.
   rev_user int unsigned NOT NULL default 0,
 
   -- Text username or IP address of the editor.
+  -- Deprecated in favor of revision_actor_temp.revactor_actor.
   rev_user_text varchar(255) binary NOT NULL default '',
 
   -- Timestamp of when revision was created
@@ -425,6 +449,29 @@ CREATE TABLE /*_*/revision_comment_temp (
 -- Ensure uniqueness
 CREATE UNIQUE INDEX /*i*/revcomment_rev ON /*_*/revision_comment_temp (revcomment_rev);
 
+--
+-- Temporary table to avoid blocking on an alter of revision.
+--
+-- On large wikis like the English Wikipedia, altering the revision table is a
+-- months-long process. This table is being created to avoid such an alter, and
+-- will be merged back into revision in the future.
+--
+CREATE TABLE /*_*/revision_actor_temp (
+  -- Key to rev_id
+  revactor_rev int unsigned NOT NULL,
+  -- Key to actor_id
+  revactor_actor bigint unsigned NOT NULL,
+  -- Copy fields from revision for indexes
+  revactor_timestamp binary(14) NOT NULL default '',
+  revactor_page int unsigned NOT NULL,
+  PRIMARY KEY (revactor_rev, revactor_actor)
+) /*$wgDBTableOptions*/;
+-- Ensure uniqueness
+CREATE UNIQUE INDEX /*i*/revactor_rev ON /*_*/revision_actor_temp (revactor_rev);
+-- Match future indexes on revision
+CREATE INDEX /*i*/actor_timestamp ON /*_*/revision_actor_temp (revactor_actor,revactor_timestamp);
+CREATE INDEX /*i*/page_actor_timestamp ON /*_*/revision_actor_temp (revactor_page,revactor_actor,revactor_timestamp);
+
 --
 -- Every time an edit by a logged out user is saved,
 -- a row is created in ip_changes. This stores
@@ -547,8 +594,9 @@ CREATE TABLE /*_*/archive (
   -- Basic revision stuff...
   ar_comment varbinary(767) NOT NULL default '', -- Deprecated in favor of ar_comment_id
   ar_comment_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that ar_comment should be used)
-  ar_user int unsigned NOT NULL default 0,
-  ar_user_text varchar(255) binary NOT NULL,
+  ar_user int unsigned NOT NULL default 0, -- Deprecated in favor of ar_actor
+  ar_user_text varchar(255) binary NOT NULL DEFAULT '', -- Deprecated in favor of ar_actor
+  ar_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that ar_user/ar_user_text should be used)
   ar_timestamp binary(14) NOT NULL default '',
   ar_minor_edit tinyint NOT NULL default 0,
 
@@ -606,6 +654,7 @@ CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar
 
 -- Index for Special:DeletedContributions
 CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp);
+CREATE INDEX /*i*/ar_actor_timestamp ON /*_*/archive (ar_actor,ar_timestamp);
 
 -- Index for linking archive rows with tables that normally link with revision
 -- rows, such as change_tag.
@@ -975,10 +1024,13 @@ CREATE TABLE /*_*/ipblocks (
   ipb_user int unsigned NOT NULL default 0,
 
   -- User ID who made the block.
-  ipb_by int unsigned NOT NULL default 0,
+  ipb_by int unsigned NOT NULL default 0, -- Deprecated in favor of ipb_by_actor
 
   -- User name of blocker
-  ipb_by_text varchar(255) binary NOT NULL default '',
+  ipb_by_text varchar(255) binary NOT NULL default '', -- Deprecated in favor of ipb_by_actor
+
+  -- Actor who made the block.
+  ipb_by_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that ipb_by/ipb_by_text should be used)
 
   -- Text comment made by blocker. Deprecated in favor of ipb_reason_id
   ipb_reason varbinary(767) NOT NULL default '',
@@ -1096,8 +1148,13 @@ CREATE TABLE /*_*/image (
   img_description varbinary(767) NOT NULL default '',
 
   -- user_id and user_name of uploader.
+  -- Deprecated in favor of img_actor.
   img_user int unsigned NOT NULL default 0,
-  img_user_text varchar(255) binary NOT NULL,
+  img_user_text varchar(255) binary NOT NULL DEFAULT '',
+
+  -- actor_id of the uploader.
+  -- ("DEFAULT 0" is temporary, signaling that img_user/img_user_text should be used)
+  img_actor bigint unsigned NOT NULL DEFAULT 0,
 
   -- Time of the upload.
   img_timestamp varbinary(14) NOT NULL default '',
@@ -1109,6 +1166,7 @@ CREATE TABLE /*_*/image (
 -- Used by Special:Newimages and ApiQueryAllImages
 CREATE INDEX /*i*/img_user_timestamp ON /*_*/image (img_user,img_timestamp);
 CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+CREATE INDEX /*i*/img_actor_timestamp ON /*_*/image (img_actor,img_timestamp);
 -- Used by Special:ListFiles for sort-by-size
 CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
 -- Used by Special:Newimages and Special:ListFiles
@@ -1156,8 +1214,9 @@ CREATE TABLE /*_*/oldimage (
   oi_bits int NOT NULL default 0,
   oi_description varbinary(767) NOT NULL default '', -- Deprecated.
   oi_description_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that oi_description should be used)
-  oi_user int unsigned NOT NULL default 0,
-  oi_user_text varchar(255) binary NOT NULL,
+  oi_user int unsigned NOT NULL default 0, -- Deprecated in favor of oi_actor
+  oi_user_text varchar(255) binary NOT NULL DEFAULT '', -- Deprecated in favor of oi_actor
+  oi_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that oi_user/oi_user_text should be used)
   oi_timestamp binary(14) NOT NULL default '',
 
   oi_metadata mediumblob NOT NULL,
@@ -1169,6 +1228,7 @@ CREATE TABLE /*_*/oldimage (
 ) /*$wgDBTableOptions*/;
 
 CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_actor_timestamp ON /*_*/oldimage (oi_actor,oi_timestamp);
 CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
 -- oi_archive_name truncated to 14 to avoid key length overflow
 CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
@@ -1217,8 +1277,9 @@ CREATE TABLE /*_*/filearchive (
   fa_minor_mime varbinary(100) default "unknown",
   fa_description varbinary(767) default '', -- Deprecated
   fa_description_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that fa_description should be used)
-  fa_user int unsigned default 0,
-  fa_user_text varchar(255) binary,
+  fa_user int unsigned default 0, -- Deprecated in favor of fa_actor
+  fa_user_text varchar(255) binary DEFAULT '', -- Deprecated in favor of fa_actor
+  fa_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that fa_user/fa_user_text should be used)
   fa_timestamp binary(14) default '',
 
   -- Visibility of deleted revisions, bitfield
@@ -1236,6 +1297,7 @@ CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_sto
 CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
 -- sort by uploader
 CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+CREATE INDEX /*i*/fa_actor_timestamp ON /*_*/filearchive (fa_actor,fa_timestamp);
 -- find file by sha1, 10 bytes will be enough for hashes to be indexed
 CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1(10));
 
@@ -1306,8 +1368,9 @@ CREATE TABLE /*_*/recentchanges (
   rc_timestamp varbinary(14) NOT NULL default '',
 
   -- As in revision
-  rc_user int unsigned NOT NULL default 0,
-  rc_user_text varchar(255) binary NOT NULL,
+  rc_user int unsigned NOT NULL default 0, -- Deprecated in favor of rc_actor
+  rc_user_text varchar(255) binary NOT NULL DEFAULT '', -- Deprecated in favor of rc_actor
+  rc_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that rc_user/rc_user_text should be used)
 
   -- When pages are renamed, their RC entries do _not_ change.
   rc_namespace int NOT NULL default 0,
@@ -1390,9 +1453,11 @@ CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip);
 
 -- Probably intended for Special:NewPages namespace filter
 CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text);
+CREATE INDEX /*i*/rc_ns_actor ON /*_*/recentchanges (rc_namespace, rc_actor);
 
 -- SiteStats active user count, Special:ActiveUsers, Special:NewPages user filter
 CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp);
+CREATE INDEX /*i*/rc_actor ON /*_*/recentchanges (rc_actor, rc_timestamp);
 
 -- ApiQueryRecentChanges (T140108)
 CREATE INDEX /*i*/rc_name_type_patrolled_timestamp ON /*_*/recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
@@ -1532,10 +1597,13 @@ CREATE TABLE /*_*/logging (
   log_timestamp binary(14) NOT NULL default '19700101000000',
 
   -- The user who performed this action; key to user_id
-  log_user int unsigned NOT NULL default 0,
+  log_user int unsigned NOT NULL default 0, -- Deprecated in favor of log_actor
 
   -- Name of the user who performed this action
-  log_user_text varchar(255) binary NOT NULL default '',
+  log_user_text varchar(255) binary NOT NULL default '', -- Deprecated in favor of log_actor
+
+  -- The actor who performed this action
+  log_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that log_user/log_user_text should be used)
 
   -- Key to the page affected. Where a user is the target,
   -- this will point to the user page.
@@ -1564,6 +1632,7 @@ CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp);
 
 -- Special:Log performer filter
 CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp);
+CREATE INDEX /*i*/actor_time ON /*_*/logging (log_actor, log_timestamp);
 
 -- Special:Log title filter, log extract
 CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp);
@@ -1573,6 +1642,7 @@ CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp);
 
 -- Special:Log filter by performer and type
 CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp);
+CREATE INDEX /*i*/log_actor_type_time ON /*_*/logging (log_actor, log_type, log_timestamp);
 
 -- Apparently just used for a few maintenance pages (findMissingFiles.php, Flow).
 -- Could be removed?
index 4f6f6b6..d4b1f91 100644 (file)
@@ -1148,7 +1148,7 @@ class ParserTestRunner {
         * @return array
         */
        private function listTables() {
-               global $wgCommentTableSchemaMigrationStage;
+               global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
 
                $tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions',
                        'protected_titles', 'revision', 'ip_changes', 'text', 'pagelinks', 'imagelinks',
@@ -1166,6 +1166,12 @@ class ParserTestRunner {
                        $tables[] = 'image_comment_temp';
                }
 
+               if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                       // The new tables for actors are in use
+                       $tables[] = 'actor';
+                       $tables[] = 'revision_actor_temp';
+               }
+
                if ( in_array( $this->db->getType(), [ 'mysql', 'sqlite', 'oracle' ] ) ) {
                        array_push( $tables, 'searchindex' );
                }
index 652b1ee..92c0714 100644 (file)
@@ -1435,8 +1435,9 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
         */
        private function resetDB( $db, $tablesUsed ) {
                if ( $db ) {
-                       $userTables = [ 'user', 'user_groups', 'user_properties' ];
-                       $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment' ];
+                       $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
+                       $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp',
+                               'revision_actor_temp', 'comment' ];
                        $coreDBDataTables = array_merge( $userTables, $pageTables );
 
                        // If any of the user or page tables were marked as used, we should clear all of them.
diff --git a/tests/phpunit/includes/ActorMigrationTest.php b/tests/phpunit/includes/ActorMigrationTest.php
new file mode 100644 (file)
index 0000000..1b0c848
--- /dev/null
@@ -0,0 +1,695 @@
+<?php
+
+use MediaWiki\User\UserIdentity;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Database
+ * @covers ActorMigration
+ */
+class ActorMigrationTest extends MediaWikiLangTestCase {
+
+       protected $tablesUsed = [
+               'revision',
+               'revision_actor_temp',
+               'ipblocks',
+               'recentchanges',
+               'actor',
+       ];
+
+       /**
+        * Create an ActorMigration for a particular stage
+        * @param int $stage
+        * @return ActorMigration
+        */
+       protected function makeMigration( $stage ) {
+               return new ActorMigration( $stage );
+       }
+
+       /**
+        * @dataProvider provideGetJoin
+        * @param int $stage
+        * @param string $key
+        * @param array $expect
+        */
+       public function testGetJoin( $stage, $key, $expect ) {
+               $m = $this->makeMigration( $stage );
+               $result = $m->getJoin( $key );
+               $this->assertEquals( $expect, $result );
+       }
+
+       public static function provideGetJoin() {
+               return [
+                       'Simple table, old' => [
+                               MIGRATION_OLD, 'rc_user', [
+                                       'tables' => [],
+                                       'fields' => [
+                                               'rc_user' => 'rc_user',
+                                               'rc_user_text' => 'rc_user_text',
+                                               'rc_actor' => 'NULL',
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Simple table, write-both' => [
+                               MIGRATION_WRITE_BOTH, '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_actor' => 'rc_actor',
+                                       ],
+                                       'joins' => [
+                                               'actor_rc_user' => [ 'LEFT JOIN', 'actor_rc_user.actor_id = rc_actor' ],
+                                       ],
+                               ],
+                       ],
+                       'Simple table, write-new' => [
+                               MIGRATION_WRITE_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_actor' => 'rc_actor',
+                                       ],
+                                       'joins' => [
+                                               'actor_rc_user' => [ 'LEFT JOIN', 'actor_rc_user.actor_id = rc_actor' ],
+                                       ],
+                               ],
+                       ],
+                       'Simple table, new' => [
+                               MIGRATION_NEW, 'rc_user', [
+                                       'tables' => [ 'actor_rc_user' => 'actor' ],
+                                       'fields' => [
+                                               'rc_user' => 'actor_rc_user.actor_user',
+                                               'rc_user_text' => 'actor_rc_user.actor_name',
+                                               'rc_actor' => 'rc_actor',
+                                       ],
+                                       'joins' => [
+                                               'actor_rc_user' => [ 'JOIN', 'actor_rc_user.actor_id = rc_actor' ],
+                                       ],
+                               ],
+                       ],
+
+                       'ipblocks, old' => [
+                               MIGRATION_OLD, 'ipb_by', [
+                                       'tables' => [],
+                                       'fields' => [
+                                               'ipb_by' => 'ipb_by',
+                                               'ipb_by_text' => 'ipb_by_text',
+                                               'ipb_by_actor' => 'NULL',
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'ipblocks, write-both' => [
+                               MIGRATION_WRITE_BOTH, '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',
+                                       ],
+                                       'joins' => [
+                                               'actor_ipb_by' => [ 'LEFT JOIN', 'actor_ipb_by.actor_id = ipb_by_actor' ],
+                                       ],
+                               ],
+                       ],
+                       'ipblocks, write-new' => [
+                               MIGRATION_WRITE_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',
+                                       ],
+                                       'joins' => [
+                                               'actor_ipb_by' => [ 'LEFT JOIN', 'actor_ipb_by.actor_id = ipb_by_actor' ],
+                                       ],
+                               ],
+                       ],
+                       'ipblocks, new' => [
+                               MIGRATION_NEW, 'ipb_by', [
+                                       'tables' => [ 'actor_ipb_by' => 'actor' ],
+                                       'fields' => [
+                                               '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' => [ 'JOIN', 'actor_ipb_by.actor_id = ipb_by_actor' ],
+                                       ],
+                               ],
+                       ],
+
+                       'Revision, old' => [
+                               MIGRATION_OLD, 'rev_user', [
+                                       'tables' => [],
+                                       'fields' => [
+                                               'rev_user' => 'rev_user',
+                                               'rev_user_text' => 'rev_user_text',
+                                               'rev_actor' => 'NULL',
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Revision, write-both' => [
+                               MIGRATION_WRITE_BOTH, '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_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' ],
+                                       ],
+                               ],
+                       ],
+                       'Revision, write-new' => [
+                               MIGRATION_WRITE_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_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' ],
+                                       ],
+                               ],
+                       ],
+                       'Revision, new' => [
+                               MIGRATION_NEW, 'rev_user', [
+                                       'tables' => [
+                                               'temp_rev_user' => 'revision_actor_temp',
+                                               'actor_rev_user' => 'actor',
+                                       ],
+                                       'fields' => [
+                                               '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' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
+                                               'actor_rev_user' => [ 'JOIN', 'actor_rev_user.actor_id = temp_rev_user.revactor_actor' ],
+                                       ],
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetWhere
+        * @param int $stage
+        * @param string $key
+        * @param UserIdentity[] $users
+        * @param bool $useId
+        * @param array $expect
+        */
+       public function testGetWhere( $stage, $key, $users, $useId, $expect ) {
+               $expect['conds'] = '(' . implode( ') OR (', $expect['orconds'] ) . ')';
+
+               if ( count( $users ) === 1 ) {
+                       $users = reset( $users );
+               }
+
+               $m = $this->makeMigration( $stage );
+               $result = $m->getWhere( $this->db, $key, $users, $useId );
+               $this->assertEquals( $expect, $result );
+       }
+
+       public function provideGetWhere() {
+               $makeUserIdentity = function ( $id, $name, $actor ) {
+                       $u = $this->getMock( UserIdentity::class );
+                       $u->method( 'getId' )->willReturn( $id );
+                       $u->method( 'getName' )->willReturn( $name );
+                       $u->method( 'getActorId' )->willReturn( $actor );
+                       return $u;
+               };
+
+               $genericUser = [ $makeUserIdentity( 1, 'User1', 11 ) ];
+               $complicatedUsers = [
+                       $makeUserIdentity( 1, 'User1', 11 ),
+                       $makeUserIdentity( 2, 'User2', 12 ),
+                       $makeUserIdentity( 3, 'User3', 0 ),
+                       $makeUserIdentity( 0, '192.168.12.34', 34 ),
+                       $makeUserIdentity( 0, '192.168.12.35', 0 ),
+               ];
+
+               return [
+                       'Simple table, old' => [
+                               MIGRATION_OLD, 'rc_user', $genericUser, true, [
+                                       'tables' => [],
+                                       'orconds' => [ 'userid' => "rc_user = '1'" ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Simple table, write-both' => [
+                               MIGRATION_WRITE_BOTH, 'rc_user', $genericUser, true, [
+                                       'tables' => [],
+                                       'orconds' => [
+                                               'actor' => "rc_actor = '11'",
+                                               'userid' => "rc_actor = '0' AND rc_user = '1'"
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Simple table, write-new' => [
+                               MIGRATION_WRITE_NEW, 'rc_user', $genericUser, true, [
+                                       'tables' => [],
+                                       'orconds' => [
+                                               'actor' => "rc_actor = '11'",
+                                               'userid' => "rc_actor = '0' AND rc_user = '1'"
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Simple table, new' => [
+                               MIGRATION_NEW, 'rc_user', $genericUser, true, [
+                                       'tables' => [],
+                                       'orconds' => [ 'actor' => "rc_actor = '11'" ],
+                                       'joins' => [],
+                               ],
+                       ],
+
+                       'ipblocks, old' => [
+                               MIGRATION_OLD, 'ipb_by', $genericUser, true, [
+                                       'tables' => [],
+                                       'orconds' => [ 'userid' => "ipb_by = '1'" ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'ipblocks, write-both' => [
+                               MIGRATION_WRITE_BOTH, 'ipb_by', $genericUser, true, [
+                                       'tables' => [],
+                                       'orconds' => [
+                                               'actor' => "ipb_by_actor = '11'",
+                                               'userid' => "ipb_by_actor = '0' AND ipb_by = '1'"
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'ipblocks, write-new' => [
+                               MIGRATION_WRITE_NEW, 'ipb_by', $genericUser, true, [
+                                       'tables' => [],
+                                       'orconds' => [
+                                               'actor' => "ipb_by_actor = '11'",
+                                               'userid' => "ipb_by_actor = '0' AND ipb_by = '1'"
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'ipblocks, new' => [
+                               MIGRATION_NEW, 'ipb_by', $genericUser, true, [
+                                       'tables' => [],
+                                       'orconds' => [ 'actor' => "ipb_by_actor = '11'" ],
+                                       'joins' => [],
+                               ],
+                       ],
+
+                       'Revision, old' => [
+                               MIGRATION_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, write-new' => [
+                               MIGRATION_WRITE_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'"
+                                       ],
+                                       'joins' => [
+                                               'temp_rev_user' => [ 'LEFT JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
+                                       ],
+                               ],
+                       ],
+                       'Revision, new' => [
+                               MIGRATION_NEW, 'rev_user', $genericUser, true, [
+                                       'tables' => [
+                                               'temp_rev_user' => 'revision_actor_temp',
+                                       ],
+                                       'orconds' => [ 'actor' => "temp_rev_user.revactor_actor = '11'" ],
+                                       'joins' => [
+                                               'temp_rev_user' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
+                                       ],
+                               ],
+                       ],
+
+                       'Multiple users, old' => [
+                               MIGRATION_OLD, 'rc_user', $complicatedUsers, true, [
+                                       'tables' => [],
+                                       'orconds' => [
+                                               'userid' => "rc_user IN ('1','2','3') ",
+                                               'username' => "rc_user_text IN ('192.168.12.34','192.168.12.35') "
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Multiple users, write-both' => [
+                               MIGRATION_WRITE_BOTH, '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') "
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Multiple users, write-new' => [
+                               MIGRATION_WRITE_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') "
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Multiple users, new' => [
+                               MIGRATION_NEW, 'rc_user', $complicatedUsers, true, [
+                                       'tables' => [],
+                                       'orconds' => [ 'actor' => "rc_actor IN ('11','12','34') " ],
+                                       'joins' => [],
+                               ],
+                       ],
+
+                       'Multiple users, no use ID, old' => [
+                               MIGRATION_OLD, 'rc_user', $complicatedUsers, false, [
+                                       'tables' => [],
+                                       'orconds' => [
+                                               'username' => "rc_user_text IN ('User1','User2','User3','192.168.12.34','192.168.12.35') "
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Multiple users, write-both' => [
+                               MIGRATION_WRITE_BOTH, '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') "
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Multiple users, write-new' => [
+                               MIGRATION_WRITE_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') "
+                                       ],
+                                       'joins' => [],
+                               ],
+                       ],
+                       'Multiple users, new' => [
+                               MIGRATION_NEW, 'rc_user', $complicatedUsers, false, [
+                                       'tables' => [],
+                                       'orconds' => [ 'actor' => "rc_actor IN ('11','12','34') " ],
+                                       'joins' => [],
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideInsertRoundTrip
+        * @param string $table
+        * @param string $key
+        * @param string $pk
+        * @param array $extraFields
+        */
+       public function testInsertRoundTrip( $table, $key, $pk, $extraFields ) {
+               $u = $this->getTestUser()->getUser();
+               $user = $this->getMock( UserIdentity::class );
+               $user->method( 'getId' )->willReturn( $u->getId() );
+               $user->method( 'getName' )->willReturn( $u->getName() );
+               if ( $u->getActorId( $this->db ) ) {
+                       $user->method( 'getActorId' )->willReturn( $u->getActorId() );
+               } else {
+                       $this->db->insert(
+                               'actor',
+                               [ 'actor_user' => $u->getId(), 'actor_name' => $u->getName() ],
+                               __METHOD__
+                       );
+                       $user->method( 'getActorId' )->willReturn( $this->db->insertId() );
+               }
+
+               $stages = [
+                       MIGRATION_OLD => [ MIGRATION_OLD, MIGRATION_WRITE_NEW ],
+                       MIGRATION_WRITE_BOTH => [ MIGRATION_OLD, MIGRATION_NEW ],
+                       MIGRATION_WRITE_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ],
+                       MIGRATION_NEW => [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ],
+               ];
+
+               $nameKey = $key . '_text';
+               $actorKey = $key === 'ipb_by' ? 'ipb_by_actor' : substr( $key, 0, -5 ) . '_actor';
+
+               foreach ( $stages as $writeStage => $readRange ) {
+                       if ( $key === 'ipb_by' ) {
+                               $extraFields['ipb_address'] = __CLASS__ . "#$writeStage";
+                       }
+
+                       $w = $this->makeMigration( $writeStage );
+                       $usesTemp = $key === 'rev_user';
+
+                       if ( $usesTemp ) {
+                               list( $fields, $callback ) = $w->getInsertValuesWithTempTable( $this->db, $key, $user );
+                       } else {
+                               $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" );
+                       } else {
+                               $this->assertArrayNotHasKey( $key, $fields, "old field, stage=$writeStage" );
+                               $this->assertArrayNotHasKey( $nameKey, $fields, "old field, stage=$writeStage" );
+                       }
+                       if ( $writeStage >= MIGRATION_WRITE_BOTH && !$usesTemp ) {
+                               $this->assertSame( $user->getActorId(), $fields[$actorKey], "new field, stage=$writeStage" );
+                       } else {
+                               $this->assertArrayNotHasKey( $actorKey, $fields, "new field, stage=$writeStage" );
+                       }
+
+                       $this->db->insert( $table, $extraFields + $fields, __METHOD__ );
+                       $id = $this->db->insertId();
+                       if ( $usesTemp ) {
+                               $callback( $id, $extraFields );
+                       }
+
+                       for ( $readStage = $readRange[0]; $readStage <= $readRange[1]; $readStage++ ) {
+                               $r = $this->makeMigration( $readStage );
+
+                               $queryInfo = $r->getJoin( $key );
+                               $row = $this->db->selectRow(
+                                       [ $table ] + $queryInfo['tables'],
+                                       $queryInfo['fields'],
+                                       [ $pk => $id ],
+                                       __METHOD__,
+                                       [],
+                                       $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(
+                                       $readStage === MIGRATION_OLD || $writeStage === MIGRATION_OLD ? 0 : $user->getActorId(),
+                                       (int)$row->$actorKey,
+                                       "w=$writeStage, r=$readStage, actor"
+                               );
+                       }
+               }
+       }
+
+       public static function provideInsertRoundTrip() {
+               $db = wfGetDB( DB_REPLICA ); // for timestamps
+
+               $ipbfields = [
+               ];
+               $revfields = [
+               ];
+
+               return [
+                       'recentchanges' => [ 'recentchanges', 'rc_user', 'rc_id', [
+                               'rc_timestamp' => $db->timestamp(),
+                               'rc_namespace' => 0,
+                               'rc_title' => 'Test',
+                               'rc_this_oldid' => 42,
+                               'rc_last_oldid' => 41,
+                               'rc_source' => 'test',
+                       ] ],
+                       'ipblocks' => [ 'ipblocks', 'ipb_by', 'ipb_id', [
+                               'ipb_range_start' => '',
+                               'ipb_range_end' => '',
+                               'ipb_timestamp' => $db->timestamp(),
+                               'ipb_expiry' => $db->getInfinity(),
+                       ] ],
+                       'revision' => [ 'revision', 'rev_user', 'rev_id', [
+                               'rev_page' => 42,
+                               'rev_text_id' => 42,
+                               'rev_len' => 0,
+                               'rev_timestamp' => $db->timestamp(),
+                       ] ],
+               ];
+       }
+
+       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 ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideStages
+        * @param int $stage
+        * @expectedException InvalidArgumentException
+        * @expectedExceptionMessage Must use getInsertValuesWithTempTable() for rev_user
+        */
+       public function testInsertWrong( $stage ) {
+               $m = $this->makeMigration( $stage );
+               $m->getInsertValues( $this->db, 'rev_user', $this->getTestUser()->getUser() );
+       }
+
+       /**
+        * @dataProvider provideStages
+        * @param int $stage
+        * @expectedException InvalidArgumentException
+        * @expectedExceptionMessage Must use getInsertValues() for rc_user
+        */
+       public function testInsertWithTempTableWrong( $stage ) {
+               $m = $this->makeMigration( $stage );
+               $m->getInsertValuesWithTempTable( $this->db, 'rc_user', $this->getTestUser()->getUser() );
+       }
+
+       /**
+        * @dataProvider provideStages
+        * @param int $stage
+        */
+       public function testInsertWithTempTableDeprecated( $stage ) {
+               $wrap = TestingAccessWrapper::newFromClass( ActorMigration::class );
+               $wrap->formerTempTables += [ 'rc_user' => '1.30' ];
+
+               $this->hideDeprecated( 'ActorMigration::getInsertValuesWithTempTable for rc_user' );
+               $m = $this->makeMigration( $stage );
+               list( $fields, $callback )
+                       = $m->getInsertValuesWithTempTable( $this->db, 'rc_user', $this->getTestUser()->getUser() );
+               $this->assertTrue( is_callable( $callback ) );
+       }
+
+       /**
+        * @dataProvider provideStages
+        * @param int $stage
+        * @expectedException InvalidArgumentException
+        * @expectedExceptionMessage $extra[rev_timestamp] is not provided
+        */
+       public function testInsertWithTempTableCallbackMissingFields( $stage ) {
+               $m = $this->makeMigration( $stage );
+               list( $fields, $callback )
+                       = $m->getInsertValuesWithTempTable( $this->db, 'rev_user', $this->getTestUser()->getUser() );
+               $callback( 1, [] );
+       }
+
+       public function testInsertUserIdentity() {
+               $user = $this->getTestUser()->getUser();
+               $userIdentity = $this->getMock( UserIdentity::class );
+               $userIdentity->method( 'getId' )->willReturn( $user->getId() );
+               $userIdentity->method( 'getName' )->willReturn( $user->getName() );
+               $userIdentity->method( 'getActorId' )->willReturn( 0 );
+
+               list( $cFields, $cCallback ) = CommentStore::newKey( 'rev_comment' )
+                       ->insertWithTempTable( $this->db, '' );
+               $m = $this->makeMigration( MIGRATION_WRITE_BOTH );
+               list( $fields, $callback ) =
+                       $m->getInsertValuesWithTempTable( $this->db, 'rev_user', $userIdentity );
+               $extraFields = [
+                       'rev_page' => 42,
+                       'rev_text_id' => 42,
+                       'rev_len' => 0,
+                       'rev_timestamp' => $this->db->timestamp(),
+               ] + $cFields;
+               $this->db->insert( 'revision', $extraFields + $fields, __METHOD__ );
+               $id = $this->db->insertId();
+               $callback( $id, $extraFields );
+               $cCallback( $id );
+
+               $qi = Revision::getQueryInfo();
+               $row = $this->db->selectRow(
+                       $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 );
+               $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() {
+               $m = ActorMigration::newMigration();
+               $this->assertInstanceOf( ActorMigration::class, $m );
+               $this->assertSame( $m, ActorMigration::newMigration() );
+       }
+
+       /**
+        * @dataProvider provideIsAnon
+        * @param int $stage
+        * @param string $isAnon
+        * @param string $isNotAnon
+        */
+       public function testIsAnon( $stage, $isAnon, $isNotAnon ) {
+               $m = $this->makeMigration( $stage );
+               $this->assertSame( $isAnon, $m->isAnon( 'foo' ) );
+               $this->assertSame( $isNotAnon, $m->isNotAnon( 'foo' ) );
+       }
+
+       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' ],
+               ];
+       }
+
+}
index 1e46555..19780a6 100644 (file)
@@ -35,6 +35,7 @@ class BlockTest extends MediaWikiLangTestCase {
                $blockOptions = [
                        'address' => $user->getName(),
                        'user' => $user->getId(),
+                       'by' => $this->getTestSysop()->getUser()->getId(),
                        'reason' => 'Parce que',
                        'expiry' => time() + 100500,
                ];
@@ -393,7 +394,7 @@ class BlockTest extends MediaWikiLangTestCase {
                $block = new Block(
                        /* address */ $username,
                        /* user */ 0,
-                       /* by */ 0,
+                       /* by */ $this->getTestSysop()->getUser()->getId(),
                        /* reason */ $reason,
                        /* timestamp */ 0,
                        /* auto */ false,
index e332cdd..a510897 100644 (file)
@@ -542,7 +542,6 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                $ipbfields = [
                        'ipb_range_start' => '',
                        'ipb_range_end' => '',
-                       'ipb_by' => 0,
                        'ipb_timestamp' => $db->timestamp(),
                        'ipb_expiry' => $db->getInfinity(),
                ];
@@ -550,8 +549,6 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                        'rev_page' => 42,
                        'rev_text_id' => 42,
                        'rev_len' => 0,
-                       'rev_user' => 0,
-                       'rev_user_text' => '',
                        'rev_timestamp' => $db->timestamp(),
                ];
                $comStoreComment = new CommentStoreComment(
index 6fbe053..7fdc3ed 100644 (file)
@@ -50,6 +50,10 @@ class PageArchiveTest extends MediaWikiTestCase {
        protected function setUp() {
                parent::setUp();
 
+               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->overrideMwServices();
+
                // First create our dummy page
                $page = Title::newFromText( 'PageArchiveTest_thePage' );
                $page = new WikiPage( $page );
@@ -84,28 +88,44 @@ class PageArchiveTest extends MediaWikiTestCase {
        public function testUndeleteRevisions() {
                // First make sure old revisions are archived
                $dbr = wfGetDB( DB_REPLICA );
-               $res = $dbr->select( 'archive', '*', [ 'ar_rev_id' => $this->ipRevId ] );
+               $arQuery = Revision::getArchiveQueryInfo();
+               $res = $dbr->select(
+                       $arQuery['tables'],
+                       $arQuery['fields'],
+                       [ 'ar_rev_id' => $this->ipRevId ],
+                       __METHOD__,
+                       [],
+                       $arQuery['joins']
+               );
                $row = $res->fetchObject();
                $this->assertEquals( $this->ipEditor, $row->ar_user_text );
 
                // Should not be in revision
-               $res = $dbr->select( 'revision', '*', [ 'rev_id' => $this->ipRevId ] );
+               $res = $dbr->select( 'revision', '1', [ 'rev_id' => $this->ipRevId ] );
                $this->assertFalse( $res->fetchObject() );
 
                // Should not be in ip_changes
-               $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $this->ipRevId ] );
+               $res = $dbr->select( 'ip_changes', '1', [ 'ipc_rev_id' => $this->ipRevId ] );
                $this->assertFalse( $res->fetchObject() );
 
                // Restore the page
                $this->archivedPage->undelete( [] );
 
                // Should be back in revision
-               $res = $dbr->select( 'revision', '*', [ 'rev_id' => $this->ipRevId ] );
+               $revQuery = Revision::getQueryInfo();
+               $res = $dbr->select(
+                       $revQuery['tables'],
+                       $revQuery['fields'],
+                       [ 'rev_id' => $this->ipRevId ],
+                       __METHOD__,
+                       [],
+                       $revQuery['joins']
+               );
                $row = $res->fetchObject();
                $this->assertEquals( $this->ipEditor, $row->rev_user_text );
 
                // Should be back in ip_changes
-               $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $this->ipRevId ] );
+               $res = $dbr->select( 'ip_changes', [ 'ipc_hex' ], [ 'ipc_rev_id' => $this->ipRevId ] );
                $row = $res->fetchObject();
                $this->assertEquals( IP::toHex( $this->ipEditor ), $row->ipc_hex );
        }
@@ -134,6 +154,7 @@ class PageArchiveTest extends MediaWikiTestCase {
                                'ar_minor_edit' => '0',
                                'ar_user' => '0',
                                'ar_user_text' => '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7',
+                               'ar_actor' => null,
                                'ar_len' => '11',
                                'ar_deleted' => '0',
                                'ar_rev_id' => '3',
@@ -159,6 +180,7 @@ class PageArchiveTest extends MediaWikiTestCase {
                                'ar_minor_edit' => '0',
                                'ar_user' => '0',
                                'ar_user_text' => '127.0.0.1',
+                               'ar_actor' => null,
                                'ar_len' => '7',
                                'ar_deleted' => '0',
                                'ar_rev_id' => '2',
index b05a742..61f1802 100644 (file)
@@ -108,7 +108,9 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                }
 
                if ( !isset( $props['user_text'] ) ) {
-                       $props['user_text'] = 'Tester';
+                       $user = $this->getTestUser()->getUser();
+                       $props['user_text'] = $user->getName();
+                       $props['user'] = $user->getId();
                }
 
                if ( !isset( $props['user'] ) ) {
@@ -243,7 +245,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                                'rev_id',
                                'rev_page',
                                'rev_text_id',
-                               'rev_user',
                                'rev_minor_edit',
                                'rev_deleted',
                                'rev_len',
@@ -257,7 +258,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                                strval( $textId ),
                                '0',
                                '0',
-                               '0',
                                '13',
                                strval( $parentId ),
                                's0ngbdoxagreuf2vjtuxzwdz64n29xm',
@@ -397,7 +397,8 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        $services->getDBLoadBalancer(),
                        $services->getService( '_SqlBlobStore' ),
                        $services->getMainWANObjectCache(),
-                       $services->getCommentStore()
+                       $services->getCommentStore(),
+                       $services->getActorMigration()
                );
 
                $store->setContentHandlerUseDB( $this->getContentHandlerUseDB() );
@@ -745,15 +746,17 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                // test it ---------------------------------
                $since = $revisions[$sinceIdx]->getTimestamp();
 
+               $revQuery = Revision::getQueryInfo();
                $allRows = iterator_to_array( $dbw->select(
-                       'revision',
-                       [ 'rev_id', 'rev_timestamp', 'rev_user' ],
+                       $revQuery['tables'],
+                       [ 'rev_id', 'rev_timestamp', 'rev_user' => $revQuery['fields']['rev_user'] ],
                        [
                                'rev_page' => $page->getId(),
                                //'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $since ) )
                        ],
                        __METHOD__,
-                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ]
+                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
+                       $revQuery['joins']
                ) );
 
                $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
index 8eac064..8b644c5 100644 (file)
@@ -157,13 +157,6 @@ class RevisionTest extends MediaWikiTestCase {
                        new MWException( "Text already stored in external store (id someid), " .
                                "can't serialize content object" )
                ];
-               yield 'unknown user id and no user name' => [
-                       [
-                               'content' => new JavaScriptContent( 'hello world.' ),
-                               'user' => 9989,
-                       ],
-                       new MWException( 'user_text not given, and unknown user ID 9989' )
-               ];
                yield 'with bad content object (class)' => [
                        [ 'content' => new stdClass() ],
                        new MWException( 'content field must contain a Content object.' )
@@ -494,7 +487,8 @@ class RevisionTest extends MediaWikiTestCase {
                        $lb,
                        $this->getBlobStore(),
                        $cache,
-                       MediaWikiServices::getInstance()->getCommentStore()
+                       MediaWikiServices::getInstance()->getCommentStore(),
+                       MediaWikiServices::getInstance()->getActorMigration()
                );
                return $blobStore;
        }
@@ -617,6 +611,7 @@ class RevisionTest extends MediaWikiTestCase {
         */
        public function testLoadFromTitle() {
                $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
                $this->overrideMwServices();
                $title = $this->getMockTitle();
 
@@ -875,6 +870,8 @@ class RevisionTest extends MediaWikiTestCase {
         */
        public function testUserJoinCond() {
                $this->hideDeprecated( 'Revision::userJoinCond' );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->overrideMwServices();
                $this->assertEquals(
                        [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
                        Revision::userJoinCond()
@@ -892,7 +889,7 @@ class RevisionTest extends MediaWikiTestCase {
                );
        }
 
-       private function overrideCommentStore() {
+       private function overrideCommentStoreAndActorMigration() {
                $mockStore = $this->getMockBuilder( CommentStore::class )
                        ->disableOriginalConstructor()
                        ->getMock();
@@ -906,8 +903,26 @@ class RevisionTest extends MediaWikiTestCase {
                                'fields' => [ 'commentstore' => 'field' ],
                                'joins' => [ 'commentstore' => 'join' ],
                        ] );
-
                $this->setService( 'CommentStore', $mockStore );
+
+               $mockStore = $this->getMockBuilder( ActorMigration::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $mockStore->expects( $this->any() )
+                       ->method( 'getJoin' )
+                       ->willReturnCallback( function ( $key ) {
+                               $p = strtok( $key, '_' );
+                               return [
+                                       'tables' => [ 'actormigration' => 'table' ],
+                                       'fields' => [
+                                               $p . '_user' => 'actormigration_user',
+                                               $p . '_user_text' => 'actormigration_user_text',
+                                               $p . '_actor' => 'actormigration_actor',
+                                       ],
+                                       'joins' => [ 'actormigration' => 'join' ],
+                               ];
+                       } );
+               $this->setService( 'ActorMigration', $mockStore );
        }
 
        public function provideSelectFields() {
@@ -920,6 +935,7 @@ class RevisionTest extends MediaWikiTestCase {
                                'rev_timestamp',
                                'rev_user_text',
                                'rev_user',
+                               'rev_actor' => 'NULL',
                                'rev_minor_edit',
                                'rev_deleted',
                                'rev_len',
@@ -939,6 +955,7 @@ class RevisionTest extends MediaWikiTestCase {
                                'rev_timestamp',
                                'rev_user_text',
                                'rev_user',
+                               'rev_actor' => 'NULL',
                                'rev_minor_edit',
                                'rev_deleted',
                                'rev_len',
@@ -956,7 +973,8 @@ class RevisionTest extends MediaWikiTestCase {
        public function testSelectFields( $contentHandlerUseDB, $expected ) {
                $this->hideDeprecated( 'Revision::selectFields' );
                $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB );
-               $this->overrideCommentStore();
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->overrideCommentStoreAndActorMigration();
                $this->assertEquals( $expected, Revision::selectFields() );
        }
 
@@ -972,6 +990,7 @@ class RevisionTest extends MediaWikiTestCase {
                                'ar_timestamp',
                                'ar_user_text',
                                'ar_user',
+                               'ar_actor' => 'NULL',
                                'ar_minor_edit',
                                'ar_deleted',
                                'ar_len',
@@ -993,6 +1012,7 @@ class RevisionTest extends MediaWikiTestCase {
                                'ar_timestamp',
                                'ar_user_text',
                                'ar_user',
+                               'ar_actor' => 'NULL',
                                'ar_minor_edit',
                                'ar_deleted',
                                'ar_len',
@@ -1010,7 +1030,8 @@ class RevisionTest extends MediaWikiTestCase {
        public function testSelectArchiveFields( $contentHandlerUseDB, $expected ) {
                $this->hideDeprecated( 'Revision::selectArchiveFields' );
                $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB );
-               $this->overrideCommentStore();
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->overrideCommentStoreAndActorMigration();
                $this->assertEquals( $expected, Revision::selectArchiveFields() );
        }
 
@@ -1068,6 +1089,7 @@ class RevisionTest extends MediaWikiTestCase {
                                'tables' => [
                                        'archive',
                                        'commentstore' => 'table',
+                                       'actormigration' => 'table',
                                ],
                                'fields' => [
                                        'ar_id',
@@ -1078,16 +1100,17 @@ class RevisionTest extends MediaWikiTestCase {
                                        'ar_text',
                                        'ar_text_id',
                                        'ar_timestamp',
-                                       'ar_user_text',
-                                       'ar_user',
                                        'ar_minor_edit',
                                        'ar_deleted',
                                        'ar_len',
                                        'ar_parent_id',
                                        'ar_sha1',
-                                       'commentstore' => 'field'
+                                       'commentstore' => 'field',
+                                       'ar_user' => 'actormigration_user',
+                                       'ar_user_text' => 'actormigration_user_text',
+                                       'ar_actor' => 'actormigration_actor',
                                ],
-                               'joins' => [ 'commentstore' => 'join' ],
+                               'joins' => [ 'commentstore' => 'join', 'actormigration' => 'join' ],
                        ]
                ];
                yield 'wgContentHandlerUseDB true' => [
@@ -1098,6 +1121,7 @@ class RevisionTest extends MediaWikiTestCase {
                                'tables' => [
                                        'archive',
                                        'commentstore' => 'table',
+                                       'actormigration' => 'table',
                                ],
                                'fields' => [
                                        'ar_id',
@@ -1108,18 +1132,19 @@ class RevisionTest extends MediaWikiTestCase {
                                        'ar_text',
                                        'ar_text_id',
                                        'ar_timestamp',
-                                       'ar_user_text',
-                                       'ar_user',
                                        'ar_minor_edit',
                                        'ar_deleted',
                                        'ar_len',
                                        'ar_parent_id',
                                        'ar_sha1',
                                        'commentstore' => 'field',
+                                       'ar_user' => 'actormigration_user',
+                                       'ar_user_text' => 'actormigration_user_text',
+                                       'ar_actor' => 'actormigration_actor',
                                        'ar_content_format',
                                        'ar_content_model',
                                ],
-                               'joins' => [ 'commentstore' => 'join' ],
+                               'joins' => [ 'commentstore' => 'join', 'actormigration' => 'join' ],
                        ]
                ];
        }
@@ -1130,7 +1155,7 @@ class RevisionTest extends MediaWikiTestCase {
         */
        public function testGetArchiveQueryInfo( $globals, $expected ) {
                $this->setMwGlobals( $globals );
-               $this->overrideCommentStore();
+               $this->overrideCommentStoreAndActorMigration();
 
                $revisionStore = $this->getRevisionStore();
                $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
@@ -1148,22 +1173,23 @@ class RevisionTest extends MediaWikiTestCase {
                        ],
                        [],
                        [
-                               'tables' => [ 'revision', 'commentstore' => 'table' ],
+                               'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table' ],
                                'fields' => [
                                        'rev_id',
                                        'rev_page',
                                        'rev_text_id',
                                        'rev_timestamp',
-                                       'rev_user_text',
-                                       'rev_user',
                                        'rev_minor_edit',
                                        'rev_deleted',
                                        'rev_len',
                                        'rev_parent_id',
                                        'rev_sha1',
                                        'commentstore' => 'field',
+                                       'rev_user' => 'actormigration_user',
+                                       'rev_user_text' => 'actormigration_user_text',
+                                       'rev_actor' => 'actormigration_actor',
                                ],
-                               'joins' => [ 'commentstore' => 'join' ],
+                               'joins' => [ 'commentstore' => 'join', 'actormigration' => 'join' ],
                        ],
                ];
                yield 'wgContentHandlerUseDB false, opts page' => [
@@ -1172,20 +1198,21 @@ class RevisionTest extends MediaWikiTestCase {
                        ],
                        [ 'page' ],
                        [
-                               'tables' => [ 'revision', 'commentstore' => 'table', 'page' ],
+                               'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table', 'page' ],
                                'fields' => [
                                        'rev_id',
                                        'rev_page',
                                        'rev_text_id',
                                        'rev_timestamp',
-                                       'rev_user_text',
-                                       'rev_user',
                                        'rev_minor_edit',
                                        'rev_deleted',
                                        'rev_len',
                                        'rev_parent_id',
                                        'rev_sha1',
                                        'commentstore' => 'field',
+                                       'rev_user' => 'actormigration_user',
+                                       'rev_user_text' => 'actormigration_user_text',
+                                       'rev_actor' => 'actormigration_actor',
                                        'page_namespace',
                                        'page_title',
                                        'page_id',
@@ -1199,6 +1226,7 @@ class RevisionTest extends MediaWikiTestCase {
                                                [ 'page_id = rev_page' ],
                                        ],
                                        'commentstore' => 'join',
+                                       'actormigration' => 'join',
                                ],
                        ],
                ];
@@ -1208,31 +1236,33 @@ class RevisionTest extends MediaWikiTestCase {
                        ],
                        [ 'user' ],
                        [
-                               'tables' => [ 'revision', 'commentstore' => 'table', 'user' ],
+                               'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table', 'user' ],
                                'fields' => [
                                        'rev_id',
                                        'rev_page',
                                        'rev_text_id',
                                        'rev_timestamp',
-                                       'rev_user_text',
-                                       'rev_user',
                                        'rev_minor_edit',
                                        'rev_deleted',
                                        'rev_len',
                                        'rev_parent_id',
                                        'rev_sha1',
                                        'commentstore' => 'field',
+                                       'rev_user' => 'actormigration_user',
+                                       'rev_user_text' => 'actormigration_user_text',
+                                       'rev_actor' => 'actormigration_actor',
                                        'user_name',
                                ],
                                'joins' => [
                                        'user' => [
                                                'LEFT JOIN',
                                                [
-                                                       'rev_user != 0',
-                                                       'user_id = rev_user',
+                                                       'actormigration_user != 0',
+                                                       'user_id = actormigration_user',
                                                ],
                                        ],
                                        'commentstore' => 'join',
+                                       'actormigration' => 'join',
                                ],
                        ],
                ];
@@ -1242,20 +1272,21 @@ class RevisionTest extends MediaWikiTestCase {
                        ],
                        [ 'text' ],
                        [
-                               'tables' => [ 'revision', 'commentstore' => 'table', 'text' ],
+                               'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table', 'text' ],
                                'fields' => [
                                        'rev_id',
                                        'rev_page',
                                        'rev_text_id',
                                        'rev_timestamp',
-                                       'rev_user_text',
-                                       'rev_user',
                                        'rev_minor_edit',
                                        'rev_deleted',
                                        'rev_len',
                                        'rev_parent_id',
                                        'rev_sha1',
                                        'commentstore' => 'field',
+                                       'rev_user' => 'actormigration_user',
+                                       'rev_user_text' => 'actormigration_user_text',
+                                       'rev_actor' => 'actormigration_actor',
                                        'old_text',
                                        'old_flags',
                                ],
@@ -1265,6 +1296,7 @@ class RevisionTest extends MediaWikiTestCase {
                                                [ 'rev_text_id=old_id' ],
                                        ],
                                        'commentstore' => 'join',
+                                       'actormigration' => 'join',
                                ],
                        ],
                ];
@@ -1274,20 +1306,23 @@ class RevisionTest extends MediaWikiTestCase {
                        ],
                        [ 'text', 'page', 'user' ],
                        [
-                               'tables' => [ 'revision', 'commentstore' => 'table', 'page', 'user', 'text' ],
+                               'tables' => [
+                                       'revision', 'commentstore' => 'table', 'actormigration' => 'table', 'page', 'user', 'text'
+                               ],
                                'fields' => [
                                        'rev_id',
                                        'rev_page',
                                        'rev_text_id',
                                        'rev_timestamp',
-                                       'rev_user_text',
-                                       'rev_user',
                                        'rev_minor_edit',
                                        'rev_deleted',
                                        'rev_len',
                                        'rev_parent_id',
                                        'rev_sha1',
                                        'commentstore' => 'field',
+                                       'rev_user' => 'actormigration_user',
+                                       'rev_user_text' => 'actormigration_user_text',
+                                       'rev_actor' => 'actormigration_actor',
                                        'page_namespace',
                                        'page_title',
                                        'page_id',
@@ -1306,8 +1341,8 @@ class RevisionTest extends MediaWikiTestCase {
                                        'user' => [
                                                'LEFT JOIN',
                                                [
-                                                       'rev_user != 0',
-                                                       'user_id = rev_user',
+                                                       'actormigration_user != 0',
+                                                       'user_id = actormigration_user',
                                                ],
                                        ],
                                        'text' => [
@@ -1315,6 +1350,7 @@ class RevisionTest extends MediaWikiTestCase {
                                                [ 'rev_text_id=old_id' ],
                                        ],
                                        'commentstore' => 'join',
+                                       'actormigration' => 'join',
                                ],
                        ],
                ];
@@ -1324,24 +1360,25 @@ class RevisionTest extends MediaWikiTestCase {
                        ],
                        [],
                        [
-                               'tables' => [ 'revision', 'commentstore' => 'table' ],
+                               'tables' => [ 'revision', 'commentstore' => 'table', 'actormigration' => 'table' ],
                                'fields' => [
                                        'rev_id',
                                        'rev_page',
                                        'rev_text_id',
                                        'rev_timestamp',
-                                       'rev_user_text',
-                                       'rev_user',
                                        'rev_minor_edit',
                                        'rev_deleted',
                                        'rev_len',
                                        'rev_parent_id',
                                        'rev_sha1',
                                        'commentstore' => 'field',
+                                       'rev_user' => 'actormigration_user',
+                                       'rev_user_text' => 'actormigration_user_text',
+                                       'rev_actor' => 'actormigration_actor',
                                        'rev_content_format',
                                        'rev_content_model',
                                ],
-                               'joins' => [ 'commentstore' => 'join' ],
+                               'joins' => [ 'commentstore' => 'join', 'actormigration' => 'join' ],
                        ],
                ];
        }
@@ -1352,7 +1389,7 @@ class RevisionTest extends MediaWikiTestCase {
         */
        public function testGetQueryInfo( $globals, $options, $expected ) {
                $this->setMwGlobals( $globals );
-               $this->overrideCommentStore();
+               $this->overrideCommentStoreAndActorMigration();
 
                $revisionStore = $this->getRevisionStore();
                $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
index d31ca5c..e81f0af 100644 (file)
@@ -134,6 +134,7 @@ class RevisionStoreDbTest extends MediaWikiTestCase {
                        $blobStore,
                        new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
                        MediaWikiServices::getInstance()->getCommentStore(),
+                       MediaWikiServices::getInstance()->getActorMigration(),
                        $wikiId
                );
 
@@ -621,12 +622,15 @@ class RevisionStoreDbTest extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
         */
        public function testNewRevisionFromRow_anonEdit() {
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->overrideMwServices();
+
                $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
                $text = __METHOD__ . 'a-ä';
                /** @var Revision $rev */
                $rev = $page->doEditContent(
                        new WikitextContent( $text ),
-                       __METHOD__. 'a'
+                       __METHOD__ . 'a'
                )->value['revision'];
 
                $store = MediaWikiServices::getInstance()->getRevisionStore();
@@ -669,6 +673,9 @@ class RevisionStoreDbTest extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
         */
        public function testNewRevisionFromRow_userEdit() {
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->overrideMwServices();
+
                $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
                $text = __METHOD__ . 'b-ä';
                /** @var Revision $rev */
@@ -1048,7 +1055,7 @@ class RevisionStoreDbTest extends MediaWikiTestCase {
                $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
                /** @var Revision $rev */
                $rev = $page->doEditContent(
-                       new WikitextContent( __METHOD__. 'b' ),
+                       new WikitextContent( __METHOD__ . 'b' ),
                        __METHOD__ . 'b',
                        0,
                        false,
index aa59a5b..43784b6 100644 (file)
@@ -30,7 +30,7 @@ class RevisionStoreRecordTest extends MediaWikiTestCase {
                $title = Title::newFromText( 'Dummy' );
                $title->resetArticleID( 17 );
 
-               $user = new UserIdentityValue( 11, 'Tester' );
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
                $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
 
                $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
@@ -58,7 +58,7 @@ class RevisionStoreRecordTest extends MediaWikiTestCase {
                $title = Title::newFromText( 'Dummy' );
                $title->resetArticleID( 17 );
 
-               $user = new UserIdentityValue( 11, 'Tester' );
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
                $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
 
                $main = SlotRecord::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
@@ -213,7 +213,7 @@ class RevisionStoreRecordTest extends MediaWikiTestCase {
                $title = Title::newFromText( 'Dummy' );
                $title->resetArticleID( 17 );
 
-               $user = new UserIdentityValue( 11, 'Tester' );
+               $user = new UserIdentityValue( 11, 'Tester', 0 );
 
                $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
 
@@ -688,7 +688,7 @@ class RevisionStoreRecordTest extends MediaWikiTestCase {
 
                        return new RevisionStoreRecord(
                                $title,
-                               new UserIdentityValue( 11, __METHOD__ ),
+                               new UserIdentityValue( 11, __METHOD__, 0 ),
                                CommentStoreComment::newUnsavedComment( __METHOD__ ),
                                (object)[
                                        'rev_id' => strval( $revId ),
index 8e8de6e..8498947 100644 (file)
@@ -32,7 +32,8 @@ class RevisionStoreTest extends MediaWikiTestCase {
                        $loadBalancer ? $loadBalancer : $this->getMockLoadBalancer(),
                        $blobStore ? $blobStore : $this->getMockSqlBlobStore(),
                        $WANObjectCache ? $WANObjectCache : $this->getHashWANObjectCache(),
-                       MediaWikiServices::getInstance()->getCommentStore()
+                       MediaWikiServices::getInstance()->getCommentStore(),
+                       MediaWikiServices::getInstance()->getActorMigration()
                );
        }
 
@@ -83,8 +84,6 @@ class RevisionStoreTest extends MediaWikiTestCase {
                        'rev_page',
                        'rev_text_id',
                        'rev_timestamp',
-                       'rev_user_text',
-                       'rev_user',
                        'rev_minor_edit',
                        'rev_deleted',
                        'rev_len',
@@ -101,6 +100,14 @@ class RevisionStoreTest extends MediaWikiTestCase {
                ];
        }
 
+       private function getActorQueryFields() {
+               return [
+                       'rev_user' => 'rev_user',
+                       'rev_user_text' => 'rev_user_text',
+                       'rev_actor' => 'NULL',
+               ];
+       }
+
        private function getContentHandlerQueryFields() {
                return [
                        'rev_content_format',
@@ -117,6 +124,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                'fields' => array_merge(
                                        $this->getDefaultQueryFields(),
                                        $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
                                        $this->getContentHandlerQueryFields()
                                ),
                                'joins' => [],
@@ -129,7 +137,8 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                'tables' => [ 'revision' ],
                                'fields' => array_merge(
                                        $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields()
+                                       $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields()
                                ),
                                'joins' => [],
                        ]
@@ -142,6 +151,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                'fields' => array_merge(
                                        $this->getDefaultQueryFields(),
                                        $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
                                        [
                                                'page_namespace',
                                                'page_title',
@@ -164,6 +174,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                'fields' => array_merge(
                                        $this->getDefaultQueryFields(),
                                        $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
                                        [
                                                'user_name',
                                        ]
@@ -181,6 +192,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                'fields' => array_merge(
                                        $this->getDefaultQueryFields(),
                                        $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
                                        [
                                                'old_text',
                                                'old_flags',
@@ -199,6 +211,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                'fields' => array_merge(
                                        $this->getDefaultQueryFields(),
                                        $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
                                        $this->getContentHandlerQueryFields(),
                                        [
                                                'page_namespace',
@@ -227,6 +240,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
         */
        public function testGetQueryInfo( $contentHandlerUseDb, $options, $expected ) {
                $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
                $this->overrideMwServices();
                $store = $this->getRevisionStore();
                $store->setContentHandlerUseDB( $contentHandlerUseDb );
@@ -243,8 +257,6 @@ class RevisionStoreTest extends MediaWikiTestCase {
                        'ar_text',
                        'ar_text_id',
                        'ar_timestamp',
-                       'ar_user_text',
-                       'ar_user',
                        'ar_minor_edit',
                        'ar_deleted',
                        'ar_len',
@@ -258,6 +270,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
         */
        public function testGetArchiveQueryInfo_contentHandlerDb() {
                $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
                $this->overrideMwServices();
                $store = $this->getRevisionStore();
                $store->setContentHandlerUseDB( true );
@@ -272,6 +285,9 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                                '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',
                                                'ar_content_format',
                                                'ar_content_model',
                                        ]
@@ -287,6 +303,7 @@ class RevisionStoreTest extends MediaWikiTestCase {
         */
        public function testGetArchiveQueryInfo_noContentHandlerDb() {
                $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
                $this->overrideMwServices();
                $store = $this->getRevisionStore();
                $store->setContentHandlerUseDB( false );
@@ -301,6 +318,9 @@ class RevisionStoreTest extends MediaWikiTestCase {
                                                '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' => [],
index 575f0c9..3f6cac9 100644 (file)
@@ -157,6 +157,7 @@ class ApiBaseTest extends ApiTestCase {
                $block = new \Block( [
                        'address' => $user->getName(),
                        'user' => $user->getID(),
+                       'by' => $this->getTestSysop()->getUser()->getId(),
                        'reason' => __METHOD__,
                        'expiry' => time() + 100500,
                ] );
index 19f66fa..24b7500 100644 (file)
@@ -589,7 +589,7 @@ class ApiQueryRecentChangesIntegrationTest extends ApiTestCase {
                        'rc_minor' => 0,
                        'rc_cur_id' => $title->getArticleID(),
                        'rc_user' => 0,
-                       'rc_user_text' => 'External User',
+                       'rc_user_text' => 'm>External User',
                        'rc_comment' => '',
                        'rc_comment_text' => '',
                        'rc_comment_data' => null,
index fdbeded..8919c5e 100644 (file)
@@ -1072,7 +1072,7 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
                        'rc_minor' => 0,
                        'rc_cur_id' => $title->getArticleID(),
                        'rc_user' => 0,
-                       'rc_user_text' => 'External User',
+                       'rc_user_text' => 'ext>External User',
                        'rc_comment' => '',
                        'rc_comment_text' => '',
                        'rc_comment_data' => null,
diff --git a/tests/phpunit/includes/api/query/ApiQueryUserContributionsTest.php b/tests/phpunit/includes/api/query/ApiQueryUserContributionsTest.php
new file mode 100644 (file)
index 0000000..6e8bc0b
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiQueryContributions
+ */
+class ApiQueryContributionsTest extends ApiTestCase {
+       public function addDBDataOnce() {
+               global $wgActorTableSchemaMigrationStage;
+
+               $reset = new \Wikimedia\ScopedCallback( function ( $v ) {
+                       global $wgActorTableSchemaMigrationStage;
+                       $wgActorTableSchemaMigrationStage = $v;
+                       $this->overrideMwServices();
+               }, [ $wgActorTableSchemaMigrationStage ] );
+               $wgActorTableSchemaMigrationStage = MIGRATION_WRITE_BOTH;
+               $this->overrideMwServices();
+
+               $users = [
+                       User::newFromName( '192.168.2.2', false ),
+                       User::newFromName( '192.168.2.1', false ),
+                       User::newFromName( '192.168.2.3', false ),
+                       User::createNew( __CLASS__ . ' B' ),
+                       User::createNew( __CLASS__ . ' A' ),
+                       User::createNew( __CLASS__ . ' C' ),
+               ];
+
+               $title = Title::newFromText( __CLASS__ );
+               $page = WikiPage::factory( $title );
+               for ( $i = 0; $i < 3; $i++ ) {
+                       foreach ( array_reverse( $users ) as $user ) {
+                               $status = $page->doEditContent(
+                                       ContentHandler::makeContent( "Test revision $user #$i", $title ), 'Test edit', 0, false, $user
+                               );
+                               if ( !$status->isOK() ) {
+                                       $this->fail( "Failed to edit $title: " . $status->getWikiText( false, false, 'en' ) );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * @dataProvider provideSorting
+        * @param int $stage One of the MIGRATION_* constants 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' );
+               }
+
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', $stage );
+               $this->overrideMwServices();
+
+               if ( isset( $params['ucuserids'] ) ) {
+                       $params['ucuserids'] = implode( '|', array_map( 'User::idFromName', $params['ucuserids'] ) );
+               }
+               if ( isset( $params['ucuser'] ) ) {
+                       $params['ucuser'] = implode( '|', $params['ucuser'] );
+               }
+
+               $sort = 'rsort';
+               if ( $reverse ) {
+                       $params['ucdir'] = 'newer';
+                       $sort = 'sort';
+               }
+
+               $params += [
+                       'action' => 'query',
+                       'list' => 'usercontribs',
+                       'ucprop' => 'ids',
+               ];
+
+               $apiResult = $this->doApiRequest( $params + [ 'uclimit' => 500 ] );
+               $this->assertArrayNotHasKey( 'continue', $apiResult[0] );
+               $this->assertArrayHasKey( 'query', $apiResult[0] );
+               $this->assertArrayHasKey( 'usercontribs', $apiResult[0]['query'] );
+
+               $count = 0;
+               $ids = [];
+               foreach ( $apiResult[0]['query']['usercontribs'] as $page ) {
+                       $count++;
+                       $ids[$page['user']][] = $page['revid'];
+               }
+               $this->assertSame( $revs, $count, 'Expected number of revisions' );
+               foreach ( $ids as $user => $revids ) {
+                       $sorted = $revids;
+                       call_user_func_array( $sort, [ &$sorted ] );
+                       $this->assertSame( $sorted, $revids, "IDs for $user are sorted" );
+               }
+
+               for ( $limit = 1; $limit < $revs; $limit++ ) {
+                       $continue = [];
+                       $count = 0;
+                       $batchedIds = [];
+                       while ( $continue !== null ) {
+                               $apiResult = $this->doApiRequest( $params + [ 'uclimit' => $limit ] + $continue );
+                               $this->assertArrayHasKey( 'query', $apiResult[0], "Batching with limit $limit" );
+                               $this->assertArrayHasKey( 'usercontribs', $apiResult[0]['query'],
+                                       "Batching with limit $limit" );
+                               $continue = isset( $apiResult[0]['continue'] ) ? $apiResult[0]['continue'] : null;
+                               foreach ( $apiResult[0]['query']['usercontribs'] as $page ) {
+                                       $count++;
+                                       $batchedIds[$page['user']][] = $page['revid'];
+                               }
+                               $this->assertLessThanOrEqual( $revs, $count, "Batching with limit $limit" );
+                       }
+                       $this->assertSame( $ids, $batchedIds, "Result set is the same when batching with limit $limit" );
+               }
+       }
+
+       public static function provideSorting() {
+               $users = [ __CLASS__ . ' A', __CLASS__ . ' B', __CLASS__ . ' C' ];
+               $users2 = [ __CLASS__ . ' A', __CLASS__ . ' B', __CLASS__ . ' D' ];
+               $ips = [ '192.168.2.1', '192.168.2.2', '192.168.2.3', '192.168.2.4' ];
+
+               foreach (
+                       [
+                               'old' => MIGRATION_OLD,
+                               'write both' => MIGRATION_WRITE_BOTH,
+                               'write new' => MIGRATION_WRITE_NEW,
+                               'new' => MIGRATION_NEW,
+                       ] as $stageName => $stage
+               ) {
+                       foreach ( [ false, true ] as $reverse ) {
+                               $name = $stageName . ( $reverse ? ', reverse' : '' );
+                               yield "Named users, $name" => [ $stage, [ 'ucuser' => $users ], $reverse, 9 ];
+                               yield "Named users including a no-edit user, $name" => [
+                                       $stage, [ 'ucuser' => $users2 ], $reverse, 6
+                               ];
+                               yield "IP users, $name" => [ $stage, [ 'ucuser' => $ips ], $reverse, 9 ];
+                               yield "All users, $name" => [
+                                       $stage, [ 'ucuser' => array_merge( $users, $ips ) ], $reverse, 18
+                               ];
+                               yield "User IDs, $name" => [ $stage, [ 'ucuserids' => $users ], $reverse, 9 ];
+                               yield "Users by prefix, $name" => [ $stage, [ 'ucuserprefix' => __CLASS__ ], $reverse, 9 ];
+                               yield "IPs by prefix, $name" => [ $stage, [ 'ucuserprefix' => '192.168.2.' ], $reverse, 9 ];
+                       }
+               }
+       }
+}
index e4056ee..211eba0 100644 (file)
@@ -1436,6 +1436,7 @@ class AuthManagerTest extends \MediaWikiTestCase {
                $blockOptions = [
                        'address' => 'UTBlockee',
                        'user' => $user->getID(),
+                       'by' => $this->getTestSysop()->getUser()->getId(),
                        'reason' => __METHOD__,
                        'expiry' => time() + 100500,
                        'createAccount' => true,
@@ -1448,6 +1449,7 @@ class AuthManagerTest extends \MediaWikiTestCase {
 
                $blockOptions = [
                        'address' => '127.0.0.0/24',
+                       'by' => $this->getTestSysop()->getUser()->getId(),
                        'reason' => __METHOD__,
                        'expiry' => time() + 100500,
                        'createAccount' => true,
index 4e44547..81cdc9d 100644 (file)
@@ -76,6 +76,7 @@ class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase
                $blockOptions = [
                        'address' => 'UTBlockee',
                        'user' => $user->getID(),
+                       'by' => $this->getTestSysop()->getUser()->getId(),
                        'reason' => __METHOD__,
                        'expiry' => time() + 100500,
                        'createAccount' => true,
@@ -149,6 +150,7 @@ class CheckBlocksSecondaryAuthenticationProviderTest extends \MediaWikiTestCase
                $blockOptions = [
                        'address' => '127.0.0.0/24',
                        'reason' => __METHOD__,
+                       'by' => $this->getTestSysop()->getUser()->getId(),
                        'expiry' => time() + 100500,
                        'createAccount' => true,
                ];
index ab42696..333eb28 100644 (file)
@@ -27,12 +27,16 @@ class RecentChangeTest extends MediaWikiTestCase {
         * @covers RecentChange::loadFromRow
         */
        public function testNewFromRow() {
+               $user = $this->getTestUser()->getUser();
+               $actorId = $user->getActorId();
+
                $row = new stdClass();
                $row->rc_foo = 'AAA';
                $row->rc_timestamp = '20150921134808';
                $row->rc_deleted = 'bar';
                $row->rc_comment_text = 'comment';
                $row->rc_comment_data = null;
+               $row->rc_user = $user->getId();
 
                $rc = RecentChange::newFromRow( $row );
 
@@ -43,6 +47,9 @@ class RecentChangeTest extends MediaWikiTestCase {
                        'rc_comment' => 'comment',
                        'rc_comment_text' => 'comment',
                        'rc_comment_data' => null,
+                       'rc_user' => $user->getId(),
+                       'rc_user_text' => $user->getName(),
+                       'rc_actor' => $actorId,
                ];
                $this->assertEquals( $expected, $rc->getAttributes() );
 
@@ -51,6 +58,7 @@ class RecentChangeTest extends MediaWikiTestCase {
                $row->rc_timestamp = '20150921134808';
                $row->rc_deleted = 'bar';
                $row->rc_comment = 'comment';
+               $row->rc_user = $user->getId();
 
                Wikimedia\suppressWarnings();
                $rc = RecentChange::newFromRow( $row );
@@ -63,6 +71,9 @@ class RecentChangeTest extends MediaWikiTestCase {
                        'rc_comment' => 'comment',
                        'rc_comment_text' => 'comment',
                        'rc_comment_data' => null,
+                       'rc_user' => $user->getId(),
+                       'rc_user_text' => $user->getName(),
+                       'rc_actor' => $actorId,
                ];
                $this->assertEquals( $expected, $rc->getAttributes() );
        }
index 7bb03db..3b91f5b 100644 (file)
@@ -290,12 +290,15 @@ EOF
                $importer->doImport();
 
                $db = wfGetDB( DB_MASTER );
+               $revQuery = Revision::getQueryInfo();
 
                $row = $db->selectRow(
-                       'revision',
-                       [ 'rev_user', 'rev_user_text' ],
+                       $revQuery['tables'],
+                       $revQuery['fields'],
                        [ 'rev_timestamp' => $db->timestamp( "201601010{$n}0000" ) ],
-                       __METHOD__
+                       __METHOD__,
+                       [],
+                       $revQuery['joins']
                );
                $this->assertSame(
                        $assign && $create ? 'UserDoesNotExist' : 'Xxx>UserDoesNotExist',
@@ -304,10 +307,12 @@ EOF
                $this->assertSame( $assign && $create ? $hookId : 0, (int)$row->rev_user );
 
                $row = $db->selectRow(
-                       'revision',
-                       [ 'rev_user', 'rev_user_text' ],
+                       $revQuery['tables'],
+                       $revQuery['fields'],
                        [ 'rev_timestamp' => $db->timestamp( "201601010{$n}0001" ) ],
-                       __METHOD__
+                       __METHOD__,
+                       [],
+                       $revQuery['joins']
                );
                $this->assertSame( ( $assign ? '' : 'Xxx>' ) . $user->getName(), $row->rev_user_text );
                $this->assertSame( $assign ? $user->getId() : 0, (int)$row->rev_user );
index 2dc9a2c..786d761 100644 (file)
@@ -36,6 +36,7 @@ abstract class LogFormatterTestCase extends MediaWikiLangTestCase {
                        'log_timestamp' => isset( $data['timestamp'] ) ? $data['timestamp'] : wfTimestampNow(),
                        'log_user' => isset( $data['user'] ) ? $data['user'] : 0,
                        'log_user_text' => isset( $data['user_text'] ) ? $data['user_text'] : 'User',
+                       'log_actor' => isset( $data['actor'] ) ? $data['actor'] : 0,
                        'log_namespace' => isset( $data['namespace'] ) ? $data['namespace'] : NS_MAIN,
                        'log_title' => isset( $data['title'] ) ? $data['title'] : 'Main_Page',
                        'log_page' => isset( $data['page'] ) ? $data['page'] : 0,
index 6b680e5..7adc43b 100644 (file)
@@ -1784,12 +1784,13 @@ more stuff
 
                // Make sure the log entry looks good
                // log_params is not checked here
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
                $this->assertSelect(
-                       'logging',
+                       [ 'logging' ] + $actorQuery['tables'],
                        [
                                'log_comment',
-                               'log_user',
-                               'log_user_text',
+                               'log_user' => $actorQuery['fields']['log_user'],
+                               'log_user_text' => $actorQuery['fields']['log_user_text'],
                                'log_namespace',
                                'log_title',
                        ],
@@ -1800,7 +1801,9 @@ more stuff
                                $user->getName(),
                                (string)$page->getTitle()->getNamespace(),
                                $page->getTitle()->getDBkey(),
-                       ] ]
+                       ] ],
+                       [],
+                       $actorQuery['joins']
                );
        }
 
index 0839cfb..78175fa 100644 (file)
@@ -26,6 +26,8 @@
  */
 class UserPasswordPolicyTest extends MediaWikiTestCase {
 
+       protected $tablesUsed = [ 'user', 'user_groups' ];
+
        protected $policies = [
                'checkuser' => [
                        'MinimalPasswordLength' => 10,
index 9b81d6d..aac25d8 100644 (file)
@@ -199,10 +199,15 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testRcHidemyselfFilter() {
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->overrideMwServices();
+
                $user = $this->getTestUser()->getUser();
+               $user->getActorId( wfGetDB( DB_MASTER ) );
                $this->assertConditions(
                        [ # expected
-                               "rc_user_text != '{$user->getName()}'",
+                               "NOT((rc_actor = '{$user->getActorId()}') OR "
+                                       . "(rc_actor = '0' AND rc_user = '{$user->getId()}'))",
                        ],
                        [
                                'hidemyself' => 1,
@@ -212,9 +217,10 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                );
 
                $user = User::newFromName( '10.11.12.13', false );
+               $id = $user->getActorId( wfGetDB( DB_MASTER ) );
                $this->assertConditions(
                        [ # expected
-                               "rc_user_text != '10.11.12.13'",
+                               "NOT((rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13'))",
                        ],
                        [
                                'hidemyself' => 1,
@@ -225,10 +231,15 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testRcHidebyothersFilter() {
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->overrideMwServices();
+
                $user = $this->getTestUser()->getUser();
+               $user->getActorId( wfGetDB( DB_MASTER ) );
                $this->assertConditions(
                        [ # expected
-                               "rc_user_text = '{$user->getName()}'",
+                               "(rc_actor = '{$user->getActorId()}') OR "
+                               . "(rc_actor = '0' AND rc_user_text = '{$user->getName()}')",
                        ],
                        [
                                'hidebyothers' => 1,
@@ -238,9 +249,10 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                );
 
                $user = User::newFromName( '10.11.12.13', false );
+               $id = $user->getActorId( wfGetDB( DB_MASTER ) );
                $this->assertConditions(
                        [ # expected
-                               "rc_user_text = '10.11.12.13'",
+                               "(rc_actor = '$id') OR (rc_actor = '0' AND rc_user_text = '10.11.12.13')",
                        ],
                        [
                                'hidebyothers' => 1,
@@ -428,10 +440,13 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelAllExperienceLevels() {
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->overrideMwServices();
+
                $this->assertConditions(
                        [
                                # expected
-                               'rc_user != 0',
+                               'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
                        ],
                        [
                                'userExpLevel' => 'newcomer;learner;experienced',
@@ -441,10 +456,13 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelRegistrered() {
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->overrideMwServices();
+
                $this->assertConditions(
                        [
                                # expected
-                               'rc_user != 0',
+                               'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
                        ],
                        [
                                'userExpLevel' => 'registered',
@@ -454,10 +472,13 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelUnregistrered() {
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->overrideMwServices();
+
                $this->assertConditions(
                        [
                                # expected
-                               'rc_user' => 0,
+                               'COALESCE( actor_rc_user.actor_user, rc_user ) = 0',
                        ],
                        [
                                'userExpLevel' => 'unregistered',
@@ -467,10 +488,13 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelRegistreredOrLearner() {
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->overrideMwServices();
+
                $this->assertConditions(
                        [
                                # expected
-                               'rc_user != 0',
+                               'COALESCE( actor_rc_user.actor_user, rc_user ) != 0',
                        ],
                        [
                                'userExpLevel' => 'registered;learner',
@@ -480,10 +504,14 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
        }
 
        public function testFilterUserExpLevelUnregistreredOrExperienced() {
+               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->overrideMwServices();
+
                $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
 
                $this->assertRegExp(
-                       '/\(rc_user = 0\) OR \(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
+                       '/\(COALESCE\( actor_rc_user.actor_user, rc_user \) = 0\) OR '
+                               . '\(\(user_editcount >= 500\) AND \(user_registration <= \'[^\']+\'\)\)/',
                        reset( $conds ),
                        "rc conditions: userExpLevel=unregistered;experienced"
                );
@@ -595,8 +623,10 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                        ]
                );
 
+               // @todo: This is not at all safe or sane. It just blindly assumes
+               // nothing in $conds depends on any other tables.
                $result = wfGetDB( DB_MASTER )->select(
-                       $tables,
+                       'user',
                        'user_name',
                        array_filter( $conds ) + [ 'user_email' => 'ut' ]
                );
index f95e387..4862747 100644 (file)
@@ -4,6 +4,9 @@
  * @group Database
  */
 class UserGroupMembershipTest extends MediaWikiTestCase {
+
+       protected $tablesUsed = [ 'user', 'user_groups' ];
+
        /**
         * @var User Belongs to no groups
         */
index 4c1a5fd..c225ba5 100644 (file)
@@ -21,7 +21,9 @@ class UserTest extends MediaWikiTestCase {
                $this->setMwGlobals( [
                        'wgGroupPermissions' => [],
                        'wgRevokePermissions' => [],
+                       'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
                ] );
+               $this->overrideMwServices();
 
                $this->setUpPermissionGlobals();
 
@@ -617,6 +619,7 @@ class UserTest extends MediaWikiTestCase {
                        'enableAutoblock' => true,
                        'expiry' => wfTimestamp( TS_MW, $expiryFiveHours ),
                ] );
+               $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $user1tmp );
                $block->setBlocker( $userBlocker );
                $res = $block->insert();
@@ -694,6 +697,7 @@ class UserTest extends MediaWikiTestCase {
                $request1 = new FauxRequest();
                $request1->getSession()->setUser( $testUser );
                $block = new Block( [ 'enableAutoblock' => true ] );
+               $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $testUser );
                $block->setBlocker( $userBlocker );
                $res = $block->insert();
@@ -739,6 +743,7 @@ class UserTest extends MediaWikiTestCase {
                $request1 = new FauxRequest();
                $request1->getSession()->setUser( $user1Tmp );
                $block = new Block( [ 'enableAutoblock' => true, 'expiry' => 'infinity' ] );
+               $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $user1Tmp );
                $block->setBlocker( $userBlocker );
                $res = $block->insert();
@@ -834,6 +839,7 @@ class UserTest extends MediaWikiTestCase {
                $request1 = new FauxRequest();
                $request1->getSession()->setUser( $user1tmp );
                $block = new Block( [ 'enableAutoblock' => true ] );
+               $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $user1tmp );
                $block->setBlocker( $userBlocker );
                $res = $block->insert();
@@ -879,6 +885,7 @@ class UserTest extends MediaWikiTestCase {
                $request1 = new FauxRequest();
                $request1->getSession()->setUser( $user1tmp );
                $block = new Block( [ 'enableAutoblock' => true ] );
+               $block->setBlocker( $this->getTestSysop()->getUser() );
                $block->setTarget( $user1tmp );
                $block->setBlocker( $userBlocker );
                $res = $block->insert();
@@ -956,20 +963,18 @@ class UserTest extends MediaWikiTestCase {
                ] );
 
                $db = wfGetDB( DB_MASTER );
-
-               $data = new stdClass();
-               $data->user_id = 1;
-               $data->user_name = 'name';
-               $data->user_real_name = 'Real Name';
-               $data->user_touched = 1;
-               $data->user_token = 'token';
-               $data->user_email = 'a@a.a';
-               $data->user_email_authenticated = null;
-               $data->user_email_token = 'token';
-               $data->user_email_token_expires = null;
-               $data->user_editcount = $editCount;
-               $data->user_registration = $db->timestamp( time() - $memberSince * 86400 );
-               $user = User::newFromRow( $data );
+               $userQuery = User::getQueryInfo();
+               $row = $db->selectRow(
+                       $userQuery['tables'],
+                       $userQuery['fields'],
+                       [ 'user_id' => $this->getTestUser()->getUser()->getId() ],
+                       __METHOD__,
+                       [],
+                       $userQuery['joins']
+               );
+               $row->user_editcount = $editCount;
+               $row->user_registration = $db->timestamp( time() - $memberSince * 86400 );
+               $user = User::newFromRow( $row );
 
                $this->assertEquals( $expLevel, $user->getExperienceLevel() );
        }
@@ -1028,4 +1033,113 @@ class UserTest extends MediaWikiTestCase {
                );
                $this->assertTrue( User::isLocallyBlockedProxy( $ip ) );
        }
+
+       public function testActorId() {
+               $this->hideDeprecated( 'User::selectFields' );
+
+               // Newly-created user has an actor ID
+               $user = User::createNew( 'UserTestActorId1' );
+               $id = $user->getId();
+               $this->assertTrue( $user->getActorId() > 0, 'User::createNew sets an actor ID' );
+
+               $user = User::newFromName( 'UserTestActorId2' );
+               $user->addToDatabase();
+               $this->assertTrue( $user->getActorId() > 0, 'User::addToDatabase sets an actor ID' );
+
+               $user = User::newFromName( 'UserTestActorId1' );
+               $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by name' );
+
+               $user = User::newFromId( $id );
+               $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by ID' );
+
+               $user2 = User::newFromActorId( $user->getActorId() );
+               $this->assertEquals( $user->getId(), $user2->getId(),
+                       'User::newFromActorId works for an existing user' );
+
+               $row = $this->db->selectRow( 'user', User::selectFields(), [ 'user_id' => $id ], __METHOD__ );
+               $user = User::newFromRow( $row );
+               $this->assertTrue( $user->getActorId() > 0,
+                       'Actor ID can be retrieved for user loaded with User::selectFields()' );
+
+               $this->db->delete( 'actor', [ 'actor_user' => $id ], __METHOD__ );
+               User::purge( wfWikiId(), $id );
+               // Because WANObjectCache->delete() stupidly doesn't delete from the process cache.
+               ObjectCache::getMainWANInstance()->clearProcessCache();
+
+               $user = User::newFromId( $id );
+               $this->assertFalse( $user->getActorId() > 0, 'No Actor ID by default if none in database' );
+               $this->assertTrue( $user->getActorId( $this->db ) > 0, 'Actor ID can be created if none in db' );
+
+               $user->setName( 'UserTestActorId4-renamed' );
+               $user->saveSettings();
+               $this->assertEquals(
+                       $user->getName(),
+                       $this->db->selectField(
+                               'actor', 'actor_name', [ 'actor_id' => $user->getActorId() ], __METHOD__
+                       ),
+                       'User::saveSettings updates actor table for name change'
+               );
+
+               // For sanity
+               $ip = '192.168.12.34';
+               $this->db->delete( 'actor', [ 'actor_name' => $ip ], __METHOD__ );
+
+               $user = User::newFromName( $ip, false );
+               $this->assertFalse( $user->getActorId() > 0, 'Anonymous user has no actor ID by default' );
+               $this->assertTrue( $user->getActorId( $this->db ) > 0,
+                       'Actor ID can be created for an anonymous user' );
+
+               $user = User::newFromName( $ip, false );
+               $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be loaded for an anonymous user' );
+               $user2 = User::newFromActorId( $user->getActorId() );
+               $this->assertEquals( $user->getName(), $user2->getName(),
+                       'User::newFromActorId works for an anonymous user' );
+       }
+
+       public function testNewFromAnyId() {
+               // Registered user
+               $user = $this->getTestUser()->getUser();
+               for ( $i = 1; $i <= 7; $i++ ) {
+                       $test = User::newFromAnyId(
+                               ( $i & 1 ) ? $user->getId() : null,
+                               ( $i & 2 ) ? $user->getName() : null,
+                               ( $i & 4 ) ? $user->getActorId() : null
+                       );
+                       $this->assertSame( $user->getId(), $test->getId() );
+                       $this->assertSame( $user->getName(), $test->getName() );
+                       $this->assertSame( $user->getActorId(), $test->getActorId() );
+               }
+
+               // Anon user. Can't load by only user ID when that's 0.
+               $user = User::newFromName( '192.168.12.34', false );
+               $user->getActorId( $this->db ); // Make sure an actor ID exists
+
+               $test = User::newFromAnyId( null, '192.168.12.34', null );
+               $this->assertSame( $user->getId(), $test->getId() );
+               $this->assertSame( $user->getName(), $test->getName() );
+               $this->assertSame( $user->getActorId(), $test->getActorId() );
+               $test = User::newFromAnyId( null, null, $user->getActorId() );
+               $this->assertSame( $user->getId(), $test->getId() );
+               $this->assertSame( $user->getName(), $test->getName() );
+               $this->assertSame( $user->getActorId(), $test->getActorId() );
+
+               // Bogus data should still "work" as long as nothing triggers a ->load(),
+               // and accessing the specified data shouldn't do that.
+               $test = User::newFromAnyId( 123456, 'Bogus', 654321 );
+               $this->assertSame( 123456, $test->getId() );
+               $this->assertSame( 'Bogus', $test->getName() );
+               $this->assertSame( 654321, $test->getActorId() );
+
+               // Exceptional cases
+               try {
+                       User::newFromAnyId( null, null, null );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+               }
+               try {
+                       User::newFromAnyId( 0, null, 0 );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+               }
+       }
 }
index b6cf239..be51626 100644 (file)
@@ -30,6 +30,40 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                return $mockStore;
        }
 
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|ActorMigration
+        */
+       private function getMockActorMigration() {
+               $mockStore = $this->getMockBuilder( ActorMigration::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $mockStore->expects( $this->any() )
+                       ->method( 'getJoin' )
+                       ->willReturn( [
+                               'tables' => [ 'actormigration' => 'table' ],
+                               'fields' => [
+                                       'rc_user' => 'actormigration_user',
+                                       'rc_user_text' => 'actormigration_user_text',
+                                       'rc_actor' => 'actormigration_actor',
+                               ],
+                               'joins' => [ 'actormigration' => 'join' ],
+                       ] );
+               $mockStore->expects( $this->any() )
+                       ->method( 'getWhere' )
+                       ->willReturn( [
+                               'tables' => [ 'actormigration' => 'table' ],
+                               'conds' => 'actormigration_conds',
+                               'joins' => [ 'actormigration' => 'join' ],
+                       ] );
+               $mockStore->expects( $this->any() )
+                       ->method( 'isAnon' )
+                       ->willReturn( 'actormigration is anon' );
+               $mockStore->expects( $this->any() )
+                       ->method( 'isNotAnon' )
+                       ->willReturn( 'actormigration is not anon' );
+               return $mockStore;
+       }
+
        /**
         * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
         * @return WatchedItemQueryService
@@ -37,7 +71,8 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
        private function newService( $mockDb ) {
                return new WatchedItemQueryService(
                        $this->getMockLoadBalancer( $mockDb ),
-                       $this->getMockCommentStore()
+                       $this->getMockCommentStore(),
+                       $this->getMockActorMigration()
                );
        }
 
@@ -57,10 +92,17 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                        )
                        ->will( $this->returnCallback( function ( $a, $conj ) {
                                $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
-                               return implode( $sqlConj, array_map( function ( $s ) {
-                                       return '(' . $s . ')';
-                               }, $a
-                               ) );
+                               $conds = [];
+                               foreach ( $a as $k => $v ) {
+                                       if ( is_int( $k ) ) {
+                                               $conds[] = "($v)";
+                                       } elseif ( is_array( $v ) ) {
+                                               $conds[] = "($k IN ('" . implode( "','", $v ) . "'))";
+                                       } else {
+                                               $conds[] = "($k = '$v')";
+                                       }
+                               }
+                               return implode( $sqlConj, $conds );
                        } ) );
 
                $mock->expects( $this->any() )
@@ -490,20 +532,20 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
                                null,
-                               [],
-                               [ 'rc_user_text' ],
-                               [],
+                               [ 'actormigration' => 'table' ],
+                               [ 'rc_user_text' => 'actormigration_user_text' ],
                                [],
                                [],
+                               [ 'actormigration' => 'join' ],
                        ],
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
                                null,
-                               [],
-                               [ 'rc_user' ],
-                               [],
+                               [ 'actormigration' => 'table' ],
+                               [ 'rc_user' => 'actormigration_user' ],
                                [],
                                [],
+                               [ 'actormigration' => 'join' ],
                        ],
                        [
                                [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
@@ -705,20 +747,20 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
                                null,
+                               [ 'actormigration' => 'table' ],
                                [],
+                               [ 'actormigration is anon' ],
                                [],
-                               [ 'rc_user = 0' ],
-                               [],
-                               [],
+                               [ 'actormigration' => 'join' ],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
                                null,
+                               [ 'actormigration' => 'table' ],
                                [],
+                               [ 'actormigration is not anon' ],
                                [],
-                               [ 'rc_user != 0' ],
-                               [],
-                               [],
+                               [ 'actormigration' => 'join' ],
                        ],
                        [
                                [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
@@ -759,20 +801,20 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                        [
                                [ 'onlyByUser' => 'SomeOtherUser' ],
                                null,
+                               [ 'actormigration' => 'table' ],
                                [],
+                               [ 'actormigration_conds' ],
                                [],
-                               [ 'rc_user_text' => 'SomeOtherUser' ],
-                               [],
-                               [],
+                               [ 'actormigration' => 'join' ],
                        ],
                        [
                                [ 'notByUser' => 'SomeOtherUser' ],
                                null,
+                               [ 'actormigration' => 'table' ],
                                [],
+                               [ 'NOT(actormigration_conds)' ],
                                [],
-                               [ "rc_user_text != 'SomeOtherUser'" ],
-                               [],
-                               [],
+                               [ 'actormigration' => 'join' ],
                        ],
                        [
                                [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
@@ -984,62 +1026,74 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                        [
                                [],
                                'deletedhistory',
+                               [],
                                [
                                        '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
                                                LogPage::DELETED_ACTION . ')'
                                ],
+                               [],
                        ],
                        [
                                [],
                                'suppressrevision',
+                               [],
                                [
                                        '(rc_type != ' . RC_LOG . ') OR (' .
                                                '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
                                                ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
                                ],
+                               [],
                        ],
                        [
                                [],
                                'viewsuppressed',
+                               [],
                                [
                                        '(rc_type != ' . RC_LOG . ') OR (' .
                                                '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
                                                ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
                                ],
+                               [],
                        ],
                        [
                                [ 'onlyByUser' => 'SomeOtherUser' ],
                                'deletedhistory',
+                               [ 'actormigration' => 'table' ],
                                [
-                                       'rc_user_text' => 'SomeOtherUser',
+                                       'actormigration_conds',
                                        '(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER,
                                        '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
                                                LogPage::DELETED_ACTION . ')'
                                ],
+                               [ 'actormigration' => 'join' ],
                        ],
                        [
                                [ 'onlyByUser' => 'SomeOtherUser' ],
                                'suppressrevision',
+                               [ 'actormigration' => 'table' ],
                                [
-                                       'rc_user_text' => 'SomeOtherUser',
+                                       'actormigration_conds',
                                        '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
                                                ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
                                        '(rc_type != ' . RC_LOG . ') OR (' .
                                                '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
                                                ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
                                ],
+                               [ 'actormigration' => 'join' ],
                        ],
                        [
                                [ 'onlyByUser' => 'SomeOtherUser' ],
                                'viewsuppressed',
+                               [ 'actormigration' => 'table' ],
                                [
-                                       'rc_user_text' => 'SomeOtherUser',
+                                       'actormigration_conds',
                                        '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
                                                ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
                                        '(rc_type != ' . RC_LOG . ') OR (' .
                                                '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
                                                ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
                                ],
+                               [ 'actormigration' => 'join' ],
                        ],
                ];
        }
@@ -1050,7 +1104,9 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
        public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
                array $options,
                $notAllowedAction,
-               array $expectedExtraConds
+               array $expectedExtraTables,
+               array $expectedExtraConds,
+               array $expectedExtraJoins
        ) {
                $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
                $conds = array_merge( $commonConds, $expectedExtraConds );
@@ -1059,12 +1115,15 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                $mockDb->expects( $this->once() )
                        ->method( 'select' )
                        ->with(
-                               [ 'recentchanges', 'watchlist', 'page' ],
+                               array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
                                $this->isType( 'array' ),
                                $conds,
                                $this->isType( 'string' ),
                                $this->isType( 'array' ),
-                               $this->isType( 'array' )
+                               array_merge( [
+                                       'watchlist' => [ 'INNER JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
+                                       'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
+                               ], $expectedExtraJoins )
                        )
                        ->will( $this->returnValue( [] ) );