use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\DBConnRef;
use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\ILoadBalancer;
/**
* Service for looking up page revisions.
private $contentHandlerUseDB = true;
/**
- * @var LoadBalancer
+ * @var ILoadBalancer
*/
private $loadBalancer;
*/
private $slotRoleStore;
- /** @var int One of the MIGRATION_* constants */
+ /** @var int An appropriate combination of SCHEMA_COMPAT_XXX flags. */
private $mcrMigrationStage;
/**
* @todo $blobStore should be allowed to be any BlobStore!
*
- * @param LoadBalancer $loadBalancer
+ * @param ILoadBalancer $loadBalancer
* @param SqlBlobStore $blobStore
* @param WANObjectCache $cache
* @param CommentStore $commentStore
* @param NameTableStore $contentModelStore
* @param NameTableStore $slotRoleStore
- * @param int $migrationStage
+ * @param int $mcrMigrationStage An appropriate combination of SCHEMA_COMPAT_XXX flags
* @param ActorMigration $actorMigration
* @param bool|string $wikiId
+ *
+ * @throws MWException if $mcrMigrationStage or $wikiId is invalid.
*/
public function __construct(
- LoadBalancer $loadBalancer,
+ ILoadBalancer $loadBalancer,
SqlBlobStore $blobStore,
WANObjectCache $cache,
CommentStore $commentStore,
NameTableStore $contentModelStore,
NameTableStore $slotRoleStore,
- $migrationStage,
+ $mcrMigrationStage,
ActorMigration $actorMigration,
$wikiId = false
) {
Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
- Assert::parameterType( 'integer', $migrationStage, '$migrationStage' );
+ Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== SCHEMA_COMPAT_READ_BOTH,
+ '$mcrMigrationStage',
+ 'Reading from the old and the new schema at the same time is not supported.'
+ );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== 0,
+ '$mcrMigrationStage',
+ 'Reading needs to be enabled for the old or the new schema.'
+ );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_BOTH ) !== 0,
+ '$mcrMigrationStage',
+ 'Writing needs to be enabled for the old or the new schema.'
+ );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_READ_OLD ) === 0
+ || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) !== 0,
+ '$mcrMigrationStage',
+ 'Cannot read the old schema when not also writing it.'
+ );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_READ_NEW ) === 0
+ || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) !== 0,
+ '$mcrMigrationStage',
+ 'Cannot read the new schema when not also writing it.'
+ );
$this->loadBalancer = $loadBalancer;
$this->blobStore = $blobStore;
$this->commentStore = $commentStore;
$this->contentModelStore = $contentModelStore;
$this->slotRoleStore = $slotRoleStore;
- $this->mcrMigrationStage = $migrationStage;
+ $this->mcrMigrationStage = $mcrMigrationStage;
$this->actorMigration = $actorMigration;
$this->wikiId = $wikiId;
$this->logger = new NullLogger();
}
+ /**
+ * @param int $flags A combination of SCHEMA_COMPAT_XXX flags.
+ * @return bool True if all the given flags were set in the $mcrMigrationStage
+ * parameter passed to the constructor.
+ */
+ private function hasMcrSchemaFlags( $flags ) {
+ return ( $this->mcrMigrationStage & $flags ) === $flags;
+ }
+
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
* @throws MWException
*/
public function setContentHandlerUseDB( $contentHandlerUseDB ) {
- if ( !$contentHandlerUseDB && $this->mcrMigrationStage > MIGRATION_OLD ) {
- throw new MWException(
- 'Content model must be stored in the database for multi content revision migration.'
- );
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
+ || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
+ ) {
+ if ( !$contentHandlerUseDB ) {
+ throw new MWException(
+ 'Content model must be stored in the database for multi content revision migration.'
+ );
+ }
}
$this->contentHandlerUseDB = $contentHandlerUseDB;
}
/**
- * @return LoadBalancer
+ * @return ILoadBalancer
*/
private function getDBLoadBalancer() {
return $this->loadBalancer;
return $lb->getConnection( $mode, [], $this->wikiId );
}
+ /**
+ * @param int $queryFlags a bit field composed of READ_XXX flags
+ *
+ * @return DBConnRef
+ */
+ private function getDBConnectionRefForQueryFlags( $queryFlags ) {
+ list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+ return $this->getDBConnectionRef( $mode );
+ }
+
/**
* @param IDatabase $connection
*/
);
}
- // While inserting into the old schema make sure only the main slot is allowed.
- // TODO: support extra slots in MIGRATION_WRITE_BOTH mode!
- if ( $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH && $slotRoles !== [ 'main' ] ) {
+ // If we are not writing into the new schema, we can't support extra slots.
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) && $slotRoles !== [ 'main' ] ) {
+ throw new InvalidArgumentException(
+ 'Only the main slot is supported when not writing to the MCR enabled schema!'
+ );
+ }
+
+ // As long as we are not reading from the new schema, we don't want to write extra slots.
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) && $slotRoles !== [ 'main' ] ) {
throw new InvalidArgumentException(
- 'Only the main slot is supported with MCR migration mode <= MIGRATION_WRITE_BOTH!'
+ 'Only the main slot is supported when not reading from the MCR enabled schema!'
);
}
);
// Trigger exception if the main slot is missing.
- // Technically, this could go away with MIGRATION_NEW: while
+ // Technically, this could go away after MCR migration: while
// calling code may require a main slot to exist, RevisionStore
// really should not know or care about that requirement.
$rev->getSlot( 'main', RevisionRecord::RAW );
$newSlots[$role] = $slot;
// Write the main slot's text ID to the revision table for backwards compatibility
- if ( $slot->getRole() === 'main' && $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
+ if ( $slot->getRole() === 'main'
+ && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
+ ) {
$blobAddress = $slot->getAddress();
$this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
}
}
// Write the main slot's text ID to the revision table for backwards compatibility
- if ( $protoSlot->getRole() === 'main' && $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
+ if ( $protoSlot->getRole() === 'main'
+ && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
+ ) {
$this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
}
- if ( $this->mcrMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
if ( $protoSlot->hasContentId() ) {
$contentId = $protoSlot->getContentId();
} else {
$revisionRow['rev_id'] = $rev->getId();
}
- if ( $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
- // In non MCR more this IF section will relate to the main slot
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
+ // In non MCR mode this IF section will relate to the main slot
$mainSlot = $rev->getSlot( 'main' );
$model = $mainSlot->getModel();
$format = $mainSlot->getFormat();
* Such revisions can for instance identify page rename
* operations and other such meta-modifications.
*
- * @note: This method grabs a FOR UPDATE lock on the relevant row of the page table,
+ * @note This method grabs a FOR UPDATE lock on the relevant row of the page table,
* to prevent a new revision from being inserted before the null revision has been written
* to the database.
*
return null;
}
- // Fetch the actual revision row, without locking all extra tables.
- $oldRevision = $this->loadRevisionFromId( $dbw, $pageLatest );
+ // Fetch the actual revision row from master, without locking all extra tables.
+ $oldRevision = $this->loadRevisionFromConds(
+ $dbw,
+ [ 'rev_id' => intval( $pageLatest ) ],
+ self::READ_LATEST,
+ $title
+ );
// Construct the new revision
$timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
* @return null|RecentChange
*/
public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
- $dbr = $this->getDBConnection( DB_REPLICA );
-
list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
+ $db = $this->getDBConnection( $dbType );
$userIdentity = $rev->getUser( RevisionRecord::RAW );
}
// 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 );
+ $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
$rc = RecentChange::newFromConds(
[
$actorWhere['conds'],
- 'rc_timestamp' => $dbr->timestamp( $rev->getTimestamp() ),
+ 'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
'rc_this_oldid' => $rev->getId()
],
__METHOD__,
$dbType
);
- $this->releaseDBConnection( $dbr );
+ $this->releaseDBConnection( $db );
// XXX: cache this locally? Glue it to the RevisionRecord?
return $rc;
$blobFlags = null;
if ( is_object( $row ) ) {
- if ( $this->mcrMigrationStage >= MIGRATION_NEW ) {
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
// Don't emulate from a row when using the new schema.
// Emulating from an array is still OK.
throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
if ( !property_exists( $row, 'old_flags' ) ) {
throw new InvalidArgumentException( 'old_flags was not set in $row' );
}
- $blobFlags = ( $row->old_flags === null ) ? '' : $row->old_flags;
+ $blobFlags = $row->old_flags ?? '';
}
$mainSlotRow->slot_revision_id = intval( $row->rev_id );
// $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
$mainSlotRow->slot_content_id =
function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
- list( $dbMode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
- $db = $this->getDBConnectionRef( $dbMode );
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, 'main' );
};
// and fall back to master. The assumption is that we only want to force the fallback
// if we are quite sure the revision exists because the caller supplied a revision ID.
// If the page isn't found at all on a replica, it probably simply does not exist.
- $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+ $db = $this->getDBConnectionRefForQueryFlags( $flags );
$conds[] = 'rev_id=page_latest';
$rev = $this->loadRevisionFromConds( $db, $conds, $flags );
- $this->releaseDBConnection( $db );
return $rev;
}
}
// and fall back to master. The assumption is that we only want to force the fallback
// if we are quite sure the revision exists because the caller supplied a revision ID.
// If the page isn't found at all on a replica, it probably simply does not exist.
- $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+ $db = $this->getDBConnectionRefForQueryFlags( $flags );
$conds[] = 'rev_id=page_latest';
$rev = $this->loadRevisionFromConds( $db, $conds, $flags );
- $this->releaseDBConnection( $db );
return $rev;
}
}
$queryFlags,
Title $title
) {
- if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
- // TODO: in MIGRATION_WRITE_BOTH, we could use the old and the new method:
- // e.g. call emulateMainSlot_1_29() if loadSlotRecords() fails.
-
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
$mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
$slots = new RevisionSlots( [ 'main' => $mainSlot ] );
} else {
$user = new UserIdentityValue( 0, '', 0 );
}
- $comment = $this->commentStore
- // Legacy because $row may have come from self::selectFields()
- ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'ar_comment', $row, true );
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+ // Legacy because $row may have come from self::selectFields()
+ $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
$slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
$user = new UserIdentityValue( 0, '', 0 );
}
- $comment = $this->commentStore
- // Legacy because $row may have come from self::selectFields()
- ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'rev_comment', $row, true );
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+ // Legacy because $row may have come from self::selectFields()
+ $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
$slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
}
if ( !empty( $fields['text_id'] ) ) {
- if ( $this->mcrMigrationStage >= MIGRATION_NEW ) {
- throw new MWException( "Cannot use text_id field with MCR schema" );
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+ throw new MWException( "The text_id field is only available in the pre-MCR schema" );
}
if ( !empty( $fields['content'] ) ) {
*
* @param array $conditions
* @param int $flags (optional)
- * @param Title $title
+ * @param Title|null $title
*
* @return RevisionRecord|null
*/
private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
- $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+ $db = $this->getDBConnectionRefForQueryFlags( $flags );
$rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
- $this->releaseDBConnection( $db );
$lb = $this->getDBLoadBalancer();
&& $lb->hasOrMadeRecentMasterChanges()
) {
$flags = self::READ_LATEST;
- $db = $this->getDBConnection( DB_MASTER );
- $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
- $this->releaseDBConnection( $db );
+ $dbw = $this->getDBConnection( DB_MASTER );
+ $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
+ $this->releaseDBConnection( $dbw );
}
return $rev;
* @param IDatabase $db
* @param array $conditions
* @param int $flags (optional)
- * @param Title $title
+ * @param Title|null $title
*
* @return RevisionRecord|null
*/
/**
* Finds the ID of a content row for a given revision and slot role.
* This can be used to re-use content rows even while the content ID
- * is still missing from SlotRecords, in MIGRATION_WRITE_BOTH mode.
+ * is still missing from SlotRecords, when writing to both the old and
+ * the new schema during MCR schema migration.
*
* @todo remove after MCR schema migration is complete.
*
* @return int|null
*/
private function findSlotContentId( IDatabase $db, $revId, $role ) {
- if ( $this->mcrMigrationStage < MIGRATION_WRITE_BOTH ) {
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
return null;
}
* - 'page': Join with the page table, and select fields to identify the page
* - 'user': Join with the user table, and select the user name
* - 'text': Join with the text table, and select fields to load page text. This
- * option is deprecated in MW 1.32 with MCR migration stage MIGRATION_WRITE_BOTH,
- * and disallowed with MIGRATION_MEW.
+ * option is deprecated in MW 1.32 when the MCR migration flag SCHEMA_COMPAT_WRITE_NEW
+ * is set, and disallowed when SCHEMA_COMPAT_READ_OLD is not set.
*
* @return array With three keys:
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
$ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
$ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
- if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
$ret['fields'][] = 'rev_text_id';
if ( $this->contentHandlerUseDB ) {
}
if ( in_array( 'text', $options, true ) ) {
- if ( $this->mcrMigrationStage === MIGRATION_NEW ) {
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
throw new InvalidArgumentException( 'text table can no longer be joined directly' );
- } elseif ( $this->mcrMigrationStage >= MIGRATION_WRITE_BOTH ) {
+ } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+ // NOTE: even when this class is set to not read from the old schema, callers
+ // should still be able to join against the text table, as long as we are still
+ // writing the old schema for compatibility.
wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
}
'joins' => [],
];
- if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
$db = $this->getDBConnectionRef( DB_REPLICA );
$ret['tables']['slots'] = 'revision';
$ret['fields']['model_name'] = 'NULL';
}
}
-
- // XXX: in MIGRATION_WRITE_BOTH mode, emulate *and* select - using a UNION?
- // See Anomie's idea at <https://gerrit.wikimedia.org/r/c/416465/
- // 8..10/includes/Storage/RevisionStore.php#2113>
} else {
$ret['tables'][] = 'slots';
$ret['tables'][] = 'slot_roles';
'joins' => $commentQuery['joins'] + $actorQuery['joins'],
];
- if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
$ret['fields'][] = 'ar_text_id';
if ( $this->contentHandlerUseDB ) {
* MCR migration note: this replaces Revision::getPrevious
*
* @param RevisionRecord $rev
- * @param Title $title if known (optional)
+ * @param Title|null $title if known (optional)
*
* @return RevisionRecord|null
*/
* MCR migration note: this replaces Revision::getNext
*
* @param RevisionRecord $rev
- * @param Title $title if known (optional)
+ * @param Title|null $title if known (optional)
*
* @return RevisionRecord|null
*/
* @return string|bool False if not found
*/
public function getTimestampFromId( $title, $id, $flags = 0 ) {
- $db = $this->getDBConnection(
- ( $flags & IDBAccessObject::READ_LATEST ) ? DB_MASTER : DB_REPLICA
- );
+ $db = $this->getDBConnectionRefForQueryFlags( $flags );
$conds = [ 'rev_id' => $id ];
$conds['rev_page'] = $title->getArticleID();
$timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
- $this->releaseDBConnection( $db );
return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
}