Merge "Title: use PageStore instead of LinkCache"

This commit is contained in:
jenkins-bot 2021-11-03 11:29:46 +00:00 committed by Gerrit Code Review
commit a48b932e8e
6 changed files with 122 additions and 100 deletions

View file

@ -508,7 +508,7 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
/** /**
* Returns a list of fields that are to be selected for initializing Title * Returns a list of fields that are to be selected for initializing Title
* objects or LinkCache entries. * objects.
* *
* @deprecated since 1.36, use PageStore::newSelectQueryBuilder() instead. * @deprecated since 1.36, use PageStore::newSelectQueryBuilder() instead.
* *
@ -1069,6 +1069,16 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
return $this->mNamespace; return $this->mNamespace;
} }
/**
* @param int $flags
*
* @return bool Whether $flags indicates that the latest information should be
* read from the primary database, bypassing caches.
*/
private function shouldReadLatest( int $flags ) {
return ( $flags & ( self::READ_LATEST | self::GAID_FOR_UPDATE ) ) > 0;
}
/** /**
* Get the page's content model id, see the CONTENT_MODEL_XXX constants. * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
* *
@ -1086,15 +1096,8 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
return $this->mContentModel; return $this->mContentModel;
} }
if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { if ( $this->shouldReadLatest( $flags ) || !$this->mContentModel ) {
$this->lazyFillContentModel( $this->loadFieldFromDB( 'page_content_model', $flags ) ); $this->lazyFillContentModel( $this->getFieldFromPageStore( 'page_content_model', $flags ) );
} elseif (
( !$this->mContentModel || $flags & self::GAID_FOR_UPDATE ) &&
$this->getArticleID( $flags )
) {
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->addLinkObj( $this ); # in case we already had an article ID
$this->lazyFillContentModel( $linkCache->getGoodLinkFieldObj( $this, 'model' ) );
} }
if ( !$this->mContentModel ) { if ( !$this->mContentModel ) {
@ -2860,25 +2863,14 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
* @return int The ID * @return int The ID
*/ */
public function getArticleID( $flags = 0 ) { public function getArticleID( $flags = 0 ) {
if ( !$this->canExist() ) { if ( $this->mArticleID === -1 && !$this->canExist() ) {
$this->mArticleID = 0; $this->mArticleID = 0;
return $this->mArticleID; return $this->mArticleID;
} }
if ( $flags & self::GAID_FOR_UPDATE ) { if ( $this->mArticleID === -1 || $this->shouldReadLatest( $flags ) ) {
$linkCache = MediaWikiServices::getInstance()->getLinkCache(); $this->mArticleID = (int)$this->getFieldFromPageStore( 'page_id', $flags );
$linkCache->clearLink( $this );
$this->mArticleID = $linkCache->addLinkObj( $this, self::READ_LATEST );
} elseif ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
// If mArticleID is >0, pageCond() will use it, making it impossible
// for the call below to return a different result, e.g. after a
// page move.
$this->mArticleID = -1;
$this->mArticleID = (int)$this->loadFieldFromDB( 'page_id', $flags );
} elseif ( $this->mArticleID == -1 ) {
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$this->mArticleID = $linkCache->addLinkObj( $this );
} }
return $this->mArticleID; return $this->mArticleID;
@ -2899,18 +2891,8 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
* @return bool * @return bool
*/ */
public function isRedirect( $flags = 0 ) { public function isRedirect( $flags = 0 ) {
if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { if ( $this->shouldReadLatest( $flags ) || $this->mRedirect === null ) {
$this->mRedirect = (bool)$this->loadFieldFromDB( 'page_is_redirect', $flags ); $this->mRedirect = (bool)$this->getFieldFromPageStore( 'page_is_redirect', $flags );
} elseif ( $this->mRedirect === null ) {
if ( $this->getArticleID( $flags ) ) {
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->addLinkObj( $this ); // in case we already had an article ID
// Note that LinkCache returns null if it thinks the page does not exist;
// always trust the state of LinkCache over that of this Title instance.
$this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
} else {
$this->mRedirect = false;
}
} }
return $this->mRedirect; return $this->mRedirect;
@ -2924,21 +2906,12 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
* @return int * @return int
*/ */
public function getLength( $flags = 0 ) { public function getLength( $flags = 0 ) {
if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { if ( $this->shouldReadLatest( $flags ) || $this->mLength < 0 ) {
$this->mLength = (int)$this->loadFieldFromDB( 'page_len', $flags ); $this->mLength = (int)$this->getFieldFromPageStore( 'page_len', $flags );
} else { }
if ( $this->mLength != -1 ) {
return $this->mLength;
} elseif ( !$this->getArticleID( $flags ) ) {
$this->mLength = 0;
return $this->mLength;
}
$linkCache = MediaWikiServices::getInstance()->getLinkCache(); if ( $this->mLength < 0 ) {
$linkCache->addLinkObj( $this ); // in case we already had an article ID $this->mLength = 0;
// Note that LinkCache returns null if it thinks the page does not exist;
// always trust the state of LinkCache over that of this Title instance.
$this->mLength = (int)$linkCache->getGoodLinkFieldObj( $this, 'length' );
} }
return $this->mLength; return $this->mLength;
@ -2951,22 +2924,12 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
* @return int Int or 0 if the page doesn't exist * @return int Int or 0 if the page doesn't exist
*/ */
public function getLatestRevID( $flags = 0 ) { public function getLatestRevID( $flags = 0 ) {
if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { if ( $this->shouldReadLatest( $flags ) || $this->mLatestID === false ) {
$this->mLatestID = (int)$this->loadFieldFromDB( 'page_latest', $flags ); $this->mLatestID = (int)$this->getFieldFromPageStore( 'page_latest', $flags );
} else { }
if ( $this->mLatestID !== false ) {
return (int)$this->mLatestID;
} elseif ( !$this->getArticleID( $flags ) ) {
$this->mLatestID = 0;
return $this->mLatestID; if ( !$this->mLatestID ) {
} $this->mLatestID = 0;
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->addLinkObj( $this ); // in case we already had an article ID
// Note that LinkCache returns null if it thinks the page does not exist;
// always trust the state of LinkCache over that of this Title instance.
$this->mLatestID = (int)$linkCache->getGoodLinkFieldObj( $this, 'revision' );
} }
return $this->mLatestID; return $this->mLatestID;
@ -3384,13 +3347,16 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
} }
/** /**
* Check if this is a new page * Check if this is a new page.
*
* @note This returns false if the page does not exist.
* @param int $flags one of the READ_XXX constants.
* *
* @return bool * @return bool
*/ */
public function isNewPage() { public function isNewPage( $flags = self::READ_NORMAL ) {
$dbr = wfGetDB( DB_REPLICA ); // NOTE: we rely on PHP casting "0" to false here.
return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); return (bool)$this->getFieldFromPageStore( 'page_is_new', $flags );
} }
/** /**
@ -3757,15 +3723,24 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
/** /**
* Get the last touched timestamp * Get the last touched timestamp
* *
* @param IDatabase|null $db * @param int $flags one of the READ_XXX constants. For historical reasons, an IDatabase
* instance is also accepted here. If an IDatabase is passed, a deprecation warning
* is triggered, caches will be bypassed, and the primary database connection will be
* used. However, the IDatabase instance itself will be ignored.
* @return string|false Last-touched timestamp * @return string|false Last-touched timestamp
*/ */
public function getTouched( $db = null ) { public function getTouched( $flags = self::READ_NORMAL ) {
if ( $db === null ) { if ( is_object( $flags ) ) {
$db = wfGetDB( DB_REPLICA ); wfDeprecatedMsg(
__METHOD__ . ' was called with a ' . get_class( $flags )
. ' instance instead of an integer!',
'1.38'
);
$flags = self::READ_LATEST;
} }
$touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
return $touched; $touched = $this->getFieldFromPageStore( 'page_touched', $flags );
return MWTimestamp::convert( TS_MW, $touched );
} }
/** /**
@ -3938,20 +3913,20 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
* to true in LocalSettings.php, otherwise returns false. If there is no language saved in * to true in LocalSettings.php, otherwise returns false. If there is no language saved in
* the db, it will return NULL. * the db, it will return NULL.
* *
* @return string|null|bool * @param int $flags
*
* @return ?string
*/ */
private function getDbPageLanguageCode() { private function getDbPageLanguageCode( int $flags = 0 ): ?string {
global $wgPageLanguageUseDB; global $wgPageLanguageUseDB;
// check, if the page language could be saved in the database, and if so and // check, if the page language could be saved in the database, and if so and
// the value is not requested already, lookup the page language using LinkCache // the value is not requested already, lookup the page language using PageStore
if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) { if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
$linkCache = MediaWikiServices::getInstance()->getLinkCache(); $this->mDbPageLanguage = $this->getFieldFromPageStore( 'page_lang', $flags );
$linkCache->addLinkObj( $this );
$this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
} }
return $this->mDbPageLanguage; return $this->mDbPageLanguage ?: null;
} }
/** /**
@ -4124,23 +4099,44 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
/** /**
* @param string $field * @param string $field
* @param int $flags Bitfield of class READ_* constants * @param int $flags Bitfield of class READ_* constants
* @return string|bool * @return string|false
*/ */
private function loadFieldFromDB( $field, $flags ) { private function getFieldFromPageStore( $field, $flags ) {
if ( !in_array( $field, self::getSelectFields(), true ) ) { $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
return false; // field does not exist
$pageStore = MediaWikiServices::getInstance()->getPageStore();
if ( !in_array( $field, $pageStore->getSelectFields(), true ) ) {
throw new InvalidArgumentException( "Unknown field: $field" );
} }
$flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c if ( $flags === self::READ_NORMAL && $this->mArticleID === 0 ) {
list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags ); // page does not exist
return false;
}
return wfGetDB( $index )->selectField( if ( !$this->canExist() ) {
'page', return false;
$field, }
$this->pageCond(),
__METHOD__, if ( $this->mArticleID > 0 && $field !== 'page_id' ) {
$options // NOTE: if we already have a page ID, we trust it.
); $page = $pageStore->getPageById( $this->getArticleID(), $flags );
} elseif ( $field === 'page_id' ) {
// NOTE: When looking up the page ID, don't use PageStore::getPageByReference().
// getPageByReference() would call exists() and getId(), which would land
// us back here, recursing until we run out of stack.
$page = $pageStore->getPageByName( $this->getNamespace(), $this->getDBkey(), $flags );
} else {
$page = $pageStore->getPageByReference( $this, $flags );
}
if ( $page instanceof PageStoreRecord ) {
return $page->getField( $field );
} else {
// page does not exist
return false;
}
} }
/** /**
@ -4277,10 +4273,10 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject {
'page_title' => $this->getDBkey(), 'page_title' => $this->getDBkey(),
'page_wiki_id' => $this->getWikiId(), 'page_wiki_id' => $this->getWikiId(),
'page_latest' => $this->getLatestRevID( $flags ), 'page_latest' => $this->getLatestRevID( $flags ),
'page_is_new' => $this->isNewPage(), // no flags? 'page_is_new' => $this->isNewPage( $flags ),
'page_is_redirect' => $this->isRedirect( $flags ), 'page_is_redirect' => $this->isRedirect( $flags ),
'page_touched' => $this->getTouched(), // no flags? 'page_touched' => $this->getTouched( $flags ),
'page_lang' => $this->getDbPageLanguageCode() ?: null, 'page_lang' => $this->getDbPageLanguageCode( $flags ),
], ],
PageIdentity::LOCAL PageIdentity::LOCAL
); );

View file

@ -103,6 +103,7 @@ interface PageLookup extends IDBAccessObject {
/** /**
* Returns the PageRecord of the given page. * Returns the PageRecord of the given page.
* May return $page if that already is a PageRecord. * May return $page if that already is a PageRecord.
* If $page is a PageIdentity, implementations may call methods like exists() and getId() on it.
* *
* The PageReference must refer to a proper page - that is, it must not refer to a special page. * The PageReference must refer to a proper page - that is, it must not refer to a special page.
* *

View file

@ -324,6 +324,9 @@ class PageStore implements PageLookup {
// if we have a page ID, use it // if we have a page ID, use it
$id = $page->getId( $this->wikiId ); $id = $page->getId( $this->wikiId );
return $this->getPageById( $id, $queryFlags ); return $this->getPageById( $id, $queryFlags );
} elseif ( $queryFlags === self::READ_NORMAL ) {
// The page does not appear to exist, and we don't have to check again.
return null;
} }
} }

View file

@ -119,8 +119,25 @@ class PageStoreRecord extends PageIdentityValue implements ExistingPageRecord {
* @return ?string * @return ?string
*/ */
public function getLanguage(): ?string { public function getLanguage(): ?string {
// field may be missing return $this->getField( 'page_lang' );
return $this->row->page_lang ?? null; }
/**
* Return the raw value for the given field as returned by the database query.
*
* Numeric values may be encoded as strings.
* Boolean values may be represented as integers (or numeric strings).
* Timestamps will use the database's native format.
*
* @internal
*
* @param string $field
*
* @return string|int|bool|null
*/
public function getField( string $field ) {
// Field may be missing entirely.
return $this->row->$field ?? null;
} }
} }

