lib/includes/Store/Sql/PropertyInfoTable.php
<?php
declare( strict_types = 1 );
namespace Wikibase\Lib\Store\Sql;
use InvalidArgumentException;
use Wikibase\DataModel\Entity\NumericPropertyId;
use Wikibase\DataModel\Entity\Property;
use Wikibase\DataModel\Entity\PropertyId;
use Wikibase\DataModel\Services\EntityId\EntityIdComposer;
use Wikibase\Lib\Rdbms\RepoDomainDb;
use Wikibase\Lib\Store\PropertyInfoLookup;
use Wikibase\Lib\Store\PropertyInfoStore;
use Wikimedia\Assert\Assert;
use Wikimedia\Rdbms\DBError;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\IReadableDatabase;
use Wikimedia\Rdbms\IResultWrapper;
/**
* Class PropertyInfoTable implements PropertyInfoStore on top of an SQL table.
*
* @license GPL-2.0-or-later
* @author Daniel Kinzler
* @author Bene* < benestar.wikimedia@gmail.com >
*/
class PropertyInfoTable implements PropertyInfoLookup, PropertyInfoStore {
private const TABLE_NAME = 'wb_property_info';
private EntityIdComposer $entityIdComposer;
private RepoDomainDb $db;
private bool $allowWrites;
/**
* @param EntityIdComposer $entityIdComposer
* @param RepoDomainDb $db
* @param bool $allowWrites Should writes be allowed to the table? false in cases that a remote property source is being used.
*
* TODO split this more cleanly into a lookup and a writer, and then $allowWrites would not be needed?
*/
public function __construct(
EntityIdComposer $entityIdComposer,
RepoDomainDb $db,
bool $allowWrites
) {
$this->entityIdComposer = $entityIdComposer;
$this->db = $db;
$this->allowWrites = $allowWrites;
}
/**
* Decodes an info blob.
*
* @param string|null|bool $blob
*
* @return array|null The decoded blob as an associative array, or null if the blob
* could not be decoded.
*/
private function decodeBlob( $blob ): ?array {
if ( $blob === false || $blob === null ) {
return null;
}
$info = json_decode( $blob, true );
if ( !is_array( $info ) ) {
$info = null;
}
return $info;
}
/**
* Decodes a result with info blobs.
*
* @param IResultWrapper $res
*
* @return array[] The array of decoded blobs
*/
private function decodeResult( IResultWrapper $res ): array {
$infos = [];
foreach ( $res as $row ) {
$info = $this->decodeBlob( $row->pi_info );
if ( $info === null ) {
wfLogWarning( "failed to decode property info blob for property "
. $row->pi_property_id . ": " . $row->pi_info );
continue;
}
$id = $this->entityIdComposer->composeEntityId(
Property::ENTITY_TYPE,
$row->pi_property_id
);
$infos[$id->getSerialization()] = $info;
}
return $infos;
}
/**
* @see PropertyInfoLookup::getPropertyInfo
*
* @param PropertyId $propertyId
*
* @return array|null
* @throws InvalidArgumentException
* @throws DBError
*/
public function getPropertyInfo( PropertyId $propertyId ): ?array {
Assert::parameterType( NumericPropertyId::class, $propertyId, '$propertyId' );
/** @var NumericPropertyId $propertyId */
'@phan-var NumericPropertyId $propertyId';
$dbr = $this->getReadConnection();
$res = $dbr->newSelectQueryBuilder()
->select( 'pi_info' )
->from( self::TABLE_NAME )
->where( [ 'pi_property_id' => $propertyId->getNumericId() ] )
->caller( __METHOD__ )
->fetchField();
if ( $res === false ) {
$info = null;
} else {
$info = $this->decodeBlob( $res );
if ( $info === null ) {
wfLogWarning( "failed to decode property info blob for " . $propertyId . ": " . substr( $res, 0, 200 ) );
}
}
return $info;
}
/**
* @see PropertyInfoLookup::getPropertyInfoForDataType
*
* @param string $dataType
*
* @return array[] Array containing serialized property IDs as keys and info arrays as values
* @throws DBError
*/
public function getPropertyInfoForDataType( $dataType ) {
$dbr = $this->getReadConnection();
$res = $dbr->newSelectQueryBuilder()
->select( [ 'pi_property_id', 'pi_info' ] )
->from( self::TABLE_NAME )
->where( [ 'pi_type' => $dataType ] )
->caller( __METHOD__ )
->fetchResultSet();
$infos = $this->decodeResult( $res );
return $infos;
}
/**
* @see PropertyInfoLookup::getAllPropertyInfo
*
* @return array[] Array containing serialized property IDs as keys and info arrays as values
* @throws DBError
*/
public function getAllPropertyInfo() {
$dbr = $this->getReadConnection();
$res = $dbr->newSelectQueryBuilder()
->select( [ 'pi_property_id', 'pi_info' ] )
->from( self::TABLE_NAME )
->caller( __METHOD__ )
->fetchResultSet();
$infos = $this->decodeResult( $res );
return $infos;
}
/**
* @see PropertyInfoStore::setPropertyInfo
*
* @param NumericPropertyId $propertyId
* @param array $info
*
* @throws DBError
* @throws InvalidArgumentException
*/
public function setPropertyInfo( NumericPropertyId $propertyId, array $info ) {
if ( !isset( $info[ PropertyInfoLookup::KEY_DATA_TYPE ] ) ) {
throw new InvalidArgumentException( 'Missing required info field: ' . PropertyInfoLookup::KEY_DATA_TYPE );
}
$this->assertCanWritePropertyInfo();
$type = $info[ PropertyInfoLookup::KEY_DATA_TYPE ];
$json = json_encode( $info );
$dbw = $this->getWriteConnection();
$dbw->newReplaceQueryBuilder()
->replaceInto( self::TABLE_NAME )
->uniqueIndexFields( 'pi_property_id' )
->row( [
'pi_property_id' => $propertyId->getNumericId(),
'pi_info' => $json,
'pi_type' => $type,
] )
->caller( __METHOD__ )
->execute();
}
/**
* @see PropertyInfoStore::removePropertyInfo
*
* @param NumericPropertyId $propertyId
*
* @throws DBError
* @throws InvalidArgumentException
* @return bool
*/
public function removePropertyInfo( NumericPropertyId $propertyId ) {
$this->assertCanWritePropertyInfo();
$dbw = $this->getWriteConnection();
$dbw->newDeleteQueryBuilder()
->deleteFrom( self::TABLE_NAME )
->where( [ 'pi_property_id' => $propertyId->getNumericId() ] )
->caller( __METHOD__ )
->execute();
$c = $dbw->affectedRows();
return $c > 0;
}
private function assertCanWritePropertyInfo(): void {
if ( !$this->allowWrites ) {
throw new InvalidArgumentException(
'This implementation cannot be used to write data to non-local database'
);
}
}
private function getWriteConnection(): IDatabase {
return $this->db->connections()->getWriteConnection();
}
private function getReadConnection(): IReadableDatabase {
return $this->db->connections()->getReadConnection();
}
/**
* Returns a database wrapper suitable for working with the database that
* contains the property info table.
*
* This is for use by closely related classes that want to operate directly
* on the database table.
*/
public function getDomainDb(): RepoDomainDb {
return $this->db;
}
/**
* Returns the (logical) name of the database table that contains the property info.
*
* This is for use for closely related classes that want to operate directly
* on the database table.
*/
public function getTableName(): string {
return self::TABLE_NAME;
}
}