Schema migration for revison_actor_temp table removal

Introduce a new schema migration stage in which rev_actor is used
directly and the revision_actor_temp table is no longer needed. This
becomes the new "new" stage whereas the previous situation is now
termed SCHEMA_COMPAT_TEMP.

Introduce migrateRevisionActorTemp which copies data from
revision_actor_temp to rev_actor. The code is similar to
migrateImageCommentTemp.php except that it doesn't delete from the old
table.

Partial revert of c29909e59f. That change removed direct
references to $wgActorTableSchemaMigrationStage and made queries
involving revision_actor_temp be unconditional. Such changes need to be
reverted to make the use of revision_actor_temp be conditional again.

In ActorMigrationTest, I compacted provideGetJoin() and
provideGetWhere(), removing most of the duplication between expected
values. I gave all the stages a short name, and mostly used the name in
providers.

Bug: T275246
Change-Id: I7498107dd6433ab7de5bf2e7b3fe2aa5e10e345d
This commit is contained in:
Tim Starling 2021-05-03 15:13:41 +10:00
parent f3bab2f249
commit d3d8dc9965
25 changed files with 893 additions and 558 deletions

View file

@ -1140,6 +1140,7 @@ $wgAutoloadLocalClasses = [
'MigrateComments' => __DIR__ . '/maintenance/migrateComments.php',
'MigrateFileRepoLayout' => __DIR__ . '/maintenance/migrateFileRepoLayout.php',
'MigrateImageCommentTemp' => __DIR__ . '/maintenance/migrateImageCommentTemp.php',
'MigrateRevisionActorTemp' => __DIR__ . '/maintenance/migrateRevisionActorTemp.php',
'MigrateUserGroup' => __DIR__ . '/maintenance/migrateUserGroup.php',
'MimeAnalyzer' => __DIR__ . '/includes/libs/mime/MimeAnalyzer.php',
'MostimagesPage' => __DIR__ . '/includes/specials/SpecialMostimages.php',

View file

@ -87,6 +87,10 @@ class ActorMigration extends ActorMigrationBase {
$stage,
ActorStoreFactory $actorStoreFactory
) {
if ( $stage & SCHEMA_COMPAT_OLD ) {
throw new InvalidArgumentException(
'The old actor table schema is no longer supported' );
}
parent::__construct(
self::FIELD_INFOS,
$stage,

View file

@ -36,8 +36,11 @@ class ActorMigrationBase {
/** @var array[] Cache for `self::getJoin()` */
private $joinCache = [];
/** @var int Combination of SCHEMA_COMPAT_* constants */
private $stage;
/** @var int One of the SCHEMA_COMPAT_READ_* values */
private $readStage;
/** @var int A combination of the SCHEMA_COMPAT_WRITE_* flags */
private $writeStage;
/** @var ActorStoreFactory */
private $actorStoreFactory;
@ -75,7 +78,18 @@ class ActorMigrationBase {
* @stable to override
* @stable to call
*
* @param int $stage The migration stage
* @param int $stage The migration stage. This is a combination of
* SCHEMA_COMPAT_* flags:
* - SCHEMA_COMPAT_READ_OLD, SCHEMA_COMPAT_WRITE_OLD: Use the old schema,
* with *_user and *_user_text fields.
* - SCHEMA_COMPAT_READ_TEMP, SCHEMA_COMPAT_WRITE_TEMP: Use the new schema,
* with an actor table. Normal tables are joined via a *_actor field,
* whereas temp tables are joined to the actor table via an
* intermediate table.
* - SCHEMA_COMPAT_READ_NEW, SCHEMA_COMPAT_WRITE_NEW: Use the new
* schema. Former temp tables are no longer used, and all relevant
* tables join directly to the actor table.
*
* @param ActorStoreFactory $actorStoreFactory
* @param array $options Array of other options. May contain:
* - allowUnknown: Allow fields not present in $fieldInfos. True by default.
@ -89,22 +103,30 @@ class ActorMigrationBase {
$this->fieldInfos = $fieldInfos;
$this->allowUnknown = $options['allowUnknown'] ?? true;
if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) {
$writeStage = $stage & SCHEMA_COMPAT_WRITE_MASK;
$readStage = $stage & SCHEMA_COMPAT_READ_MASK;
if ( $writeStage === 0 ) {
throw new InvalidArgumentException( '$stage must include a write mode' );
}
if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) {
if ( $readStage === 0 ) {
throw new InvalidArgumentException( '$stage must include a read mode' );
}
if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_BOTH ) {
throw new InvalidArgumentException( 'Cannot read both schemas' );
if ( !in_array( $readStage,
[ SCHEMA_COMPAT_READ_OLD, SCHEMA_COMPAT_READ_TEMP, SCHEMA_COMPAT_READ_NEW ] )
) {
throw new InvalidArgumentException( 'Cannot read multiple schemas' );
}
if ( ( $stage & SCHEMA_COMPAT_READ_OLD ) && !( $stage & SCHEMA_COMPAT_WRITE_OLD ) ) {
if ( $readStage === SCHEMA_COMPAT_READ_OLD && !( $writeStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
throw new InvalidArgumentException( 'Cannot read the old schema without also writing it' );
}
if ( ( $stage & SCHEMA_COMPAT_READ_NEW ) && !( $stage & SCHEMA_COMPAT_WRITE_NEW ) ) {
if ( $readStage === SCHEMA_COMPAT_READ_TEMP && !( $writeStage & SCHEMA_COMPAT_WRITE_TEMP ) ) {
throw new InvalidArgumentException( 'Cannot read the temp schema without also writing it' );
}
if ( $readStage === SCHEMA_COMPAT_READ_NEW && !( $writeStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
throw new InvalidArgumentException( 'Cannot read the new schema without also writing it' );
}
$this->stage = $stage;
$this->readStage = $readStage;
$this->writeStage = $writeStage;
$this->actorStoreFactory = $actorStoreFactory;
}
@ -173,7 +195,7 @@ class ActorMigrationBase {
* @return string
*/
public function isAnon( $field ) {
return ( $this->stage & SCHEMA_COMPAT_READ_NEW ) ? "$field IS NULL" : "$field = 0";
return ( $this->readStage >= SCHEMA_COMPAT_READ_TEMP ) ? "$field IS NULL" : "$field = 0";
}
/**
@ -182,7 +204,7 @@ class ActorMigrationBase {
* @return string
*/
public function isNotAnon( $field ) {
return ( $this->stage & SCHEMA_COMPAT_READ_NEW ) ? "$field IS NOT NULL" : "$field != 0";
return ( $this->readStage >= SCHEMA_COMPAT_READ_TEMP ) ? "$field IS NOT NULL" : "$field != 0";
}
/**
@ -230,11 +252,11 @@ class ActorMigrationBase {
list( $text, $actor ) = $this->getFieldNames( $key );
if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) {
if ( $this->readStage === SCHEMA_COMPAT_READ_OLD ) {
$fields[$key] = $key;
$fields[$text] = $text;
$fields[$actor] = 'NULL';
} else {
} elseif ( $this->readStage === SCHEMA_COMPAT_READ_TEMP ) {
$tempTableInfo = $this->getTempTableInfo( $key );
if ( $tempTableInfo ) {
$alias = "temp_$key";
@ -253,6 +275,14 @@ class ActorMigrationBase {
$fields[$key] = "{$alias}.actor_user";
$fields[$text] = "{$alias}.actor_name";
$fields[$actor] = $joinField;
} else /* SCHEMA_COMPAT_READ_NEW */ {
$alias = "actor_$key";
$tables[$alias] = 'actor';
$joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$actor}" ];
$fields[$key] = "{$alias}.actor_user";
$fields[$text] = "{$alias}.actor_name";
$fields[$actor] = $actor;
}
$this->joinCache[$key] = [
@ -283,11 +313,13 @@ class ActorMigrationBase {
list( $text, $actor ) = $this->getFieldNames( $key );
$ret = [];
if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
$ret[$key] = $user->getId();
$ret[$text] = $user->getName();
}
if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_TEMP
|| $this->writeStage & SCHEMA_COMPAT_WRITE_NEW
) {
$ret[$actor] = $this->actorStoreFactory
->getActorNormalization( $dbw->getDomainID() )
->acquireActorId( $user, $dbw );
@ -323,50 +355,59 @@ class ActorMigrationBase {
list( $text, $actor ) = $this->getFieldNames( $key );
$ret = [];
$callback = null;
if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
$ret[$key] = $user->getId();
$ret[$text] = $user->getName();
}
if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
if ( $this->writeStage & ( SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_WRITE_NEW ) ) {
$id = $this->actorStoreFactory
->getActorNormalization( $dbw->getDomainID() )
->acquireActorId( $user, $dbw );
if ( $tempTableInfo ) {
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_TEMP ) {
$func = __METHOD__;
$callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $dbw, $id, $func ) {
$set = [ $tempTableInfo['field'] => $id ];
foreach ( $tempTableInfo['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(
$tempTableInfo['table'],
[ $tempTableInfo['pk'] => $pk ] + $set,
[ [ $tempTableInfo['pk'] ] ],
$set,
$func
);
};
}
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_NEW ) {
$ret[$actor] = $id;
}
} else {
$ret[$actor] = $id;
}
}
if ( $callback === null ) {
// Make a validation-only callback if there was temp table info
if ( $tempTableInfo ) {
$func = __METHOD__;
$callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $dbw, $id, $func ) {
$set = [ $tempTableInfo['field'] => $id ];
$callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $func ) {
foreach ( $tempTableInfo['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(
$tempTableInfo['table'],
[ $tempTableInfo['pk'] => $pk ] + $set,
[ [ $tempTableInfo['pk'] ] ],
$set,
$func
);
};
} else {
$ret[$actor] = $id;
$callback = static function ( $pk, array $extra ) {
};
}
} elseif ( $tempTableInfo ) {
$func = __METHOD__;
$callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $func ) {
foreach ( $tempTableInfo['extra'] as $to => $from ) {
if ( !array_key_exists( $from, $extra ) ) {
throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
}
}
};
} else {
$callback = static function ( $pk, array $extra ) {
};
}
return [ $ret, $callback ];
}
@ -436,7 +477,11 @@ class ActorMigrationBase {
list( $text, $actor ) = $this->getFieldNames( $key );
// Combine data into conditions to be ORed together
if ( $this->stage & SCHEMA_COMPAT_READ_NEW ) {
if ( $this->readStage === SCHEMA_COMPAT_READ_NEW ) {
if ( $actors ) {
$conds['newactor'] = $db->makeList( [ $actor => $actors ], IDatabase::LIST_AND );
}
} elseif ( $this->readStage === SCHEMA_COMPAT_READ_TEMP ) {
if ( $actors ) {
$tempTableInfo = $this->getTempTableInfo( $key );
if ( $tempTableInfo ) {

View file

@ -2390,6 +2390,28 @@ $wgDatabaseReplicaLagCritical = 30;
*/
$wgMultiContentRevisionSchemaMigrationStage = SCHEMA_COMPAT_NEW;
/**
* Actor table schema migration stage, for migration from the temporary table
* revision_actor_temp to the revision.rev_actor field.
*
* Use the SCHEMA_COMPAT_XXX flags. Supported values:
*
* - SCHEMA_COMPAT_TEMP
* - SCHEMA_COMPAT_WRITE_TEMP_AND_NEW | SCHEMA_COMPAT_READ_TEMP
* - SCHEMA_COMPAT_WRITE_TEMP_AND_NEW | SCHEMA_COMPAT_READ_NEW
* - SCHEMA_COMPAT_NEW
*
* History:
* - 1.31: Added
* - 1.32: Now uses SCHEMA_COMPAT_XXX flags
* - 1.34: Removed, implicitly SCHEMA_COMPAT_NEW always
* - 1.37: Re-added with SCHEMA_COMPAT_NEW renamed to SCHEMA_COMPAT_TEMP for
* a new migration which removes temporary tables.
*
* @var int An appropriate combination of SCHEMA_COMPAT_XXX flags.
*/
$wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_TEMP;
// endregion -- End of DB settings
/***************************************************************************/

View file

@ -252,16 +252,29 @@ define( 'SHELL_MAX_ARG_STRLEN', '100000' );
*
* - SCHEMA_COMPAT_WRITE_OLD: Whether information is written to the old schema.
* - SCHEMA_COMPAT_READ_OLD: Whether information stored in the old schema is read.
* - SCHEMA_COMPAT_WRITE_NEW: Whether information is written to the new schema.
* - SCHEMA_COMPAT_READ_NEW: Whether information stored in the new schema is read.
* - SCHEMA_COMPAT_WRITE_TEMP: Whether information is written to a temporary
* intermediate schema.
* - SCHEMA_COMPAT_READ_TEMP: Whether information is read from the temporary
* intermediate schema.
* - SCHEMA_COMPAT_WRITE_NEW: Whether information is written to the new schema
* - SCHEMA_COMPAT_READ_NEW: Whether information is read from the new schema
*/
define( 'SCHEMA_COMPAT_WRITE_OLD', 0x01 );
define( 'SCHEMA_COMPAT_READ_OLD', 0x02 );
define( 'SCHEMA_COMPAT_WRITE_NEW', 0x10 );
define( 'SCHEMA_COMPAT_READ_NEW', 0x20 );
define( 'SCHEMA_COMPAT_WRITE_TEMP', 0x10 );
define( 'SCHEMA_COMPAT_READ_TEMP', 0x20 );
define( 'SCHEMA_COMPAT_WRITE_NEW', 0x100 );
define( 'SCHEMA_COMPAT_READ_NEW', 0x200 );
define( 'SCHEMA_COMPAT_WRITE_MASK',
SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_WRITE_NEW );
define( 'SCHEMA_COMPAT_READ_MASK',
SCHEMA_COMPAT_READ_OLD | SCHEMA_COMPAT_READ_TEMP | SCHEMA_COMPAT_READ_NEW );
define( 'SCHEMA_COMPAT_WRITE_BOTH', SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_WRITE_NEW );
define( 'SCHEMA_COMPAT_WRITE_OLD_AND_TEMP', SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_WRITE_TEMP );
define( 'SCHEMA_COMPAT_WRITE_TEMP_AND_NEW', SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_WRITE_NEW );
define( 'SCHEMA_COMPAT_READ_BOTH', SCHEMA_COMPAT_READ_OLD | SCHEMA_COMPAT_READ_NEW );
define( 'SCHEMA_COMPAT_OLD', SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_OLD );
define( 'SCHEMA_COMPAT_TEMP', SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_READ_TEMP );
define( 'SCHEMA_COMPAT_NEW', SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_NEW );
/** @} */

View file

@ -302,6 +302,8 @@ class MergeHistory {
* @return Status status of the history merge
*/
public function merge( Authority $performer, $reason = '' ) {
global $wgActorTableSchemaMigrationStage;
$status = new Status();
// Check validity and permissions required for merge
@ -333,16 +335,18 @@ class MergeHistory {
}
// Update denormalized revactor_page too
$this->dbw->update(
'revision_actor_temp',
[ 'revactor_page' => $this->dest->getId() ],
[
'revactor_page' => $this->source->getId(),
// Slightly hacky, but should work given the values assigned in this class
str_replace( 'rev_timestamp', 'revactor_timestamp', $this->getTimeWhere() )
],
__METHOD__
);
if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_TEMP ) {
$this->dbw->update(
'revision_actor_temp',
[ 'revactor_page' => $this->dest->getId() ],
[
'revactor_page' => $this->source->getId(),
// Slightly hacky, but should work given the values assigned in this class
str_replace( 'rev_timestamp', 'revactor_timestamp', $this->getTimeWhere() )
],
__METHOD__
);
}
$haveRevisions = $this->dbw->lockForUpdate(
'revision',

View file

@ -2,6 +2,7 @@
namespace MediaWiki\Rest\Handler;
use ActorMigration;
use ChangeTags;
use MediaWiki\Page\ExistingPageRecord;
use MediaWiki\Page\PageLookup;
@ -61,6 +62,9 @@ class PageHistoryCountHandler extends SimpleHandler {
/** @var WANObjectCache */
private $cache;
/** @var ActorMigration */
private $actorMigration;
/** @var RevisionRecord|false|null */
private $revision = false;
@ -77,6 +81,7 @@ class PageHistoryCountHandler extends SimpleHandler {
* @param ILoadBalancer $loadBalancer
* @param WANObjectCache $cache
* @param PageLookup $pageLookup
* @param ActorMigration $actorMigration
*/
public function __construct(
RevisionStore $revisionStore,
@ -84,7 +89,8 @@ class PageHistoryCountHandler extends SimpleHandler {
GroupPermissionsLookup $groupPermissionsLookup,
ILoadBalancer $loadBalancer,
WANObjectCache $cache,
PageLookup $pageLookup
PageLookup $pageLookup,
ActorMigration $actorMigration
) {
$this->revisionStore = $revisionStore;
$this->changeTagDefStore = $nameTableStoreFactory->getChangeTagDef();
@ -92,6 +98,7 @@ class PageHistoryCountHandler extends SimpleHandler {
$this->loadBalancer = $loadBalancer;
$this->cache = $cache;
$this->pageLookup = $pageLookup;
$this->actorMigration = $actorMigration;
}
private function normalizeType( $type ) {
@ -416,6 +423,8 @@ class PageHistoryCountHandler extends SimpleHandler {
protected function getAnonCount( $pageId, RevisionRecord $fromRev = null ) {
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
$revQuery = $this->actorMigration->getJoin( 'rev_user' );
$cond = [
'rev_page' => $pageId,
'actor_user IS NULL',
@ -429,26 +438,22 @@ class PageHistoryCountHandler extends SimpleHandler {
"OR rev_timestamp > {$oldTs}";
}
// This redundant join condition tells MySQL that rev_page and revactor_page are the
// same, so it can propagate the condition
if ( isset( $revQuery['tables']['temp_rev_user'] ) /* SCHEMA_COMPAT_READ_TEMP */ ) {
$revQuery['joins']['temp_rev_user'][1] =
"temp_rev_user.revactor_rev = rev_id AND revactor_page = rev_page";
}
$edits = $dbr->selectRowCount(
[
'revision_actor_temp',
'revision',
'actor'
],
] + $revQuery['tables'],
'1',
$cond,
__METHOD__,
[ 'LIMIT' => self::COUNT_LIMITS['anonymous'] + 1 ], // extra to detect truncation
[
'revision' => [
'JOIN',
'revactor_rev = rev_id AND revactor_page = rev_page'
],
'actor' => [
'JOIN',
'revactor_actor = actor_id'
]
]
$revQuery['joins']
);
return $edits;
}
@ -461,6 +466,15 @@ class PageHistoryCountHandler extends SimpleHandler {
protected function getBotCount( $pageId, RevisionRecord $fromRev = null ) {
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
$revQuery = $this->actorMigration->getJoin( 'rev_user' );
// This redundant join condition tells MySQL that rev_page and revactor_page are the
// same, so it can propagate the condition
if ( isset( $revQuery['tables']['temp_rev_user'] ) /* SCHEMA_COMPAT_READ_TEMP */ ) {
$revQuery['joins']['temp_rev_user'][1] =
"temp_rev_user.revactor_rev = rev_id AND revactor_page = rev_page";
}
$cond = [
'rev_page=' . intval( $pageId ),
$dbr->bitAnd( 'rev_deleted',
@ -470,7 +484,7 @@ class PageHistoryCountHandler extends SimpleHandler {
'user_groups',
'1',
[
'actor.actor_user = ug_user',
$revQuery['fields']['rev_user'] . ' = ug_user',
'ug_group' => $this->groupPermissionsLookup->getGroupsWithPermission( 'bot' ),
'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
],
@ -486,24 +500,13 @@ class PageHistoryCountHandler extends SimpleHandler {
$edits = $dbr->selectRowCount(
[
'revision_actor_temp',
'revision',
'actor',
],
] + $revQuery['tables'],
'1',
$cond,
__METHOD__,
[ 'LIMIT' => self::COUNT_LIMITS['bot'] + 1 ], // extra to detect truncation
[
'revision' => [
'JOIN',
'revactor_rev = rev_id AND revactor_page = rev_page'
],
'actor' => [
'JOIN',
'revactor_actor = actor_id'
],
]
$revQuery['joins']
);
return $edits;
}

View file

@ -193,8 +193,10 @@ class PageHistoryHandler extends SimpleHandler {
if ( $params['filter'] ) {
// This redundant join condition tells MySQL that rev_page and revactor_page are the
// same, so it can propagate the condition
$revQuery['joins']['temp_rev_user'][1] =
"temp_rev_user.revactor_rev = rev_id AND revactor_page = rev_page";
if ( isset( $revQuery['tables']['temp_rev_user'] ) /* SCHEMA_COMPAT_READ_TEMP */ ) {
$revQuery['joins']['temp_rev_user'][1] =
"temp_rev_user.revactor_rev = rev_id AND revactor_page = rev_page";
}
// The validator ensures this value, if present, is one of the expected values
switch ( $params['filter'] ) {

View file

@ -20,7 +20,8 @@
"GroupPermissionsLookup",
"DBLoadBalancer",
"MainWANObjectCache",
"PageStore"
"PageStore",
"ActorMigration"
]
},
{

View file

@ -151,7 +151,7 @@ use Wikimedia\UUID\GlobalIdGenerator;
return [
'ActorMigration' => static function ( MediaWikiServices $services ) : ActorMigration {
return new ActorMigration(
SCHEMA_COMPAT_NEW,
$services->getMainConfig()->get( 'ActorTableSchemaMigrationStage' ),
$services->getActorStoreFactory()
);
},

View file

@ -827,6 +827,8 @@ class InfoAction extends FormlessAction {
self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ),
WANObjectCache::TTL_WEEK,
static function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname, $services ) {
global $wgActorTableSchemaMigrationStage;
$title = $page->getTitle();
$id = $title->getArticleID();
@ -834,10 +836,17 @@ class InfoAction extends FormlessAction {
$dbrWatchlist = wfGetDB( DB_REPLICA, 'watchlist' );
$setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
$tables = [ 'revision_actor_temp' ];
$field = 'revactor_actor';
$pageField = 'revactor_page';
$tsField = 'revactor_timestamp';
if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
$tables = [ 'revision' ];
$field = 'rev_actor';
$pageField = 'rev_page';
$tsField = 'rev_timestamp';
} else /* SCHEMA_COMPAT_READ_TEMP */ {
$tables = [ 'revision_actor_temp' ];
$field = 'revactor_actor';
$pageField = 'revactor_page';
$tsField = 'revactor_timestamp';
}
$joins = [];
$watchedItemStore = $services->getWatchedItemStore();

View file

@ -82,6 +82,8 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
* @return void
*/
protected function run( ApiPageSet $resultPageSet = null ) {
global $wgActorTableSchemaMigrationStage;
$db = $this->getDB();
$params = $this->extractRequestParams( false );
@ -92,7 +94,9 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
$tsField = 'rev_timestamp';
$idField = 'rev_id';
$pageField = 'rev_page';
if ( $params['user'] !== null ) {
if ( $params['user'] !== null &&
( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_TEMP )
) {
// The query is probably best done using the actor_timestamp index on
// revision_actor_temp. Use the denormalized fields from that table.
$tsField = 'revactor_timestamp';

View file

@ -79,6 +79,8 @@ class ApiQueryContributors extends ApiQueryBase {
}
public function execute() {
global $wgActorTableSchemaMigrationStage;
$db = $this->getDB();
$params = $this->extractRequestParams();
$this->requireMaxOneParameter( $params, 'group', 'excludegroup', 'rights', 'excluderights' );
@ -111,10 +113,14 @@ class ApiQueryContributors extends ApiQueryBase {
$result = $this->getResult();
$revQuery = $this->revisionStore->getQueryInfo();
// Target indexes on the revision_actor_temp table.
$pageField = 'revactor_page';
$idField = 'revactor_actor';
$countField = 'revactor_actor';
// For SCHEMA_COMPAT_READ_TEMP, target indexes on the
// revision_actor_temp table, otherwise on the revision table.
$pageField = ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_TEMP )
? 'revactor_page' : 'rev_page';
$idField = ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_TEMP )
? 'revactor_actor' : 'rev_actor';
$countField = ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_TEMP )
? 'revactor_actor' : 'rev_actor';
// First, count anons
$this->addTables( $revQuery['tables'] );

View file

@ -125,6 +125,8 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
}
protected function run( ApiPageSet $resultPageSet = null ) {
global $wgActorTableSchemaMigrationStage;
$params = $this->extractRequestParams( false );
// If any of those parameters are used, work in 'enumeration' mode.
@ -183,7 +185,9 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
];
$useIndex = [];
if ( $params['user'] !== null ) {
if ( $params['user'] !== null &&
( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_TEMP )
) {
// We're going to want to use the page_actor_timestamp index (on revision_actor_temp)
// so use that table's denormalized fields.
$idField = 'revactor_rev';

View file

@ -309,33 +309,42 @@ class ApiQueryUserContribs extends ApiQueryBase {
* @param int $limit
*/
private function prepareQuery( array $users, $limit ) {
global $wgActorTableSchemaMigrationStage;
$this->resetQueryParams();
$db = $this->getDB();
$revQuery = $this->revisionStore->getQueryInfo( [ 'page' ] );
$revWhere = $this->actorMigration->getWhere( $db, 'rev_user', $users );
$orderUserField = 'rev_actor';
$userField = $this->orderBy === 'actor' ? 'revactor_actor' : 'actor_name';
$tsField = 'revactor_timestamp';
$idField = 'revactor_rev';
// T221511: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor`
// before `revision_actor_temp` and filesorting is somehow better than querying $limit+1 rows
// from `revision_actor_temp`. Tell it not to reorder the query (and also reorder it ourselves
// because as generated by RevisionStore it'll have `revision` first rather than
// `revision_actor_temp`). But not when uctag is used, as it seems as likely to be harmed as
// helped in that case, and not when there's only one User because in that case it fetches
// the one `actor` row as a constant and doesn't filesort.
if ( count( $users ) > 1 && !isset( $this->params['tag'] ) ) {
$revQuery['joins']['revision'] = $revQuery['joins']['temp_rev_user'];
unset( $revQuery['joins']['temp_rev_user'] );
$this->addOption( 'STRAIGHT_JOIN' );
// It isn't actually necesssary to reorder $revQuery['tables'] as Database does the right thing
// when join conditions are given for all joins, but Gergő is wary of relying on that so pull
// `revision_actor_temp` to the start.
$revQuery['tables'] =
[ 'temp_rev_user' => $revQuery['tables']['temp_rev_user'] ] + $revQuery['tables'];
if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_TEMP ) {
$orderUserField = 'rev_actor';
$userField = $this->orderBy === 'actor' ? 'revactor_actor' : 'actor_name';
$tsField = 'revactor_timestamp';
$idField = 'revactor_rev';
// T221511: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor`
// before `revision_actor_temp` and filesorting is somehow better than querying $limit+1 rows
// from `revision_actor_temp`. Tell it not to reorder the query (and also reorder it ourselves
// because as generated by RevisionStore it'll have `revision` first rather than
// `revision_actor_temp`). But not when uctag is used, as it seems as likely to be harmed as
// helped in that case, and not when there's only one User because in that case it fetches
// the one `actor` row as a constant and doesn't filesort.
if ( count( $users ) > 1 && !isset( $this->params['tag'] ) ) {
$revQuery['joins']['revision'] = $revQuery['joins']['temp_rev_user'];
unset( $revQuery['joins']['temp_rev_user'] );
$this->addOption( 'STRAIGHT_JOIN' );
// It isn't actually necesssary to reorder $revQuery['tables'] as Database does the right thing
// when join conditions are given for all joins, but Gergő is wary of relying on that so pull
// `revision_actor_temp` to the start.
$revQuery['tables'] =
[ 'temp_rev_user' => $revQuery['tables']['temp_rev_user'] ] + $revQuery['tables'];
}
} else /* SCHEMA_COMPAT_READ_NEW */ {
$orderUserField = 'rev_actor';
$userField = $this->orderBy === 'actor' ? 'rev_actor' : 'actor_name';
$tsField = 'rev_timestamp';
$idField = 'rev_id';
}
$this->addTables( $revQuery['tables'] );

View file

@ -2991,7 +2991,7 @@ class WikiPage implements Page, IDBAccessObject, PageRecord {
* @return bool
*/
protected function archiveRevisions( $dbw, $id, $suppress ) {
global $wgDeleteRevisionsBatchSize;
global $wgDeleteRevisionsBatchSize, $wgActorTableSchemaMigrationStage;
// Given the lock above, we can be confident in the title and page ID values
$namespace = $this->getTitle()->getNamespace();
@ -3087,7 +3087,9 @@ class WikiPage implements Page, IDBAccessObject, PageRecord {
$dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
$dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
$dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_TEMP ) {
$dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
}
// Also delete records from ip_changes as applicable.
if ( count( $ipRevIds ) > 0 ) {

View file

@ -44,6 +44,8 @@ class RevisionDeleteUser {
* @return bool True on success, false on failure (e.g. invalid user ID)
*/
private static function setUsernameBitfields( $name, $userId, $op, IDatabase $dbw = null ) {
global $wgActorTableSchemaMigrationStage;
if ( !$userId || ( $op !== '|' && $op !== '&' ) ) {
return false; // sanity check
}
@ -70,14 +72,27 @@ class RevisionDeleteUser {
$actorId = $dbw->selectField( 'actor', 'actor_id', [ 'actor_name' => $name ], __METHOD__ );
if ( $actorId ) {
# Hide name from live edits
$ids = $dbw->selectFieldValues(
'revision_actor_temp', 'revactor_rev', [ 'revactor_actor' => $actorId ], __METHOD__
);
if ( $ids ) {
# This query depends on the actor migration read stage, not the
# write stage, because the stage determines how we find the rows to
# delete. The write stage determines whether or not to write to
# rev_actor and revision_actor_temp which is not relevant here.
if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_TEMP ) {
$ids = $dbw->selectFieldValues(
'revision_actor_temp', 'revactor_rev', [ 'revactor_actor' => $actorId ], __METHOD__
);
if ( $ids ) {
$dbw->update(
'revision',
[ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ],
[ 'rev_id' => $ids ],
__METHOD__
);
}
} else /* SCHEMA_COMPAT_READ_NEW */ {
$dbw->update(
'revision',
[ self::buildSetBitDeletedField( 'rev_deleted', $op, $delUser, $dbw ) ],
[ 'rev_id' => $ids ],
[ 'rev_actor' => $actorId ],
__METHOD__
);
}

View file

@ -310,7 +310,6 @@ class ContribsPager extends RangeChronologicalPager {
} else {
$conds = $this->actorMigration->getWhere( $dbr, 'rev_user', $user );
if ( isset( $conds['orconds']['actor'] ) ) {
// @todo: This will need changing when revision_actor_temp goes away
return 'revision_actor_temp';
}
}
@ -345,7 +344,6 @@ class ContribsPager extends RangeChronologicalPager {
$queryInfo['conds'][] = $conds['conds'];
// Force the appropriate index to avoid bad query plans (T189026)
if ( isset( $conds['orconds']['actor'] ) ) {
// @todo: This will need changing when revision_actor_temp goes away
$queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
}
}

View file

@ -172,7 +172,7 @@ class UserEditTracker {
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
$actorWhere = $this->actorMigration->getWhere( $dbr, 'rev_user', $user );
$tsField = isset( $actorWhere['tables']['temp_rev_user'] )
$tsField = isset( $actorWhere['tables']['temp_rev_user'] ) // SCHEMA_COMPAT_READ_TEMP
? 'revactor_timestamp' : 'rev_timestamp';
$sortOrder = ( $type === self::FIRST_EDIT ) ? 'ASC' : 'DESC';

View file

@ -60,24 +60,14 @@ class FindMissingActors extends Maintenance {
*/
private $actorNormalization;
private const TABLES = [
// 'rev_actor' => [ 'revision', 'rev_actor', 'rev_id' ], // not yet used in 1.35
'revactor_actor' => [ 'revision_actor_temp', 'revactor_actor', 'revactor_rev' ],
'ar_actor' => [ 'archive', 'ar_actor', 'ar_id' ],
'ipb_by_actor' => [ 'ipblocks', 'ipb_by_actor', 'ipb_id' ], // no index on ipb_by_actor!
'img_actor' => [ 'image', 'img_actor', 'img_name' ],
'oi_actor' => [ 'oldimage', 'oi_actor', 'oi_archive_name' ], // no index on oi_archive_name!
'fa_actor' => [ 'filearchive', 'fa_actor', 'fa_id' ],
'rc_actor' => [ 'recentchanges', 'rc_actor', 'rc_id' ],
'log_actor' => [ 'logging', 'log_actor', 'log_id' ],
];
/** @var array|null */
private $tables;
public function __construct() {
parent::__construct();
$this->addDescription( 'Find and fix invalid actor IDs.' );
$this->addOption( 'field', 'The name of a database field to process. '
. 'Possible values: ' . implode( ', ', array_keys( self::TABLES ) ),
$this->addOption( 'field', 'The name of a database field to process',
true, true );
$this->addOption( 'skip', 'A comma-separated list of actor IDs to skip.',
false, true );
@ -108,6 +98,43 @@ class FindMissingActors extends Maintenance {
$services->getActorNormalization();
}
/**
* @return array
*/
private function getTables() {
global $wgActorTableSchemaMigrationStage;
if ( !$this->tables ) {
$tables = [
'ar_actor' => [ 'archive', 'ar_actor', 'ar_id' ],
'ipb_by_actor' => [ 'ipblocks', 'ipb_by_actor', 'ipb_id' ], // no index on ipb_by_actor!
'img_actor' => [ 'image', 'img_actor', 'img_name' ],
'oi_actor' => [ 'oldimage', 'oi_actor', 'oi_archive_name' ], // no index on oi_archive_name!
'fa_actor' => [ 'filearchive', 'fa_actor', 'fa_id' ],
'rc_actor' => [ 'recentchanges', 'rc_actor', 'rc_id' ],
'log_actor' => [ 'logging', 'log_actor', 'log_id' ],
];
if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_TEMP ) {
$tables['revactor_actor'] = [ 'revision_actor_temp', 'revactor_actor', 'revactor_rev' ];
}
if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
$tables['rev_actor'] = [ 'revision', 'rev_actor', 'rev_id' ];
}
$this->tables = $tables;
}
return $this->tables;
}
/**
* @param string $field
* @return array|null
*/
private function getTableInfo( $field ) {
$tables = $this->getTables();
return $tables[$field] ?? null;
}
/**
* Returns the actor ID of the user specified with the --overwrite-with option,
* or null if --overwrite-with is not set.
@ -153,14 +180,11 @@ class FindMissingActors extends Maintenance {
return $actorId;
}
/**
* @inheritDoc
*/
public function execute() {
$this->initializeServices();
$field = $this->getOption( 'field' );
if ( !isset( self::TABLES[$field] ) ) {
if ( !$this->getTableInfo( $field ) ) {
$this->fatalError( "Unknown field: $field.\n" );
}
@ -195,7 +219,7 @@ class FindMissingActors extends Maintenance {
* @return array a list of row IDs, identifying rows in which the actor ID needs to be replaced.
*/
private function findBadActors( $field, $skip ) {
[ $table, $actorField, $idField ] = self::TABLES[$field];
[ $table, $actorField, $idField ] = $this->getTableInfo( $field );
$this->output( "Finding invalid actor IDs in $table.$actorField...\n" );
$dbr = $this->loadBalancer->getConnectionRef(
@ -269,7 +293,7 @@ class FindMissingActors extends Maintenance {
* @return int
*/
private function overwriteActorIDs( $field, array $ids, int $overwrite ) {
[ $table, $actorField, $idField ] = self::TABLES[$field];
[ $table, $actorField, $idField ] = $this->getTableInfo( $field );
$count = count( $ids );
$this->output( "OVERWRITING $count actor IDs in $table.$actorField with $overwrite...\n" );

View file

@ -0,0 +1,92 @@
<?php
require_once __DIR__ . '/Maintenance.php';
/**
* Maintenance script that merges the revision_actor_temp table into the
* revision table.
*
* @ingroup Maintenance
* @since 1.37
*/
class MigrateRevisionActorTemp extends LoggedUpdateMaintenance {
public function __construct() {
parent::__construct();
$this->addDescription(
'Copy the data from the revision_actor_temp into the revision table'
);
}
protected function getUpdateKey() {
return __CLASS__;
}
protected function doDBUpdates() {
$batchSize = $this->getBatchSize();
$dbw = $this->getDB( DB_PRIMARY );
if ( !$dbw->fieldExists( 'revision', 'rev_actor', __METHOD__ ) ) {
$this->output( "Run update.php to create rev_actor.\n" );
return false;
}
if ( !$dbw->tableExists( 'revision_actor_temp', __METHOD__ ) ) {
$this->output( "revision_actor_temp does not exist, so nothing to do.\n" );
return true;
}
$this->output( "Merging the revision_actor_temp table into the revision table...\n" );
$conds = [];
$updated = 0;
while ( true ) {
$res = $dbw->newSelectQueryBuilder()
->select( [ 'rev_id', 'rev_actor', 'revactor_actor' ] )
->from( 'revision' )
->join( 'revision_actor_temp', null, 'rev_id=revactor_rev' )
->where( $conds )
->limit( $batchSize )
->orderBy( 'rev_id' )
->caller( __METHOD__ )
->fetchResultSet();
$numRows = $res->numRows();
$last = null;
foreach ( $res as $row ) {
$last = $row->rev_id;
if ( !$row->rev_actor ) {
$dbw->update(
'revision',
[ 'rev_actor' => $row->revactor_actor ],
[ 'rev_id' => $row->rev_id ],
__METHOD__
);
$updated += $dbw->affectedRows();
} elseif ( $row->rev_actor !== $row->revactor_actor ) {
$this->error(
"Revision ID $row->rev_id has rev_actor = $row->rev_actor and "
. "revactor_actor = $row->revactor_actor. Ignoring the latter."
);
}
}
if ( $numRows < $batchSize ) {
// We must have reached the end
break;
}
$this->output( "... rev_id=$last, updated $updated\n" );
$conds = [ 'rev_id > ' . $dbw->addQuotes( $last ) ];
}
$this->output(
"Completed merge of revision_actor into the revision table, "
. "$updated rows updated.\n"
);
return true;
}
}
$maintClass = MigrateRevisionActorTemp::class;
require_once RUN_MAINTENANCE_IF_MAIN;

View file

@ -76,6 +76,7 @@ class ReassignEdits extends Maintenance {
* @return int Number of entries changed, or that would be changed
*/
private function doReassignEdits( &$from, &$to, $rc = false, $report = false ) {
$actorTableSchemaMigrationStage = $this->getConfig()->get( 'ActorTableSchemaMigrationStage' );
$dbw = $this->getDB( DB_PRIMARY );
$this->beginTransaction( $dbw, __METHOD__ );
$actorNormalization = MediaWikiServices::getInstance()->getActorNormalization();
@ -131,12 +132,22 @@ class ReassignEdits extends Maintenance {
if ( $total ) {
# Reassign edits
$this->output( "\nReassigning current edits..." );
$dbw->update(
'revision_actor_temp',
[ 'revactor_actor' => $toActorId ],
[ 'revactor_actor' => $fromActorId ],
__METHOD__
);
if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_TEMP ) {
$dbw->update(
'revision_actor_temp',
[ 'revactor_actor' => $toActorId ],
[ 'revactor_actor' => $fromActorId ],
__METHOD__
);
}
if ( $actorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
$dbw->update(
'revision',
[ 'rev_actor' => $toActorId ],
[ 'rev_actor' => $fromActorId ],
__METHOD__
);
}
$this->output( "done.\nReassigning deleted edits..." );
$dbw->update( 'archive',
[ 'ar_actor' => $toActorId ],

View file

@ -20,6 +20,16 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
'actor',
];
private const STAGES_BY_NAME = [
'old' => SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_OLD,
'read-old' => SCHEMA_COMPAT_WRITE_OLD_AND_TEMP | SCHEMA_COMPAT_READ_OLD,
'read-temp' => SCHEMA_COMPAT_WRITE_OLD_AND_TEMP | SCHEMA_COMPAT_READ_TEMP,
'temp' => SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_READ_TEMP,
'read-temp2' => SCHEMA_COMPAT_WRITE_TEMP_AND_NEW | SCHEMA_COMPAT_READ_TEMP,
'read-new' => SCHEMA_COMPAT_WRITE_TEMP_AND_NEW | SCHEMA_COMPAT_READ_NEW,
'new' => SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_NEW
];
protected function getSchemaOverrides( IMaintainableDatabase $db ) {
return [
'scripts' => [
@ -55,6 +65,21 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
);
}
private static function makeActorCases( $inputs, $expected ) {
foreach ( $expected as $inputName => $expectedCases ) {
foreach ( $expectedCases as list( $stages, $expected ) ) {
foreach ( $stages as $stage ) {
$cases[$inputName . ', ' . $stage] = array_merge(
[ $stage ],
$inputs[$inputName],
[ $expected ]
);
}
}
}
return $cases;
}
/**
* @dataProvider provideConstructor
* @param int $stage
@ -76,201 +101,161 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
return [
[ 0, '$stage must include a write mode' ],
[ SCHEMA_COMPAT_READ_OLD, '$stage must include a write mode' ],
[ SCHEMA_COMPAT_READ_NEW, '$stage must include a write mode' ],
[ SCHEMA_COMPAT_READ_TEMP, '$stage must include a write mode' ],
[ SCHEMA_COMPAT_READ_BOTH, '$stage must include a write mode' ],
[ SCHEMA_COMPAT_WRITE_OLD, '$stage must include a read mode' ],
[ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_OLD, null ],
[
SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_NEW,
'Cannot read the new schema without also writing it'
SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_TEMP,
'Cannot read the temp schema without also writing it'
],
[ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, 'Cannot read both schemas' ],
[ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, 'Cannot read multiple schemas' ],
[ SCHEMA_COMPAT_WRITE_NEW, '$stage must include a read mode' ],
[ SCHEMA_COMPAT_WRITE_TEMP, '$stage must include a read mode' ],
[
SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_OLD,
SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_READ_OLD,
'Cannot read the old schema without also writing it'
],
[ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_NEW, null ],
[ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH, 'Cannot read both schemas' ],
[ SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_READ_TEMP, null ],
[ SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_READ_BOTH, 'Cannot read multiple schemas' ],
[ SCHEMA_COMPAT_WRITE_BOTH, '$stage must include a read mode' ],
[ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, null ],
[ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, null ],
[ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH, 'Cannot read both schemas' ],
[ SCHEMA_COMPAT_WRITE_OLD_AND_TEMP, '$stage must include a read mode' ],
[ SCHEMA_COMPAT_WRITE_OLD_AND_TEMP | SCHEMA_COMPAT_READ_OLD, null ],
[ SCHEMA_COMPAT_WRITE_OLD_AND_TEMP | SCHEMA_COMPAT_READ_TEMP, null ],
[ SCHEMA_COMPAT_WRITE_OLD_AND_TEMP | SCHEMA_COMPAT_READ_BOTH, 'Cannot read multiple schemas' ],
];
}
/**
* @dataProvider provideGetJoin
* @param int $stage
* @param string $stageName
* @param string $key
* @param array $expect
*/
public function testGetJoin( $stage, $key, $expect ) {
public function testGetJoin( $stageName, $key, $expect ) {
$stage = self::STAGES_BY_NAME[$stageName];
$m = $this->getMigration( $stage );
$result = $m->getJoin( $key );
$this->assertEquals( $expect, $result );
}
public static function provideGetJoin() {
return [
'Simple table, old' => [
SCHEMA_COMPAT_OLD, 'am1_user', [
'tables' => [],
'fields' => [
'am1_user' => 'am1_user',
'am1_user_text' => 'am1_user_text',
'am1_actor' => 'NULL',
],
'joins' => [],
$inputs = [
'Simple table' => [ 'am1_user' ],
'Special name' => [ 'am3_xxx' ],
'Temp table' => [ 'am2_user' ],
];
$expected = [
'Simple table' => [
[
[ 'old', 'read-old' ],
[
'tables' => [],
'fields' => [
'am1_user' => 'am1_user',
'am1_user_text' => 'am1_user_text',
'am1_actor' => 'NULL',
],
'joins' => [],
]
],
],
'Simple table, read-old' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'am1_user', [
'tables' => [],
'fields' => [
'am1_user' => 'am1_user',
'am1_user_text' => 'am1_user_text',
'am1_actor' => 'NULL',
],
'joins' => [],
],
],
'Simple table, read-new' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'am1_user', [
'tables' => [ 'actor_am1_user' => 'actor' ],
'fields' => [
'am1_user' => 'actor_am1_user.actor_user',
'am1_user_text' => 'actor_am1_user.actor_name',
'am1_actor' => 'am1_actor',
],
'joins' => [
'actor_am1_user' => [ 'JOIN', 'actor_am1_user.actor_id = am1_actor' ],
],
],
],
'Simple table, new' => [
SCHEMA_COMPAT_NEW, 'am1_user', [
'tables' => [ 'actor_am1_user' => 'actor' ],
'fields' => [
'am1_user' => 'actor_am1_user.actor_user',
'am1_user_text' => 'actor_am1_user.actor_name',
'am1_actor' => 'am1_actor',
],
'joins' => [
'actor_am1_user' => [ 'JOIN', 'actor_am1_user.actor_id = am1_actor' ],
[
[ 'read-temp', 'temp', 'read-temp2', 'read-new', 'new' ],
[
'tables' => [ 'actor_am1_user' => 'actor' ],
'fields' => [
'am1_user' => 'actor_am1_user.actor_user',
'am1_user_text' => 'actor_am1_user.actor_name',
'am1_actor' => 'am1_actor',
],
'joins' => [
'actor_am1_user' => [ 'JOIN', 'actor_am1_user.actor_id = am1_actor' ],
],
],
],
],
'Special name, old' => [
SCHEMA_COMPAT_OLD, 'am3_xxx', [
'tables' => [],
'fields' => [
'am3_xxx' => 'am3_xxx',
'am3_xxx_text' => 'am3_xxx_text',
'am3_xxx_actor' => 'NULL',
],
'joins' => [],
],
],
'Special name, read-old' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'am3_xxx', [
'tables' => [],
'fields' => [
'am3_xxx' => 'am3_xxx',
'am3_xxx_text' => 'am3_xxx_text',
'am3_xxx_actor' => 'NULL',
],
'joins' => [],
],
],
'Special name, read-new' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'am3_xxx', [
'tables' => [ 'actor_am3_xxx' => 'actor' ],
'fields' => [
'am3_xxx' => 'actor_am3_xxx.actor_user',
'am3_xxx_text' => 'actor_am3_xxx.actor_name',
'am3_xxx_actor' => 'am3_xxx_actor',
],
'joins' => [
'actor_am3_xxx' => [ 'JOIN', 'actor_am3_xxx.actor_id = am3_xxx_actor' ],
'Special name' => [
[
[ 'old', 'read-old' ],
[
'tables' => [],
'fields' => [
'am3_xxx' => 'am3_xxx',
'am3_xxx_text' => 'am3_xxx_text',
'am3_xxx_actor' => 'NULL',
],
'joins' => [],
],
],
],
'Special name, new' => [
SCHEMA_COMPAT_NEW, 'am3_xxx', [
'tables' => [ 'actor_am3_xxx' => 'actor' ],
'fields' => [
'am3_xxx' => 'actor_am3_xxx.actor_user',
'am3_xxx_text' => 'actor_am3_xxx.actor_name',
'am3_xxx_actor' => 'am3_xxx_actor',
],
'joins' => [
'actor_am3_xxx' => [ 'JOIN', 'actor_am3_xxx.actor_id = am3_xxx_actor' ],
[
[ 'read-temp', 'temp', 'read-temp2', 'read-new', 'new' ],
[
'tables' => [ 'actor_am3_xxx' => 'actor' ],
'fields' => [
'am3_xxx' => 'actor_am3_xxx.actor_user',
'am3_xxx_text' => 'actor_am3_xxx.actor_name',
'am3_xxx_actor' => 'am3_xxx_actor',
],
'joins' => [
'actor_am3_xxx' => [ 'JOIN', 'actor_am3_xxx.actor_id = am3_xxx_actor' ],
],
],
],
],
'Temp table, old' => [
SCHEMA_COMPAT_OLD, 'am2_user', [
'tables' => [],
'fields' => [
'am2_user' => 'am2_user',
'am2_user_text' => 'am2_user_text',
'am2_actor' => 'NULL',
],
'joins' => [],
'Temp table' => [
[
[ 'old', 'read-old' ],
[
'tables' => [],
'fields' => [
'am2_user' => 'am2_user',
'am2_user_text' => 'am2_user_text',
'am2_actor' => 'NULL',
],
'joins' => [],
]
],
],
'Temp table, read-old' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'am2_user', [
'tables' => [],
'fields' => [
'am2_user' => 'am2_user',
'am2_user_text' => 'am2_user_text',
'am2_actor' => 'NULL',
],
'joins' => [],
[
[ 'read-temp', 'temp', 'read-temp2' ],
[
'tables' => [
'temp_am2_user' => 'actormigration2_temp',
'actor_am2_user' => 'actor',
],
'fields' => [
'am2_user' => 'actor_am2_user.actor_user',
'am2_user_text' => 'actor_am2_user.actor_name',
'am2_actor' => 'temp_am2_user.am2t_actor',
],
'joins' => [
'temp_am2_user' => [ 'JOIN', 'temp_am2_user.am2t_id = am2_id' ],
'actor_am2_user' => [ 'JOIN', 'actor_am2_user.actor_id = temp_am2_user.am2t_actor' ],
],
]
],
],
'Temp table, read-new' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'am2_user', [
'tables' => [
'temp_am2_user' => 'actormigration2_temp',
'actor_am2_user' => 'actor',
],
'fields' => [
'am2_user' => 'actor_am2_user.actor_user',
'am2_user_text' => 'actor_am2_user.actor_name',
'am2_actor' => 'temp_am2_user.am2t_actor',
],
'joins' => [
'temp_am2_user' => [ 'JOIN', 'temp_am2_user.am2t_id = am2_id' ],
'actor_am2_user' => [ 'JOIN', 'actor_am2_user.actor_id = temp_am2_user.am2t_actor' ],
],
],
],
'Temp table, new' => [
SCHEMA_COMPAT_NEW, 'am2_user', [
'tables' => [
'temp_am2_user' => 'actormigration2_temp',
'actor_am2_user' => 'actor',
],
'fields' => [
'am2_user' => 'actor_am2_user.actor_user',
'am2_user_text' => 'actor_am2_user.actor_name',
'am2_actor' => 'temp_am2_user.am2t_actor',
],
'joins' => [
'temp_am2_user' => [ 'JOIN', 'temp_am2_user.am2t_id = am2_id' ],
'actor_am2_user' => [ 'JOIN', 'actor_am2_user.actor_id = temp_am2_user.am2t_actor' ],
],
[
[ 'read-new', 'new' ],
[
'tables' => [
'actor_am2_user' => 'actor',
],
'fields' => [
'am2_user' => 'actor_am2_user.actor_user',
'am2_user_text' => 'actor_am2_user.actor_name',
'am2_actor' => 'am2_actor',
],
'joins' => [
'actor_am2_user' => [ 'JOIN', 'actor_am2_user.actor_id = am2_actor' ],
],
]
],
],
];
return self::makeActorCases( $inputs, $expected );
}
private const ACTORS = [
@ -322,13 +307,14 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
/**
* @dataProvider provideGetWhere
* @param int $stage
* @param string $stageName
* @param string $key
* @param UserIdentity|UserIdentity[]|null|false $users
* @param bool $useId
* @param array $expect
*/
public function testGetWhere( $stage, $key, $users, $useId, $expect ) {
public function testGetWhere( $stageName, $key, $users, $useId, $expect ) {
$stage = self::STAGES_BY_NAME[$stageName];
if ( !isset( $expect['conds'] ) ) {
$expect['conds'] = '(' . implode( ') OR (', $expect['orconds'] ) . ')';
}
@ -350,199 +336,186 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
new UserIdentityValue( 0, '2600:1004:b14a:5ddd:3ebe:bba4:bfba:f37e' ),
];
return [
'Simple table, old' => [
SCHEMA_COMPAT_OLD, 'am1_user', $genericUser, true, [
'tables' => [],
'orconds' => [ 'userid' => "am1_user = 1" ],
'joins' => [],
],
],
'Simple table, read-old' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'am1_user', $genericUser, true, [
'tables' => [],
'orconds' => [ 'userid' => "am1_user = 1" ],
'joins' => [],
],
],
'Simple table, read-new' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'am1_user', $genericUser, true, [
'tables' => [],
'orconds' => [ 'actor' => "am1_actor = 11" ],
'joins' => [],
],
],
'Simple table, new' => [
SCHEMA_COMPAT_NEW, 'am1_user', $genericUser, true, [
'tables' => [],
'orconds' => [ 'actor' => "am1_actor = 11" ],
'joins' => [],
],
],
'Special name, old' => [
SCHEMA_COMPAT_OLD, 'am3_xxx', $genericUser, true, [
'tables' => [],
'orconds' => [ 'userid' => "am3_xxx = 1" ],
'joins' => [],
],
],
'Special name, read-old' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'am3_xxx', $genericUser, true, [
'tables' => [],
'orconds' => [ 'userid' => "am3_xxx = 1" ],
'joins' => [],
],
],
'Special name, read-new' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'am3_xxx', $genericUser, true, [
'tables' => [],
'orconds' => [ 'actor' => "am3_xxx_actor = 11" ],
'joins' => [],
],
],
'Special name, new' => [
SCHEMA_COMPAT_NEW, 'am3_xxx', $genericUser, true, [
'tables' => [],
'orconds' => [ 'actor' => "am3_xxx_actor = 11" ],
'joins' => [],
],
],
'Temp table, old' => [
SCHEMA_COMPAT_OLD, 'am2_user', $genericUser, true, [
'tables' => [],
'orconds' => [ 'userid' => "am2_user = 1" ],
'joins' => [],
],
],
'Temp table, read-old' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'am2_user', $genericUser, true, [
'tables' => [],
'orconds' => [ 'userid' => "am2_user = 1" ],
'joins' => [],
],
],
'Temp table, read-new' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'am2_user', $genericUser, true, [
'tables' => [
'temp_am2_user' => 'actormigration2_temp',
],
'orconds' => [ 'actor' => "temp_am2_user.am2t_actor = 11" ],
'joins' => [
'temp_am2_user' => [ 'JOIN', 'temp_am2_user.am2t_id = am2_id' ],
],
],
],
'Temp table, new' => [
SCHEMA_COMPAT_NEW, 'am2_user', $genericUser, true, [
'tables' => [
'temp_am2_user' => 'actormigration2_temp',
],
'orconds' => [ 'actor' => "temp_am2_user.am2t_actor = 11" ],
'joins' => [
'temp_am2_user' => [ 'JOIN', 'temp_am2_user.am2t_id = am2_id' ],
],
],
],
'Multiple users, old' => [
SCHEMA_COMPAT_OLD, 'am1_user', $complicatedUsers, true, [
'tables' => [],
'orconds' => [
'userid' => "am1_user IN (1,2,3) ",
'username' => "am1_user_text IN ('192.168.12.34','192.168.12.35',"
. "'2600:1004:B14A:5DDD:3EBE:BBA4:BFBA:F37E') "
],
'joins' => [],
],
],
'Multiple users, read-old' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'am1_user', $complicatedUsers, true, [
'tables' => [],
'orconds' => [
'userid' => "am1_user IN (1,2,3) ",
'username' => "am1_user_text IN ('192.168.12.34','192.168.12.35',"
. "'2600:1004:B14A:5DDD:3EBE:BBA4:BFBA:F37E') "
],
'joins' => [],
],
],
'Multiple users, read-new' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'am1_user', $complicatedUsers, true, [
'tables' => [],
'orconds' => [ 'actor' => "am1_actor IN (11,12,34) " ],
'joins' => [],
],
],
'Multiple users, new' => [
SCHEMA_COMPAT_NEW, 'am1_user', $complicatedUsers, true, [
'tables' => [],
'orconds' => [ 'actor' => "am1_actor IN (11,12,34) " ],
'joins' => [],
],
],
'Multiple users, no use ID, old' => [
SCHEMA_COMPAT_OLD, 'am1_user', $complicatedUsers, false, [
'tables' => [],
'orconds' => [
'username' => "am1_user_text IN ('User1','User2','User3','192.168.12.34',"
. "'192.168.12.35','2600:1004:B14A:5DDD:3EBE:BBA4:BFBA:F37E') "
],
'joins' => [],
],
],
'Multiple users, no use ID, read-old' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'am1_user', $complicatedUsers, false, [
'tables' => [],
'orconds' => [
'username' => "am1_user_text IN ('User1','User2','User3','192.168.12.34',"
. "'192.168.12.35','2600:1004:B14A:5DDD:3EBE:BBA4:BFBA:F37E') "
],
'joins' => [],
],
],
'Multiple users, no use ID, read-new' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'am1_user', $complicatedUsers, false, [
'tables' => [],
'orconds' => [ 'actor' => "am1_actor IN (11,12,34) " ],
'joins' => [],
],
],
'Multiple users, no use ID, new' => [
SCHEMA_COMPAT_NEW, 'am1_user', $complicatedUsers, false, [
'tables' => [],
'orconds' => [ 'actor' => "am1_actor IN (11,12,34) " ],
'joins' => [],
],
],
'Empty $users' => [
SCHEMA_COMPAT_NEW, 'am1_user', [], true, [
'tables' => [],
'conds' => '1=0',
'orconds' => [],
'joins' => [],
],
],
'Null $users' => [
SCHEMA_COMPAT_NEW, 'am1_user', null, true, [
'tables' => [],
'conds' => '1=0',
'orconds' => [],
'joins' => [],
],
],
'False $users' => [
SCHEMA_COMPAT_NEW, 'am1_user', false, true, [
'tables' => [],
'conds' => '1=0',
'orconds' => [],
'joins' => [],
],
],
$inputs = [
'Simple table' => [ 'am1_user', $genericUser, true ],
'Special name' => [ 'am3_xxx', $genericUser, true ],
'Temp table' => [ 'am2_user', $genericUser, true ],
'Multiple users' => [ 'am1_user', $complicatedUsers, true ],
'Multiple users, no use ID' => [ 'am1_user', $complicatedUsers, false ],
'Empty $users' => [ 'am1_user', [], true ],
'Null $users' => [ 'am1_user', null, true ],
'False $users' => [ 'am1_user', false, true ],
];
$expected = [
'Simple table' => [
[
[ 'old', 'read-old' ],
[
'tables' => [],
'orconds' => [ 'userid' => "am1_user = 1" ],
'joins' => [],
]
], [
[ 'read-temp', 'temp', 'read-temp2' ],
[
'tables' => [],
'orconds' => [ 'actor' => "am1_actor = 11" ],
'joins' => [],
],
], [
[ 'read-new', 'new' ],
[
'tables' => [],
'orconds' => [ 'newactor' => "am1_actor = 11" ],
'joins' => [],
],
],
],
'Special name' => [
[
[ 'old', 'read-old' ],
[
'tables' => [],
'orconds' => [ 'userid' => "am3_xxx = 1" ],
'joins' => [],
],
], [
[ 'read-temp', 'temp', 'read-temp2' ],
[
'tables' => [],
'orconds' => [ 'actor' => "am3_xxx_actor = 11" ],
'joins' => [],
],
], [
[ 'read-new', 'new' ],
[
'tables' => [],
'orconds' => [ 'newactor' => "am3_xxx_actor = 11" ],
'joins' => [],
],
],
],
'Temp table' => [
[
[ 'old', 'read-old' ],
[
'tables' => [],
'orconds' => [ 'userid' => "am2_user = 1" ],
'joins' => [],
],
], [
[ 'read-temp', 'temp', 'read-temp2' ],
[
'tables' => [
'temp_am2_user' => 'actormigration2_temp',
],
'orconds' => [ 'actor' => "temp_am2_user.am2t_actor = 11" ],
'joins' => [
'temp_am2_user' => [ 'JOIN', 'temp_am2_user.am2t_id = am2_id' ],
],
],
], [
[ 'read-new', 'new' ],
[
'tables' => [],
'orconds' => [ 'newactor' => "am2_actor = 11" ],
'joins' => [],
]
]
],
'Multiple users' => [
[
[ 'old', 'read-old' ],
[
'tables' => [],
'orconds' => [
'userid' => "am1_user IN (1,2,3) ",
'username' => "am1_user_text IN ('192.168.12.34','192.168.12.35',"
. "'2600:1004:B14A:5DDD:3EBE:BBA4:BFBA:F37E') "
],
'joins' => [],
]
], [
[ 'read-temp', 'temp', 'read-temp2' ],
[
'tables' => [],
'orconds' => [ 'actor' => "am1_actor IN (11,12,34) " ],
'joins' => [],
]
], [
[ 'read-new', 'new' ],
[
'tables' => [],
'orconds' => [ 'newactor' => "am1_actor IN (11,12,34) " ],
'joins' => [],
]
]
],
'Multiple users, no use ID' => [
[
[ 'old', 'read-old' ],
[
'tables' => [],
'orconds' => [
'username' => "am1_user_text IN ('User1','User2','User3','192.168.12.34',"
. "'192.168.12.35','2600:1004:B14A:5DDD:3EBE:BBA4:BFBA:F37E') "
],
'joins' => [],
],
], [
[ 'read-temp', 'temp', 'read-temp2' ],
[
'tables' => [],
'orconds' => [ 'actor' => "am1_actor IN (11,12,34) " ],
'joins' => [],
]
], [
[ 'read-new', 'new' ],
[
'tables' => [],
'orconds' => [ 'newactor' => "am1_actor IN (11,12,34) " ],
'joins' => [],
]
]
],
'Empty $users' => [ [
[ 'old', 'read-old', 'read-temp', 'temp', 'read-temp2', 'read-new', 'new' ],
[
'tables' => [],
'conds' => '1=0',
'orconds' => [],
'joins' => [],
],
] ],
'Null $users' => [ [
[ 'old', 'read-old', 'read-temp', 'temp', 'read-temp2', 'read-new', 'new' ],
[
'tables' => [],
'conds' => '1=0',
'orconds' => [],
'joins' => [],
],
] ],
'False $users' => [ [
[ 'old', 'read-old', 'read-temp', 'temp', 'read-temp2', 'read-new', 'new' ],
[
'tables' => [],
'conds' => '1=0',
'orconds' => [],
'joins' => [],
],
] ],
];
return self::makeActorCases( $inputs, $expected );
}
/**
@ -569,40 +542,54 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
$u = $this->getTestUser()->getUser();
$user = new UserIdentityValue( $u->getId(), $u->getName() );
$stageNames = [
SCHEMA_COMPAT_OLD => 'old',
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD => 'write-both-read-old',
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW => 'write-both-read-new',
SCHEMA_COMPAT_NEW => 'new',
];
$stageNames = array_flip( self::STAGES_BY_NAME );
$stages = [
SCHEMA_COMPAT_OLD => [
SCHEMA_COMPAT_OLD,
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
'old' => [
'old',
'read-old',
],
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD => [
SCHEMA_COMPAT_OLD,
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
SCHEMA_COMPAT_NEW
'read-old' => [
'old',
'read-old',
'read-temp',
'temp'
],
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW => [
SCHEMA_COMPAT_OLD,
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
SCHEMA_COMPAT_NEW
'read-temp' => [
'old',
'read-old',
'read-temp',
'temp'
],
SCHEMA_COMPAT_NEW => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
SCHEMA_COMPAT_NEW
'temp' => [
'read-temp',
'temp'
],
'read-temp2' => [
'read-temp',
'temp',
'read-temp2',
'read-new',
'new'
],
'read-new' => [
'read-temp',
'temp',
'read-temp2',
'read-new',
'new'
],
'new' => [
'read-new',
'new'
],
];
$nameKey = $key . '_text';
$actorKey = ( $key === 'am3_xxx' ? $key : substr( $key, 0, -5 ) ) . '_actor';
foreach ( $stages as $writeStage => $possibleReadStages ) {
foreach ( $stages as $writeStageName => $possibleReadStages ) {
$writeStage = self::STAGES_BY_NAME[$writeStageName];
$w = $this->getMigration( $writeStage );
if ( $usesTemp ) {
@ -620,7 +607,9 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
$this->assertArrayNotHasKey( $key, $fields, "old field, stage={$stageNames[$writeStage]}" );
$this->assertArrayNotHasKey( $nameKey, $fields, "old field, stage={$stageNames[$writeStage]}" );
}
if ( ( $writeStage & SCHEMA_COMPAT_WRITE_NEW ) && !$usesTemp ) {
if ( ( ( $writeStage & SCHEMA_COMPAT_WRITE_TEMP ) && !$usesTemp )
|| ( $writeStage & SCHEMA_COMPAT_WRITE_NEW )
) {
$this->assertArrayHasKey( $actorKey, $fields,
"new field, stage={$stageNames[$writeStage]}" );
} else {
@ -634,7 +623,8 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
$callback( $id, [] );
}
foreach ( $possibleReadStages as $readStage ) {
foreach ( $possibleReadStages as $readStageName ) {
$readStage = self::STAGES_BY_NAME[$readStageName];
$r = $this->getMigration( $readStage );
$queryInfo = $r->getJoin( $key );
@ -664,12 +654,11 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
}
public static function provideStages() {
return [
'old' => [ SCHEMA_COMPAT_OLD ],
'read-old' => [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD ],
'read-new' => [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW ],
'new' => [ SCHEMA_COMPAT_NEW ],
];
$cases = [];
foreach ( self::STAGES_BY_NAME as $name => $stage ) {
$cases[$name] = [ $stage ];
}
return $cases;
}
/**
@ -768,7 +757,8 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
$this->assertSame( $user->getId(), (int)$row->am2_user );
$this->assertSame( $user->getName(), $row->am2_user_text );
$this->assertSame(
( $stage & SCHEMA_COMPAT_READ_NEW ) ? $user->getActorId() : 0,
( ( $stage & SCHEMA_COMPAT_READ_MASK ) >= SCHEMA_COMPAT_READ_TEMP )
? $user->getActorId() : 0,
(int)$row->am2_actor
);
@ -781,7 +771,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
$this->assertArrayNotHasKey( 'dummy_user', $fields );
$this->assertArrayNotHasKey( 'dummy_user_text', $fields );
}
if ( $stage & SCHEMA_COMPAT_WRITE_NEW ) {
if ( $stage & SCHEMA_COMPAT_WRITE_TEMP || $stage & SCHEMA_COMPAT_WRITE_NEW ) {
$this->assertSame( $user->getActorId(), $fields['dummy_actor'] );
} else {
$this->assertArrayNotHasKey( 'dummy_actor', $fields );
@ -796,24 +786,26 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
/**
* @dataProvider provideIsAnon
* @param int $stage
* @param string $stage
* @param string $isAnon
* @param string $isNotAnon
*/
public function testIsAnon( $stage, $isAnon, $isNotAnon ) {
$m = $this->getMigration( $stage );
$numericStage = self::STAGES_BY_NAME[$stage];
$m = $this->getMigration( $numericStage );
$this->assertSame( $isAnon, $m->isAnon( 'foo' ) );
$this->assertSame( $isNotAnon, $m->isNotAnon( 'foo' ) );
}
public static function provideIsAnon() {
return [
'old' => [ SCHEMA_COMPAT_OLD, 'foo = 0', 'foo != 0' ],
'read-old' => [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, 'foo = 0', 'foo != 0' ],
'read-new' => [
SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, 'foo IS NULL', 'foo IS NOT NULL'
],
'new' => [ SCHEMA_COMPAT_NEW, 'foo IS NULL', 'foo IS NOT NULL' ],
'old' => [ 'old', 'foo = 0', 'foo != 0' ],
'read-old' => [ 'read-old', 'foo = 0', 'foo != 0' ],
'read-temp' => [ 'read-temp', 'foo IS NULL', 'foo IS NOT NULL' ],
'temp' => [ 'temp', 'foo IS NULL', 'foo IS NOT NULL' ],
'read-temp2' => [ 'temp', 'foo IS NULL', 'foo IS NOT NULL' ],
'read-new' => [ 'temp', 'foo IS NULL', 'foo IS NOT NULL' ],
'new' => [ 'temp', 'foo IS NULL', 'foo IS NOT NULL' ],
];
}
@ -830,7 +822,7 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
'removedVersion' => '1.34',
],
],
SCHEMA_COMPAT_NEW,
SCHEMA_COMPAT_TEMP,
MediaWikiServices::getInstance()->getActorStoreFactory()
) extends ActorMigrationBase {
public function checkDeprecationForTest( $key ) {

View file

@ -10,7 +10,8 @@ CREATE TABLE /*_*/actormigration1 (
CREATE TABLE /*_*/actormigration2 (
am2_id integer not null,
am2_user integer,
am2_user_text varchar(200)
am2_user_text varchar(200),
am2_actor integer
);
CREATE TABLE /*_*/actormigration2_temp (

View file

@ -51,7 +51,7 @@ class RevisionQueryInfoTest extends MediaWikiIntegrationTestCase {
return $fields;
}
protected function getNewCommentQueryFields( $prefix ) {
protected function getCommentQueryFields( $prefix ) {
return [
"{$prefix}_comment_text" => "comment_{$prefix}_comment.comment_text",
"{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
@ -59,19 +59,25 @@ class RevisionQueryInfoTest extends MediaWikiIntegrationTestCase {
];
}
protected function getNewActorQueryFields( $prefix, $tmp = false ) {
protected function getActorQueryFields( $prefix, $tmp = false ) {
if ( $tmp ) {
return [
"{$prefix}_user" => "actor_{$prefix}_user.actor_user",
"{$prefix}_user_text" => "actor_{$prefix}_user.actor_name",
"{$prefix}_actor" => "temp_{$prefix}_user.{$prefix}actor_actor",
];
} else {
} elseif ( $prefix === 'ar' ) {
return [
"{$prefix}_actor",
"{$prefix}_user" => 'archive_actor.actor_user',
"{$prefix}_user_text" => 'archive_actor.actor_name',
];
} else {
return [
"{$prefix}_actor" => "{$prefix}_actor",
"{$prefix}_user" => "actor_{$prefix}_user.actor_user",
"{$prefix}_user_text" => "actor_{$prefix}_user.actor_name",
];
}
}
@ -117,8 +123,8 @@ class RevisionQueryInfoTest extends MediaWikiIntegrationTestCase {
],
'fields' => array_merge(
$this->getArchiveQueryFields( false ),
$this->getNewActorQueryFields( 'ar' ),
$this->getNewCommentQueryFields( 'ar' )
$this->getActorQueryFields( 'ar' ),
$this->getCommentQueryFields( 'ar' )
),
'joins' => [
'comment_ar_comment'
@ -131,8 +137,10 @@ class RevisionQueryInfoTest extends MediaWikiIntegrationTestCase {
public function provideQueryInfo() {
// TODO: more option variations
yield 'page and user option' => [
[],
yield 'page and user option, actor-temp' => [
[
'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_TEMP,
],
[ 'page', 'user' ],
[
'tables' => [
@ -148,8 +156,8 @@ class RevisionQueryInfoTest extends MediaWikiIntegrationTestCase {
$this->getRevisionQueryFields( false ),
$this->getPageQueryFields(),
$this->getUserQueryFields(),
$this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
$this->getNewCommentQueryFields( 'rev' )
$this->getActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
$this->getCommentQueryFields( 'rev' )
),
'joins' => [
'page' => [ 'JOIN', [ 'page_id = rev_page' ] ],
@ -167,8 +175,46 @@ class RevisionQueryInfoTest extends MediaWikiIntegrationTestCase {
],
]
];
yield 'no options' => [
[],
yield 'page and user option, actor-new' => [
[
'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
],
[ 'page', 'user' ],
[
'tables' => [
'revision',
'page',
'user',
'temp_rev_comment' => 'revision_comment_temp',
'actor_rev_user' => 'actor',
'comment_rev_comment' => 'comment',
],
'fields' => array_merge(
$this->getRevisionQueryFields( false ),
$this->getPageQueryFields(),
$this->getUserQueryFields(),
$this->getActorQueryFields( 'rev' ),
$this->getCommentQueryFields( 'rev' )
),
'joins' => [
'page' => [ 'JOIN', [ 'page_id = rev_page' ] ],
'user' => [
'LEFT JOIN',
[ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ],
],
'comment_rev_comment' => [
'JOIN',
'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id',
],
'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
'actor_rev_user' => [ 'JOIN', 'actor_rev_user.actor_id = rev_actor' ]
],
]
];
yield 'no options, actor-temp' => [
[
'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_TEMP,
],
[],
[
'tables' => [
@ -180,8 +226,8 @@ class RevisionQueryInfoTest extends MediaWikiIntegrationTestCase {
],
'fields' => array_merge(
$this->getRevisionQueryFields( false ),
$this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
$this->getNewCommentQueryFields( 'rev' )
$this->getActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
$this->getCommentQueryFields( 'rev' )
),
'joins' => [
'comment_rev_comment' => [
@ -194,6 +240,33 @@ class RevisionQueryInfoTest extends MediaWikiIntegrationTestCase {
]
]
];
yield 'no options, actor-new' => [
[
'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
],
[],
[
'tables' => [
'revision',
'temp_rev_comment' => 'revision_comment_temp',
'actor_rev_user' => 'actor',
'comment_rev_comment' => 'comment',
],
'fields' => array_merge(
$this->getRevisionQueryFields( false ),
$this->getActorQueryFields( 'rev' ),
$this->getCommentQueryFields( 'rev' )
),
'joins' => [
'comment_rev_comment' => [
'JOIN',
'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id',
],
'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
'actor_rev_user' => [ 'JOIN', 'actor_rev_user.actor_id = rev_actor' ],
]
]
];
}
public function provideSlotsQueryInfo() {