wikimedia/mediawiki-extensions-Wikibase

View on GitHub
lib/includes/Store/Sql/PageTableEntityQueryBase.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

declare( strict_types = 1 );

namespace Wikibase\Lib\Store\Sql;

use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\NameTableAccessException;
use MediaWiki\Storage\NameTableStore;
use stdClass;
use Traversable;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Services\Lookup\EntityLookupException;
use Wikibase\Lib\Store\EntityNamespaceLookup;
use Wikimedia\Rdbms\IReadableDatabase;
use Wikimedia\Rdbms\RawSQLExpression;
use Wikimedia\Rdbms\SelectQueryBuilder;

/**
 * Abstract PageTableEntityQuery implementation allowing simple mapping between rows and entity IDs
 * using one or more fields and some simple logic.
 *
 * @license GPL-2.0-or-later
 */
abstract class PageTableEntityQueryBase implements PageTableEntityQuery {

    private EntityNamespaceLookup $entityNamespaceLookup;

    private NameTableStore $slotRoleStore;

    public function __construct(
        EntityNamespaceLookup $entityNamespaceLookup,
        NameTableStore $slotRoleStore
    ) {
        $this->entityNamespaceLookup = $entityNamespaceLookup;
        $this->slotRoleStore = $slotRoleStore;
    }

    /**
     * @param array $fields Fields to select
     * @param array|null $revisionJoinConds If non-null, perform an INNER JOIN
     * against the revision table on these join conditions.
     * @param EntityId[] $entityIds EntityIds to select
     * @param IReadableDatabase $db DB to query on
     * @return stdClass[] Array of rows with keys of their entity ID serializations
     */
    public function selectRows(
        array $fields,
        ?array $revisionJoinConds,
        array $entityIds,
        IReadableDatabase $db
    ): array {
        $queryBuilder = $db->newSelectQueryBuilder()
            ->select( $fields )
            ->select( $this->getFieldsNeededForMapping() )
            ->from( 'page' );
        if ( $revisionJoinConds !== null ) {
            $queryBuilder->join( 'revision', null, $revisionJoinConds );
        }
        $this->updateQueryBuilder( $queryBuilder, $entityIds, $db );

        $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet();

        return $this->indexRowsByEntityId( $res );
    }

    /**
     * @param SelectQueryBuilder $queryBuilder
     * @param EntityId[] $entityIds
     * @param IReadableDatabase $db
     */
    private function updateQueryBuilder( SelectQueryBuilder $queryBuilder, array $entityIds, IReadableDatabase $db ) {
        $where = [];
        $joinSlotsTable = false;

        foreach ( $entityIds as $entityId ) {
            $entityType = $entityId->getEntityType();
            $slotRole = $this->entityNamespaceLookup->getEntitySlotRole( $entityType );
            $namespace = $this->entityNamespaceLookup->getEntityNamespace( $entityType );

            if ( $namespace === null ) {
                throw new EntityLookupException(
                    $entityId,
                    "No namespace configured for entity type `$entityType`"
                );
            }

            $conditions = $this->getConditionForEntityId( $entityId );
            $conditions['page_namespace'] = $namespace;

            /**
             * Only check against the slot role when we are not using the main slot.
             * If we are using the main slot, then we only need to check that the page
             * exists rather than a specific slot within the page.
             * This ensures comparability with the pre MCR schema as long as only the
             * main slot is used.
             */
            if ( $slotRole !== SlotRecord::MAIN ) {
                try {
                    $slotRoleId = $this->slotRoleStore->getId( $slotRole );
                } catch ( NameTableAccessException $e ) {
                    // The slot role is not yet saved, nothing to retrieve.
                    continue;
                }

                $conditions['slot_role_id'] = $slotRoleId;
                $joinSlotsTable = true;
            }

            $where[] = $db->andExpr( $conditions );
        }

        if ( !$where ) {
            // If we skipped all entity IDs, select nothing, not everything.
            $queryBuilder->where( new RawSQLExpression( '0=1' ) );
            return;
        }

        if ( $joinSlotsTable ) {
            $usesRevisionTable = in_array( 'revision', $queryBuilder->getQueryInfo()['tables'], true );
            $slotJoinConds = $usesRevisionTable ? 'rev_id=slot_revision_id' : 'page_latest=slot_revision_id';
            $queryBuilder->join( 'slots', null, $slotJoinConds );
        }

        $queryBuilder->where( $db->orExpr( $where ) );
    }

    /**
     * @param Traversable $rows
     * @return stdClass[] Array of rows with keys of their entity ID serializations
     */
    private function indexRowsByEntityId( Traversable $rows ): array {
        $indexedRows = [];

        foreach ( $rows as $row ) {
            $indexedRows[$this->getEntityIdStringFromRow( $row )] = $row;
        }

        return $indexedRows;
    }

    /**
     * @param EntityId $entityId
     * @return array SQL condition
     */
    abstract protected function getConditionForEntityId( EntityId $entityId );

    /**
     * @param stdClass $row
     * @return string serialization of an entity ID for example Q123
     */
    abstract protected function getEntityIdStringFromRow( stdClass $row );

    /**
     * @return string[] Extra fields needed for the mapping done in mapRowsToEntityIds
     */
    abstract protected function getFieldsNeededForMapping();

}