* 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 ===
'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',
'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',
'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',
--- /dev/null
+<?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,
+ ];
+ }
+
+}
* @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',
*/
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',
'ipb_block_email',
'ipb_allow_usertalk',
'ipb_parent_block_id',
- ] + $commentQuery['fields'],
- 'joins' => $commentQuery['joins'],
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
];
}
*/
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;
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" );
$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(),
'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;
}
*/
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() );
}
/**
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
/**
* 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;
*/
$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
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 ) ) {
$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__,
'USE INDEX' => [ 'revision' => 'page_timestamp' ],
'ORDER BY' => 'rev_timestamp DESC',
'LIMIT' => $wgShowRollbackEditCount + 1
- ]
+ ],
+ $revQuery['joins']
);
$editCount = 0;
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
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;
* @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' ] ];
}
* @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' );
'rev_timestamp',
'rev_user_text',
'rev_user',
+ 'rev_actor' => 'NULL',
'rev_minor_edit',
'rev_deleted',
'rev_len',
* @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' );
'ar_timestamp',
'ar_user_text',
'ar_user',
+ 'ar_actor' => 'NULL',
'ar_minor_edit',
'ar_deleted',
'ar_len',
*/
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' );
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
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
*/
'WatchedItemQueryService' => function ( MediaWikiServices $services ) {
return new WatchedItemQueryService(
$services->getDBLoadBalancer(),
- $services->getCommentStore()
+ $services->getCommentStore(),
+ $services->getActorMigration()
);
},
$services->getDBLoadBalancer(),
$blobStore,
$services->getMainWANObjectCache(),
- $services->getCommentStore()
+ $services->getCommentStore(),
+ $services->getActorMigration()
);
$store->setLogger( LoggerFactory::getInstance( 'RevisionStore' ) );
$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
namespace MediaWiki\Storage;
+use ActorMigration;
use CommentStore;
use CommentStoreComment;
use Content;
*/
private $commentStore;
+ /**
+ * @var ActorMigration
+ */
+ private $actorMigration;
+
/**
* @var LoggerInterface
*/
* @param SqlBlobStore $blobStore
* @param WANObjectCache $cache
* @param CommentStore $commentStore
+ * @param ActorMigration $actorMigration
* @param bool|string $wikiId
*/
public function __construct(
SqlBlobStore $blobStore,
WANObjectCache $cache,
CommentStore $commentStore,
+ ActorMigration $actorMigration,
$wikiId = false
) {
Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
$this->blobStore = $blobStore;
$this->cache = $cache;
$this->commentStore = $commentStore;
+ $this->actorMigration = $actorMigration;
$this->wikiId = $wikiId;
$this->logger = new NullLogger();
}
$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,
$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
$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__ );
}
$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,
'page' => $title->getArticleID(),
'user_text' => $user->getName(),
'user' => $user->getId(),
+ 'actor' => $user->getActorId(),
'comment' => $comment,
'minor_edit' => $minor,
'text_id' => $current->rev_text_id,
}
// 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()
],
'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',
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 );
}
$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()
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
*
}
}
- $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()
}
}
- // 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 )
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;
}
}
'rev_page',
'rev_text_id',
'rev_timestamp',
- 'rev_user_text',
- 'rev_user',
'rev_minor_edit',
'rev_deleted',
'rev_len',
$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';
$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 ) ) {
*/
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',
'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 ) {
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 ) {
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;
}
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();
$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 = [];
$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
);
}
// 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)
}
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'] ) ) {
$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'] );
// 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() )
]
] ] );
}
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 );
}
}
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 ) {
}
public function execute() {
+ global $wgActorTableSchemaMigrationStage;
+
$params = $this->extractRequestParams();
$activeUserDays = $this->getConfig()->get( 'ActiveUserDays' );
] ] );
// 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
) . ')'
] );
}
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'] );
$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',
}
public function execute() {
+ global $wgActorTableSchemaMigrationStage;
+
$db = $this->getDB();
$params = $this->extractRequestParams();
$this->requireMaxOneParameter( $params, 'group', 'excludegroup', 'rights', 'excluderights' );
}
$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 ],
// 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 = [];
$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() )
]
$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)"
);
}
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;
}
}
}
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'] ) ) {
$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 );
}
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'] ) ) {
}
$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?
$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' ] ] ] );
// 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
$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'];
$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'] ) );
$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. */
/* 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 );
$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)
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();
// 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(
}
if ( User::isIP( $u ) ) {
- $anyIPs = true;
- $this->usernames[] = $u;
+ $names[$u] = null;
} else {
$name = User::getCanonicalName( $u, 'valid' );
if ( $name === false ) {
[ '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 );
$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' ) ) {
$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'] );
$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
$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' );
}
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;
}
/**
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";
* @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 );
* @param string $caller The calling method
*/
public function doQuery( array $userIds, $options = [], $caller = '' ) {
+ global $wgActorTableSchemaMigrationStage;
+
$usersToCheck = [];
$usersToQuery = [];
// 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;
}
}
'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() );
* 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)
* @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',
*/
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',
'rc_log_type',
'rc_log_action',
'rc_params',
- ] + $commentQuery['fields'],
- 'joins' => $commentQuery['joins'],
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
];
}
*/
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' );
}
}
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'] );
'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,
'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,
'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,
'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,
$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();
}
/**
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;
}
return 'log_user_text';
}
+ public function getAuthorActorField() {
+ return 'log_actor';
+ }
+
public function canView() {
return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED, $this->list->getUser() );
}
// 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 ) {
}
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 ],
$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',
--- /dev/null
+<?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 {
+}
$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 ) {
$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 ) {
}
# 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 = '>';
}
# 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'] );
# 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...
}
} 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 );
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 );
/** @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;
$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;
* @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',
'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 */
*/
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',
'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'],
];
}
$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 ) ) {
* @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'." );
* @return int
*/
public function getRawUser() {
- $this->load();
-
- return $this->user;
+ return $this->getUser( 'id' );
}
/**
* @return string
*/
public function getRawUserText() {
- $this->load();
-
- return $this->user_text;
+ return $this->getUser( 'text' );
}
/**
/** @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;
* @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',
'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' );
*/
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',
'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 ) ) {
$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.
// 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' ];
}
/**
$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'] );
}
}
+ 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']}";
}
/**
- * 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'." );
}
/**
function recordUpload2(
$oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
) {
- global $wgCommentTableSchemaMigrationStage;
+ global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
if ( is_null( $user ) ) {
global $wgUser;
$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 );
$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(),
'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'
);
'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',
}
}
+ 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
'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__
);
}
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() );
'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'
];
}
}
+ 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 );
}
$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',
'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 );
}
}
$dbw = $this->file->repo->getMasterDB();
$commentStore = CommentStore::getStore();
+ $actorMigration = ActorMigration::newMigration();
$status = $this->file->repo->newGood();
}
$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,
'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 ) {
'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'],
'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;
* @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',
'oi_minor_mime',
'oi_user',
'oi_user_text',
+ 'oi_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'oi_actor' : null,
'oi_timestamp',
'oi_deleted',
'oi_sha1',
*/
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',
'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 ) ) {
}
$commentFields = CommentStore::getStore()->insert( $dbw, 'oi_description', $comment );
+ $actorFields = ActorMigration::newMigration()->getInsertValues( $dbw, 'oi_user', $user );
$dbw->insert( 'oldimage',
[
'oi_name' => $this->getName(),
'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;
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() ) {
'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__
);
'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;
);
$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" );
}
}
[ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
[ 'addTable', 'content_models', 'patch-content_models.sql' ],
[ 'migrateArchiveText' ],
+ [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+ [ 'migrateActors' ],
];
}
[ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
[ 'addTable', 'content_models', 'patch-content_models.sql' ],
[ 'migrateArchiveText' ],
+ [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+ [ 'migrateActors' ],
];
}
[ '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' ],
[ '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' ],
];
}
[ 'addTable', 'slots', 'patch-slots.sql' ],
[ 'addTable', 'slot_roles', 'patch-slot_roles.sql' ],
[ 'migrateArchiveText' ],
+ [ 'addTable', 'actor', 'patch-actor-table.sql' ],
+ [ 'migrateActors' ],
];
}
$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 ) ),
[
'GROUP BY' => [ 'rc_user_text' ],
'ORDER BY' => 'NULL' // avoid filesort
- ]
+ ],
+ $actorQuery['joins']
);
$names = [];
foreach ( $res as $row ) {
* 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:
*/
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,
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 );
}
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;
* @throws MWException
*/
public function insert( IDatabase $dbw = null ) {
+ global $wgActorTableSchemaMigrationStage;
+
$dbw = $dbw ?: wfGetDB( DB_MASTER );
if ( $this->timestamp === null ) {
$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 ) {
'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(),
$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();
'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();
// 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;
$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)',
];
// 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',
];
$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" );
}
$commentStore = CommentStore::getStore();
+ $actorMigration = ActorMigration::newMigration();
$revQuery = Revision::getQueryInfo();
$bitfield = false;
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,
'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;
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 ) {
// 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' ] ];
}
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__
);
return 'ar_user_text';
}
+ public function getAuthorActorField() {
+ return 'ar_actor';
+ }
+
public function getId() {
# Convert DB timestamp to MW timestamp
return $this->revision->getTimestamp();
return 'fa_user_text';
}
+ public function getAuthorActorField() {
+ return 'fa_actor';
+ }
+
public function getId() {
return $this->row->fa_id;
}
return 'oi_user_text';
}
+ public function getAuthorActorField() {
+ return 'oi_actor';
+ }
+
public function getId() {
$parts = explode( '!', $this->row->oi_archive_name );
* @since 1.23 Added 'perItemStatus' param
*/
public function setVisibility( array $params ) {
+ global $wgActorTableSchemaMigrationStage;
+
$status = Status::newGood();
$bitPars = $params['value'];
$missing = array_flip( $this->ids );
$this->clearFileOps();
$idsForLog = [];
- $authorIds = $authorIPs = [];
+ $authorIds = $authorIPs = $authorActors = [];
if ( $perItemStatus ) {
$status->itemStatuses = [];
$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
}
// 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,
[
'oldBits' => $virtualOldBits,
'comment' => $comment,
'ids' => $idsForLog,
- 'authorIds' => $authorIds,
- 'authorIPs' => $authorIPs,
'tags' => isset( $params['tags'] ) ? $params['tags'] : [],
- ]
+ ] + $authorFields
);
// Clear caches after commit
* 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
*/
$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();
return 'log_user_text';
}
+ public function getAuthorActorField() {
+ return 'log_actor';
+ }
+
public function canView() {
return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED, $this->list->getUser() );
}
$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']
);
}
return 'rev_user_text';
}
+ public function getAuthorActorField() {
+ return 'rev_actor';
+ }
+
public function canView() {
return $this->revision->userCan( Revision::DELETED_RESTRICTED, $this->list->getUser() );
}
* @return bool
*/
private static function setUsernameBitfields( $name, $userId, $op, $dbw ) {
+ global $wgActorTableSchemaMigrationStage;
+
if ( !$userId || ( $op !== '|' && $op !== '&' ) ) {
return false; // sanity check
}
$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 ) ],
__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;
}
'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,
'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,
]
'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 ) {
'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 ) {
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();
if ( in_array( 'unregistered', $selectedExpLevels ) ) {
$selectedExpLevels = array_diff( $selectedExpLevels, [ 'unregistered' ] );
- $conditions[] = 'rc_user = 0';
+ $conditions[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
}
if ( $selectedExpLevels === [ 'newcomer' ] ) {
} 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 ) {
}
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'],
];
}
}
public function execute( $par ) {
+ global $wgActorTableSchemaMigrationStage;
+
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addModules( 'mediawiki.userSuggest' );
# 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 {
// 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',
'img_size',
'img_width',
'img_height',
- 'img_user_text',
+ 'img_user_text' => $imgQuery['fields']['img_user_text'],
'img_timestamp'
],
'conds' => [
MEDIATYPE_3D,
],
] + $minorType,
+ 'join_conds' => $imgQuery['joins'],
];
return $qi;
'deleted' => $result->rc_deleted,
'user_text' => $result->rc_user_text,
'user' => $result->rc_user,
+ 'actor' => $result->rc_actor,
], 0, $title );
}
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 );
// 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;
// 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;
}
}
'log_user_text' => 'user'
];
- foreach ( $logparams as $logKey ) {
+ foreach ( $logparams as $logKey => $dummy ) {
$query[$keys[$logKey]] = $matchedRows[0]->$logKey;
}
$query['offset'] = $query['offset'] + 1;
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' ),
'recentedits' => 'COUNT(*)'
],
'options' => [ 'GROUP BY' => [ 'qcc_title' ] ],
- 'conds' => $conds
+ 'conds' => $conds,
+ 'join_conds' => $jconds,
];
}
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',
'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
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' );
$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() )
$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'];
}
}
}
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)
}
$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'],
];
}
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';
}
'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,
}
$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.
$tables = [ $table ];
$fields = $this->getFieldNames();
unset( $fields['img_description'] );
+ unset( $fields['img_user_text'] );
$fields = array_keys( $fields );
if ( $table === 'oldimage' ) {
$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 = [];
$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'] ) ) {
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' ];
}
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
'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() )
]
];
}
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'
]
];
}
function getQueryInfo() {
+ $rcQuery = RecentChange::getQueryInfo();
+
$conds = [];
$conds['rc_new'] = 1;
}
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!
$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;
}
$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',
'pr_expiry',
'pr_cascade',
'log_timestamp',
- 'log_user',
'log_deleted',
- ] + $commentQuery['fields'],
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
'conds' => $conds,
'join_conds' => [
'log_search' => [
'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']
];
}
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.
/**
* @const int Serialized record version.
*/
- const VERSION = 11;
+ const VERSION = 12;
/**
* Exclude user options that are set to their default value.
'mGroupMemberships',
// user_properties table
'mOptionOverrides',
+ // actor table
+ 'mActorId',
];
/**
public $mId;
/** @var string */
public $mName;
+ /** @var int|null */
+ protected $mActorId;
/** @var string */
public $mRealName;
* - '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.
*
* @see newFromName()
* @see newFromId()
+ * @see newFromActorId()
* @see newFromConfirmationCode()
* @see newFromSession()
* @see newFromRow()
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.
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
public function loadDefaults( $name = false ) {
$this->mId = 0;
$this->mName = $name;
+ $this->mActorId = null;
$this->mRealName = '';
$this->mEmail = '';
$this->mOptionOverrides = null;
* 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' );
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;
}
* 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;
$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.
$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();
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;
+ } );
}
/**
$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.
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
// 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;
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
*/
public static function getQueryInfo() {
- return [
+ global $wgActorTableSchemaMigrationStage;
+
+ $ret = [
'tables' => [ 'user' ],
'fields' => [
'user_id',
],
'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;
}
/**
*/
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.
}
*/
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;
}
/**
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;
+ }
+
}
/** @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;
}
/**
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;
}
$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'];
}
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() ) {
$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)
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;
}
--- /dev/null
+--
+-- 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);
$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 ) {
$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 ) {
}
public function execute() {
+ global $wgActorTableSchemaMigrationStage;
+
$dbw = $this->getDB( DB_MASTER );
- $user = $dbw->tableName( 'user' );
- $revision = $dbw->tableName( 'revision' );
// Autodetect mode...
if ( $this->hasOption( 'background' ) ) {
$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" );
$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',
}
} 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" );
--- /dev/null
+<?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;
--- /dev/null
+--
+-- 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);
-- 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
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
);
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.
--
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,
);
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);
-- 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 '',
-- 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 '',
);
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
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,
);
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);
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
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);
-- 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,
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);
-- 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,
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,'');
--- /dev/null
+--
+-- 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);
(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,
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,
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),
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
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,
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)
);
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,
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,
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);
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)
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
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),
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
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,
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)
}
$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 ) {
$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();
}
protected function doDBUpdates() {
+ global $wgActorTableSchemaMigrationStage;
+
$batchSize = $this->getBatchSize();
$db = $this->getDB( DB_MASTER );
if ( !$db->tableExists( 'log_search' ) ) {
'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 ) {
$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 );
$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;
}
$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;
--- /dev/null
+--
+-- 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);
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;
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,
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,
);
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 (
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,
);
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 (
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,
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 ''
);
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,
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 ''
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 '',
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 '',
);
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,
* @licence GNU General Public Licence 2.0 or later
*/
+use Wikimedia\Rdbms\IDatabase;
+
require_once __DIR__ . '/Maintenance.php';
/**
* @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;
# 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;
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" );
}
}
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;
}
/**
$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',
'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 ) )
[ '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,
'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 ) {
$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',
'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 ) ),
[
'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,
'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__
);
$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
# 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(
# 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();
+ }
}
}
}
}
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 {
# 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__ );
* (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',
];
$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__ );
/**
* 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;
--- /dev/null
+--
+-- 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;
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
-- 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
-- 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
-- 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,
-- 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.
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 '',
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 '',
-- 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
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,
) /*$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));
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
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));
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,
-- 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);
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.
-- 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);
-- 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?
* @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',
$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' );
}
*/
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.
--- /dev/null
+<?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' ],
+ ];
+ }
+
+}
$blockOptions = [
'address' => $user->getName(),
'user' => $user->getId(),
+ 'by' => $this->getTestSysop()->getUser()->getId(),
'reason' => 'Parce que',
'expiry' => time() + 100500,
];
$block = new Block(
/* address */ $username,
/* user */ 0,
- /* by */ 0,
+ /* by */ $this->getTestSysop()->getUser()->getId(),
/* reason */ $reason,
/* timestamp */ 0,
/* auto */ false,
$ipbfields = [
'ipb_range_start' => '',
'ipb_range_end' => '',
- 'ipb_by' => 0,
'ipb_timestamp' => $db->timestamp(),
'ipb_expiry' => $db->getInfinity(),
];
'rev_page' => 42,
'rev_text_id' => 42,
'rev_len' => 0,
- 'rev_user' => 0,
- 'rev_user_text' => '',
'rev_timestamp' => $db->timestamp(),
];
$comStoreComment = new CommentStoreComment(
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 );
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 );
}
'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',
'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',
}
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'] ) ) {
'rev_id',
'rev_page',
'rev_text_id',
- 'rev_user',
'rev_minor_edit',
'rev_deleted',
'rev_len',
strval( $textId ),
'0',
'0',
- '0',
'13',
strval( $parentId ),
's0ngbdoxagreuf2vjtuxzwdz64n29xm',
$services->getDBLoadBalancer(),
$services->getService( '_SqlBlobStore' ),
$services->getMainWANObjectCache(),
- $services->getCommentStore()
+ $services->getCommentStore(),
+ $services->getActorMigration()
);
$store->setContentHandlerUseDB( $this->getContentHandlerUseDB() );
// 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 );
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.' )
$lb,
$this->getBlobStore(),
$cache,
- MediaWikiServices::getInstance()->getCommentStore()
+ MediaWikiServices::getInstance()->getCommentStore(),
+ MediaWikiServices::getInstance()->getActorMigration()
);
return $blobStore;
}
*/
public function testLoadFromTitle() {
$this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
$this->overrideMwServices();
$title = $this->getMockTitle();
*/
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()
);
}
- private function overrideCommentStore() {
+ private function overrideCommentStoreAndActorMigration() {
$mockStore = $this->getMockBuilder( CommentStore::class )
->disableOriginalConstructor()
->getMock();
'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() {
'rev_timestamp',
'rev_user_text',
'rev_user',
+ 'rev_actor' => 'NULL',
'rev_minor_edit',
'rev_deleted',
'rev_len',
'rev_timestamp',
'rev_user_text',
'rev_user',
+ 'rev_actor' => 'NULL',
'rev_minor_edit',
'rev_deleted',
'rev_len',
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() );
}
'ar_timestamp',
'ar_user_text',
'ar_user',
+ 'ar_actor' => 'NULL',
'ar_minor_edit',
'ar_deleted',
'ar_len',
'ar_timestamp',
'ar_user_text',
'ar_user',
+ 'ar_actor' => 'NULL',
'ar_minor_edit',
'ar_deleted',
'ar_len',
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() );
}
'tables' => [
'archive',
'commentstore' => 'table',
+ 'actormigration' => 'table',
],
'fields' => [
'ar_id',
'ar_text',
'ar_text_id',
'ar_timestamp',
- 'ar_user_text',
- 'ar_user',
'ar_minor_edit',
'ar_deleted',
'ar_len',
'ar_parent_id',
'ar_sha1',
- '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' => [
'tables' => [
'archive',
'commentstore' => 'table',
+ 'actormigration' => 'table',
],
'fields' => [
'ar_id',
'ar_text',
'ar_text_id',
'ar_timestamp',
- 'ar_user_text',
- 'ar_user',
'ar_minor_edit',
'ar_deleted',
'ar_len',
'ar_parent_id',
'ar_sha1',
'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' ],
]
];
}
*/
public function testGetArchiveQueryInfo( $globals, $expected ) {
$this->setMwGlobals( $globals );
- $this->overrideCommentStore();
+ $this->overrideCommentStoreAndActorMigration();
$revisionStore = $this->getRevisionStore();
$revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
],
[],
[
- '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' => [
],
[ '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',
[ 'page_id = rev_page' ],
],
'commentstore' => 'join',
+ 'actormigration' => 'join',
],
],
];
],
[ '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',
],
],
];
],
[ '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',
],
[ 'rev_text_id=old_id' ],
],
'commentstore' => 'join',
+ 'actormigration' => 'join',
],
],
];
],
[ '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',
'user' => [
'LEFT JOIN',
[
- 'rev_user != 0',
- 'user_id = rev_user',
+ 'actormigration_user != 0',
+ 'user_id = actormigration_user',
],
],
'text' => [
[ 'rev_text_id=old_id' ],
],
'commentstore' => 'join',
+ 'actormigration' => 'join',
],
],
];
],
[],
[
- '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' ],
],
];
}
*/
public function testGetQueryInfo( $globals, $options, $expected ) {
$this->setMwGlobals( $globals );
- $this->overrideCommentStore();
+ $this->overrideCommentStoreAndActorMigration();
$revisionStore = $this->getRevisionStore();
$revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
$blobStore,
new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
MediaWikiServices::getInstance()->getCommentStore(),
+ MediaWikiServices::getInstance()->getActorMigration(),
$wikiId
);
* @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();
* @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 */
$page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
/** @var Revision $rev */
$rev = $page->doEditContent(
- new WikitextContent( __METHOD__. 'b' ),
+ new WikitextContent( __METHOD__ . 'b' ),
__METHOD__ . 'b',
0,
false,
$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' ) );
$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' ) );
$title = Title::newFromText( 'Dummy' );
$title->resetArticleID( 17 );
- $user = new UserIdentityValue( 11, 'Tester' );
+ $user = new UserIdentityValue( 11, 'Tester', 0 );
$comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
return new RevisionStoreRecord(
$title,
- new UserIdentityValue( 11, __METHOD__ ),
+ new UserIdentityValue( 11, __METHOD__, 0 ),
CommentStoreComment::newUnsavedComment( __METHOD__ ),
(object)[
'rev_id' => strval( $revId ),
$loadBalancer ? $loadBalancer : $this->getMockLoadBalancer(),
$blobStore ? $blobStore : $this->getMockSqlBlobStore(),
$WANObjectCache ? $WANObjectCache : $this->getHashWANObjectCache(),
- MediaWikiServices::getInstance()->getCommentStore()
+ MediaWikiServices::getInstance()->getCommentStore(),
+ MediaWikiServices::getInstance()->getActorMigration()
);
}
'rev_page',
'rev_text_id',
'rev_timestamp',
- 'rev_user_text',
- 'rev_user',
'rev_minor_edit',
'rev_deleted',
'rev_len',
];
}
+ private function getActorQueryFields() {
+ return [
+ 'rev_user' => 'rev_user',
+ 'rev_user_text' => 'rev_user_text',
+ 'rev_actor' => 'NULL',
+ ];
+ }
+
private function getContentHandlerQueryFields() {
return [
'rev_content_format',
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
+ $this->getActorQueryFields(),
$this->getContentHandlerQueryFields()
),
'joins' => [],
'tables' => [ 'revision' ],
'fields' => array_merge(
$this->getDefaultQueryFields(),
- $this->getCommentQueryFields()
+ $this->getCommentQueryFields(),
+ $this->getActorQueryFields()
),
'joins' => [],
]
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
+ $this->getActorQueryFields(),
[
'page_namespace',
'page_title',
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
+ $this->getActorQueryFields(),
[
'user_name',
]
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
+ $this->getActorQueryFields(),
[
'old_text',
'old_flags',
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
+ $this->getActorQueryFields(),
$this->getContentHandlerQueryFields(),
[
'page_namespace',
*/
public function testGetQueryInfo( $contentHandlerUseDb, $options, $expected ) {
$this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
$this->overrideMwServices();
$store = $this->getRevisionStore();
$store->setContentHandlerUseDB( $contentHandlerUseDb );
'ar_text',
'ar_text_id',
'ar_timestamp',
- 'ar_user_text',
- 'ar_user',
'ar_minor_edit',
'ar_deleted',
'ar_len',
*/
public function testGetArchiveQueryInfo_contentHandlerDb() {
$this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
$this->overrideMwServices();
$store = $this->getRevisionStore();
$store->setContentHandlerUseDB( true );
'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',
]
*/
public function testGetArchiveQueryInfo_noContentHandlerDb() {
$this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
$this->overrideMwServices();
$store = $this->getRevisionStore();
$store->setContentHandlerUseDB( false );
'ar_comment_text' => 'ar_comment',
'ar_comment_data' => 'NULL',
'ar_comment_cid' => 'NULL',
+ 'ar_user_text' => 'ar_user_text',
+ 'ar_user' => 'ar_user',
+ 'ar_actor' => 'NULL',
]
),
'joins' => [],
$block = new \Block( [
'address' => $user->getName(),
'user' => $user->getID(),
+ 'by' => $this->getTestSysop()->getUser()->getId(),
'reason' => __METHOD__,
'expiry' => time() + 100500,
] );
'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,
'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,
--- /dev/null
+<?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 ];
+ }
+ }
+ }
+}
$blockOptions = [
'address' => 'UTBlockee',
'user' => $user->getID(),
+ 'by' => $this->getTestSysop()->getUser()->getId(),
'reason' => __METHOD__,
'expiry' => time() + 100500,
'createAccount' => true,
$blockOptions = [
'address' => '127.0.0.0/24',
+ 'by' => $this->getTestSysop()->getUser()->getId(),
'reason' => __METHOD__,
'expiry' => time() + 100500,
'createAccount' => true,
$blockOptions = [
'address' => 'UTBlockee',
'user' => $user->getID(),
+ 'by' => $this->getTestSysop()->getUser()->getId(),
'reason' => __METHOD__,
'expiry' => time() + 100500,
'createAccount' => true,
$blockOptions = [
'address' => '127.0.0.0/24',
'reason' => __METHOD__,
+ 'by' => $this->getTestSysop()->getUser()->getId(),
'expiry' => time() + 100500,
'createAccount' => true,
];
* @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 );
'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() );
$row->rc_timestamp = '20150921134808';
$row->rc_deleted = 'bar';
$row->rc_comment = 'comment';
+ $row->rc_user = $user->getId();
Wikimedia\suppressWarnings();
$rc = RecentChange::newFromRow( $row );
'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() );
}
$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',
$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 );
'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,
// 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',
],
$user->getName(),
(string)$page->getTitle()->getNamespace(),
$page->getTitle()->getDBkey(),
- ] ]
+ ] ],
+ [],
+ $actorQuery['joins']
);
}
*/
class UserPasswordPolicyTest extends MediaWikiTestCase {
+ protected $tablesUsed = [ 'user', 'user_groups' ];
+
protected $policies = [
'checkuser' => [
'MinimalPasswordLength' => 10,
}
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,
);
$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,
}
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,
);
$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,
}
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',
}
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',
}
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',
}
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',
}
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"
);
]
);
+ // @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' ]
);
* @group Database
*/
class UserGroupMembershipTest extends MediaWikiTestCase {
+
+ protected $tablesUsed = [ 'user', 'user_groups' ];
+
/**
* @var User Belongs to no groups
*/
$this->setMwGlobals( [
'wgGroupPermissions' => [],
'wgRevokePermissions' => [],
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
] );
+ $this->overrideMwServices();
$this->setUpPermissionGlobals();
'enableAutoblock' => true,
'expiry' => wfTimestamp( TS_MW, $expiryFiveHours ),
] );
+ $block->setBlocker( $this->getTestSysop()->getUser() );
$block->setTarget( $user1tmp );
$block->setBlocker( $userBlocker );
$res = $block->insert();
$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();
$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();
$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();
$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();
] );
$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() );
}
);
$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 ) {
+ }
+ }
}
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
private function newService( $mockDb ) {
return new WatchedItemQueryService(
$this->getMockLoadBalancer( $mockDb ),
- $this->getMockCommentStore()
+ $this->getMockCommentStore(),
+ $this->getMockActorMigration()
);
}
)
->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() )
[
[ '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 ] ],
[
[ '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 ] ],
[
[ '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 ],
[
[],
'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' ],
],
];
}
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 );
$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( [] ) );