repo/includes/Store/Sql/SqlStore.php
<?php
namespace Wikibase\Repo\Store\Sql;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\MediaWikiServices;
use ObjectCacheFactory;
use Wikibase\DataAccess\DatabaseEntitySource;
use Wikibase\DataAccess\WikibaseServices;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Services\Entity\EntityPrefetcher;
use Wikibase\DataModel\Services\EntityId\EntityIdComposer;
use Wikibase\DataModel\Services\Lookup\EntityLookup;
use Wikibase\DataModel\Services\Lookup\EntityRedirectLookup;
use Wikibase\DataModel\Services\Lookup\RedirectResolvingEntityLookup;
use Wikibase\Lib\Changes\EntityChangeFactory;
use Wikibase\Lib\EntityTypeDefinitions;
use Wikibase\Lib\SettingsArray;
use Wikibase\Lib\Store\CacheAwarePropertyInfoStore;
use Wikibase\Lib\Store\CacheRetrievingEntityRevisionLookup;
use Wikibase\Lib\Store\CachingEntityRevisionLookup;
use Wikibase\Lib\Store\EntityByLinkedTitleLookup;
use Wikibase\Lib\Store\EntityIdLookup;
use Wikibase\Lib\Store\EntityNamespaceLookup;
use Wikibase\Lib\Store\EntityRevisionCache;
use Wikibase\Lib\Store\EntityRevisionLookup;
use Wikibase\Lib\Store\EntityStore;
use Wikibase\Lib\Store\EntityStoreWatcher;
use Wikibase\Lib\Store\LookupConstants;
use Wikibase\Lib\Store\PropertyInfoLookup;
use Wikibase\Lib\Store\PropertyInfoStore;
use Wikibase\Lib\Store\RevisionBasedEntityLookup;
use Wikibase\Lib\Store\SiteLinkStore;
use Wikibase\Lib\Store\Sql\EntityChangeLookup;
use Wikibase\Lib\Store\Sql\PrefetchingWikiPageEntityMetaDataAccessor;
use Wikibase\Lib\Store\Sql\PropertyInfoTable;
use Wikibase\Lib\Store\Sql\SiteLinkTable;
use Wikibase\Lib\Store\Sql\SqlChangeStore;
use Wikibase\Lib\Store\TypeDispatchingEntityRevisionLookup;
use Wikibase\Lib\Store\TypeDispatchingEntityStore;
use Wikibase\Repo\Store\DispatchingEntityStoreWatcher;
use Wikibase\Repo\Store\EntityTitleStoreLookup;
use Wikibase\Repo\Store\IdGenerator;
use Wikibase\Repo\Store\ItemsWithoutSitelinksFinder;
use Wikibase\Repo\Store\Store;
use Wikibase\Repo\WikibaseRepo;
use Wikimedia\ObjectCache\EmptyBagOStuff;
use Wikimedia\ObjectCache\HashBagOStuff;
/**
* Implementation of the store interface using an SQL backend via MediaWiki's
* storage abstraction layer.
*
* @license GPL-2.0-or-later
* @author Daniel Kinzler
*/
class SqlStore implements Store {
/**
* @var EntityChangeFactory
*/
private $entityChangeFactory;
/**
* @var EntityIdParser
*/
private $entityIdParser;
/**
* @var EntityIdComposer
*/
private $entityIdComposer;
/**
* @var EntityRevisionLookup|null
*/
private $entityRevisionLookup = null;
/**
* @var EntityRevisionLookup|null
*/
private $rawEntityRevisionLookup = null;
/**
* @var CacheRetrievingEntityRevisionLookup|null
*/
private $cacheRetrievingEntityRevisionLookup = null;
/**
* @var EntityStore|null
*/
private $entityStore = null;
/**
* @var DispatchingEntityStoreWatcher|null
*/
private $entityStoreWatcher = null;
/**
* @var PropertyInfoLookup
*/
private PropertyInfoLookup $propertyInfoLookup;
/**
* @var PropertyInfoStore|null
*/
private $propertyInfoStore = null;
/**
* @var PropertyInfoTable|null
*/
private $propertyInfoTable = null;
/**
* @var PrefetchingWikiPageEntityMetaDataAccessor|null
*/
private $entityPrefetcher = null;
/**
* @var EntityIdLookup
*/
private $entityIdLookup;
/**
* @var EntityTitleStoreLookup
*/
private $entityTitleLookup;
/**
* @var EntityNamespaceLookup
*/
private $entityNamespaceLookup;
/**
* @var WikibaseServices
*/
private $wikibaseServices;
/**
* @var HookContainer
*/
private $hookContainer;
/**
* @var string
*/
private $cacheKeyPrefix;
/**
* @var string
*/
private $cacheKeyGroup;
/**
* @var int
*/
private $cacheType;
/**
* @var int
*/
private $cacheDuration;
/**
* @var IdGenerator
*/
private $idGenerator;
/** @var DatabaseEntitySource */
private $entitySource;
/**
* @var ObjectCacheFactory
*/
private $objectCacheFactory;
/**
* @param EntityChangeFactory $entityChangeFactory
* @param EntityIdParser $entityIdParser
* @param EntityIdComposer $entityIdComposer
* @param EntityIdLookup $entityIdLookup
* @param EntityTitleStoreLookup $entityTitleLookup
* @param EntityNamespaceLookup $entityNamespaceLookup
* @param IdGenerator $idGenerator
* @param WikibaseServices $wikibaseServices Service container providing data access services
* @param HookContainer $hookContainer Service container providing data access services
* @param DatabaseEntitySource $entitySource
* @param SettingsArray $settings
*/
public function __construct(
EntityChangeFactory $entityChangeFactory,
EntityIdParser $entityIdParser,
EntityIdComposer $entityIdComposer,
EntityIdLookup $entityIdLookup,
EntityTitleStoreLookup $entityTitleLookup,
EntityNamespaceLookup $entityNamespaceLookup,
IdGenerator $idGenerator,
WikibaseServices $wikibaseServices,
HookContainer $hookContainer,
DatabaseEntitySource $entitySource,
SettingsArray $settings,
PropertyInfoLookup $propertyInfoLookup,
ObjectCacheFactory $objectCacheFactory
) {
$this->entityChangeFactory = $entityChangeFactory;
$this->entityIdParser = $entityIdParser;
$this->entityIdComposer = $entityIdComposer;
$this->entityIdLookup = $entityIdLookup;
$this->entityTitleLookup = $entityTitleLookup;
$this->entityNamespaceLookup = $entityNamespaceLookup;
$this->idGenerator = $idGenerator;
$this->wikibaseServices = $wikibaseServices;
$this->hookContainer = $hookContainer;
$this->entitySource = $entitySource;
$this->objectCacheFactory = $objectCacheFactory;
$this->cacheKeyPrefix = $settings->getSetting( 'sharedCacheKeyPrefix' );
$this->cacheKeyGroup = $settings->getSetting( 'sharedCacheKeyGroup' );
$this->cacheType = $settings->getSetting( 'sharedCacheType' );
$this->cacheDuration = $settings->getSetting( 'sharedCacheDuration' );
$this->propertyInfoLookup = $propertyInfoLookup;
}
/**
* @see Store::newSiteLinkStore
*
* @return SiteLinkStore
*/
public function newSiteLinkStore() {
return new SiteLinkTable(
'wb_items_per_site',
false,
WikibaseRepo::getRepoDomainDbFactory()->newForEntitySource( $this->entitySource )
);
}
/**
* @see Store::getEntityByLinkedTitleLookup
*
* @return EntityByLinkedTitleLookup
*/
public function getEntityByLinkedTitleLookup() {
$lookup = $this->newSiteLinkStore();
$this->hookContainer->run( 'GetEntityByLinkedTitleLookup', [ &$lookup ] );
return $lookup;
}
/**
* @see Store::newItemsWithoutSitelinksFinder
*
* @return ItemsWithoutSitelinksFinder
*/
public function newItemsWithoutSitelinksFinder() {
return new SqlItemsWithoutSitelinksFinder(
$this->entityNamespaceLookup,
WikibaseRepo::getRepoDomainDbFactory()->newForEntitySource( $this->entitySource )
);
}
/**
* @return EntityRedirectLookup
*/
public function getEntityRedirectLookup() {
return new WikiPageEntityRedirectLookup(
$this->entityTitleLookup,
$this->entityIdLookup,
WikibaseRepo::getRepoDomainDbFactory()->newForEntitySource( $this->entitySource )
);
}
/**
* @see Store::getEntityLookup
* @see SqlStore::getEntityRevisionLookup
*
* The EntityLookup returned by this method will resolve redirects.
*
* @param string $cache One of self::LOOKUP_CACHING_*
* @see self::LOOKUP_CACHING_DISABLED to get an uncached direct lookup
* self::LOOKUP_CACHING_RETRIEVE_ONLY to get a lookup which reads from the cache, but doesn't store retrieved entities
* self::LOOKUP_CACHING_ENABLED to get a caching lookup (default)
*
* @param string $lookupMode One of LookupConstants::LATEST_FROM_*
*
* @return EntityLookup
*/
public function getEntityLookup( $cache = self::LOOKUP_CACHING_ENABLED, string $lookupMode = LookupConstants::LATEST_FROM_REPLICA ) {
$revisionLookup = $this->getEntityRevisionLookup( $cache );
$revisionBasedLookup = new RevisionBasedEntityLookup( $revisionLookup, $lookupMode );
$resolvingLookup = new RedirectResolvingEntityLookup( $revisionBasedLookup );
return $resolvingLookup;
}
/**
* @see Store::getEntityStoreWatcher
*
* @return EntityStoreWatcher
*/
public function getEntityStoreWatcher() {
if ( !$this->entityStoreWatcher ) {
$this->entityStoreWatcher = new DispatchingEntityStoreWatcher();
}
return $this->entityStoreWatcher;
}
/**
* @see Store::getEntityStore
*
* @return EntityStore
*/
public function getEntityStore() {
if ( !$this->entityStore ) {
$this->entityStore = $this->newEntityStore();
}
return $this->entityStore;
}
/**
* @return EntityStore
*/
private function newEntityStore() {
$contentFactory = WikibaseRepo::getEntityContentFactory();
$entityTitleStoreLookup = WikibaseRepo::getEntityTitleStoreLookup();
$services = MediaWikiServices::getInstance();
$store = new WikiPageEntityStore(
$contentFactory,
$entityTitleStoreLookup,
$this->idGenerator,
$this->entityIdComposer,
$services->getRevisionStore(),
$this->entitySource,
$services->getActorNormalization(),
$services->getPermissionManager(),
$services->getWatchlistManager(),
$services->getWikiPageFactory(),
WikibaseRepo::getRepoDomainDbFactory( $services )->newRepoDb()
);
$store->registerWatcher( $this->getEntityStoreWatcher() );
$store = new TypeDispatchingEntityStore(
WikibaseRepo::getEntityTypeDefinitions() // TODO inject
->get( EntityTypeDefinitions::ENTITY_STORE_FACTORY_CALLBACK ),
$store,
$this->getEntityRevisionLookup( self::LOOKUP_CACHING_DISABLED )
);
return $store;
}
/**
* @see Store::getEntityRevisionLookup
*
* @param string $cache One of self::LOOKUP_CACHING_*
* self::LOOKUP_CACHING_DISABLED to get an uncached direct lookup
* self::LOOKUP_CACHING_RETRIEVE_ONLY to get a lookup which reads from the cache, but doesn't store retrieved entities
* self::LOOKUP_CACHING_ENABLED to get a caching lookup (default)
*
* @return EntityRevisionLookup
*/
public function getEntityRevisionLookup( $cache = self::LOOKUP_CACHING_ENABLED ) {
if ( !$this->entityRevisionLookup ) {
[ $this->rawEntityRevisionLookup, $this->entityRevisionLookup ] = $this->newEntityRevisionLookup();
}
if ( $cache === self::LOOKUP_CACHING_DISABLED ) {
return $this->rawEntityRevisionLookup;
} elseif ( $cache === self::LOOKUP_CACHING_RETRIEVE_ONLY ) {
return $this->getCacheRetrievingEntityRevisionLookup();
} else {
return $this->entityRevisionLookup;
}
}
/**
* @return string
*/
private function getEntityRevisionLookupCacheKey() {
// NOTE: Keep cache key in sync with DirectSqlStore::newEntityRevisionLookup in WikibaseClient
return $this->cacheKeyPrefix . ':WikiPageEntityRevisionLookup';
}
/**
* Creates a strongly connected pair of EntityRevisionLookup services, the first being the
* non-caching lookup, the second being the caching lookup.
*
* @return EntityRevisionLookup[] A two-element array with a "raw", non-caching and a caching
* EntityRevisionLookup.
*/
private function newEntityRevisionLookup() {
// Maintain a list of watchers to be notified of changes to any entities,
// in order to update caches.
/** @var WikiPageEntityStore $dispatcher */
$dispatcher = $this->getEntityStoreWatcher();
'@phan-var WikiPageEntityStore $dispatcher';
$dispatcher->registerWatcher( $this->wikibaseServices->getEntityStoreWatcher() );
$nonCachingLookup = $this->wikibaseServices->getEntityRevisionLookup();
$nonCachingLookup = new TypeDispatchingEntityRevisionLookup(
WikibaseRepo::getEntityTypeDefinitions() // TODO inject
->get( EntityTypeDefinitions::ENTITY_REVISION_LOOKUP_FACTORY_CALLBACK ),
$nonCachingLookup
);
// Lower caching layer using persistent cache (e.g. memcached).
$persistentCachingLookup = new CachingEntityRevisionLookup(
new EntityRevisionCache(
new EmptyBagOStuff(),
$this->cacheDuration,
$this->getEntityRevisionLookupCacheKey()
),
$nonCachingLookup
);
// We need to verify the revision ID against the database to avoid stale data.
$persistentCachingLookup->setVerifyRevision( true );
$dispatcher->registerWatcher( $persistentCachingLookup );
// Top caching layer using an in-process hash.
$hashCachingLookup = new CachingEntityRevisionLookup(
new EntityRevisionCache( new HashBagOStuff( [ 'maxKeys' => 1000 ] ) ),
$persistentCachingLookup
);
// No need to verify the revision ID, we'll ignore updates that happen during the request.
$hashCachingLookup->setVerifyRevision( false );
$dispatcher->registerWatcher( $hashCachingLookup );
return [ $nonCachingLookup, $hashCachingLookup ];
}
/**
* @return CacheRetrievingEntityRevisionLookup
*/
private function getCacheRetrievingEntityRevisionLookup() {
if ( !$this->cacheRetrievingEntityRevisionLookup ) {
$cacheRetrievingEntityRevisionLookup = new CacheRetrievingEntityRevisionLookup(
new EntityRevisionCache(
$this->objectCacheFactory->getInstance( $this->cacheType ),
$this->cacheDuration,
$this->getEntityRevisionLookupCacheKey()
),
$this->getEntityRevisionLookup( self::LOOKUP_CACHING_DISABLED )
);
$cacheRetrievingEntityRevisionLookup->setVerifyRevision( true );
$this->cacheRetrievingEntityRevisionLookup = $cacheRetrievingEntityRevisionLookup;
}
return $this->cacheRetrievingEntityRevisionLookup;
}
/**
* @deprecated use WikibaseRepo::getPropertyInfoLookup instead
*
* @return PropertyInfoLookup
*/
public function getPropertyInfoLookup(): PropertyInfoLookup {
return $this->propertyInfoLookup;
}
/**
* @see Store::getPropertyInfoStore
*
* @return PropertyInfoStore
*/
public function getPropertyInfoStore() {
if ( !$this->propertyInfoStore ) {
$this->propertyInfoStore = $this->newPropertyInfoStore();
}
return $this->propertyInfoStore;
}
/**
* Creates a new PropertyInfoStore
* Note: cache key used by the lookup should be the same as the cache key used
* by CachedPropertyInfoLookup.
*
* @return PropertyInfoStore
*/
private function newPropertyInfoStore() {
// TODO: this should be changed so it uses the same PropertyInfoTable instance which is used by the
// lookup configured for local entity source As we don't want to introduce DispatchingPropertyInfoStore service.
$table = $this->getPropertyInfoTable();
// TODO: we might want to register the CachingPropertyInfoLookup instance defined in
// WikibaseRepo.PropertyInfoLookup as a watcher to this CacheAwarePropertyInfoStore instance.
return new CacheAwarePropertyInfoStore(
new CacheAwarePropertyInfoStore(
$table,
MediaWikiServices::getInstance()->getMainWANObjectCache(),
$this->cacheDuration,
$this->cacheKeyGroup
),
MediaWikiServices::getInstance()->getLocalServerObjectCache(),
$this->cacheDuration,
$this->cacheKeyGroup
);
}
/**
* @return PropertyInfoTable
*/
private function getPropertyInfoTable() {
if ( $this->propertyInfoTable === null ) {
$this->propertyInfoTable = new PropertyInfoTable(
$this->entityIdComposer,
WikibaseRepo::getRepoDomainDbFactory()->newForEntitySource( $this->entitySource ),
true
);
}
return $this->propertyInfoTable;
}
/**
* @return PrefetchingWikiPageEntityMetaDataAccessor
*/
public function getEntityPrefetcher() {
if ( $this->entityPrefetcher === null ) {
$this->entityPrefetcher = $this->newEntityPrefetcher();
}
return $this->entityPrefetcher;
}
/**
* @return EntityPrefetcher
*/
private function newEntityPrefetcher() {
return $this->wikibaseServices->getEntityPrefetcher();
}
/**
* @return EntityChangeLookup
*/
public function getEntityChangeLookup() {
return new EntityChangeLookup(
$this->entityChangeFactory,
$this->entityIdParser,
WikibaseRepo::getRepoDomainDbFactory()->newRepoDb()
);
}
/**
* @return SqlChangeStore
*/
public function getChangeStore() {
return new SqlChangeStore(
WikibaseRepo::getRepoDomainDbFactory()
->newForEntitySource( $this->entitySource )
);
}
}