View file

@ -516,6 +516,7 @@ class TitleTest extends MediaWikiIntegrationTestCase {
$this->assertSame( MWTimestamp::convert( TS_MW, $title->getTouched() ), $record->getTouched() ); $this->assertSame( MWTimestamp::convert( TS_MW, $title->getTouched() ), $record->getTouched() );
$this->assertSame( $title->isNewPage(), $record->isNew() ); $this->assertSame( $title->isNewPage(), $record->isNew() );
$this->assertSame( $title->isRedirect(), $record->isRedirect() ); $this->assertSame( $title->isRedirect(), $record->isRedirect() );
$this->assertSame( $title->getTouched(), $record->getTouched() );
} }
/** /**

View file

@ -100,6 +100,10 @@ class PageStoreRecordTest extends MediaWikiUnitTestCase {
$this->assertSame( $row->page_is_redirect, $pageRecord->isRedirect() ); $this->assertSame( $row->page_is_redirect, $pageRecord->isRedirect() );
$this->assertSame( $row->page_lang ?? null, $pageRecord->getLanguage() ); $this->assertSame( $row->page_lang ?? null, $pageRecord->getLanguage() );
foreach ( $row as $name => $value ) {
$this->assertEquals( $value, $pageRecord->getField( $name ) );
}
} }
public function badConstructorProvider() { public function badConstructorProvider() {