wikimedia/mediawiki-extensions-Wikibase

View on GitHub
repo/includes/Store/Sql/PropertyInfoTableBuilder.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace Wikibase\Repo\Store\Sql;

use Onoi\MessageReporter\MessageReporter;
use RuntimeException;
use Wikibase\DataModel\Entity\NumericPropertyId;
use Wikibase\DataModel\Entity\Property;
use Wikibase\DataModel\Services\Lookup\PropertyLookup;
use Wikibase\Lib\Store\EntityNamespaceLookup;
use Wikibase\Lib\Store\Sql\PropertyInfoTable;
use Wikibase\Repo\PropertyInfoBuilder;

/**
 * Utility class for rebuilding the wb_property_info table.
 *
 * @license GPL-2.0-or-later
 * @author Daniel Kinzler
 */
class PropertyInfoTableBuilder {

    /**
     * @var PropertyInfoTable
     */
    private $propertyInfoTable;

    /**
     * @var PropertyLookup
     */
    private $propertyLookup;

    /**
     * @var PropertyInfoBuilder
     */
    private $propertyInfoBuilder;

    /**
     * @var EntityNamespaceLookup
     */
    private $entityNamespaceLookup;

    /**
     * @var MessageReporter|null
     */
    private $reporter = null;

    /**
     * Whether all entries should be updated, or only missing entries
     *
     * @var bool
     */
    private $shouldUpdateAllEntities = false;

    /**
     * The batch size, giving the number of rows to be updated in each database transaction.
     *
     * @var int
     */
    private $batchSize = 100;

    public function __construct(
        PropertyInfoTable $propertyInfoTable,
        PropertyLookup $propertyLookup,
        PropertyInfoBuilder $propertyInfoBuilder,
        EntityNamespaceLookup $entityNamespaceLookup
    ) {
        $this->propertyInfoTable = $propertyInfoTable;
        $this->propertyLookup = $propertyLookup;
        $this->propertyInfoBuilder = $propertyInfoBuilder;
        $this->entityNamespaceLookup = $entityNamespaceLookup;
    }

    /**
     * @param bool $all
     */
    public function setRebuildAll( $all ) {
        $this->shouldUpdateAllEntities = $all;
    }

    /**
     * @param int $batchSize
     */
    public function setBatchSize( $batchSize ) {
        $this->batchSize = $batchSize;
    }

    /**
     * Sets the reporter to use for reporting progress.
     *
     * @param MessageReporter $reporter
     */
    public function setReporter( MessageReporter $reporter ) {
        $this->reporter = $reporter;
    }

    /**
     * Rebuild the property info entries.
     * Use the rebuildPropertyInfo.php maintenance script to invoke this from the command line.
     *
     * Database updates a batched into multiple transactions. Do not call this
     * method within an (explicit) database transaction.
     */
    public function rebuildPropertyInfo(): int {
        $propertyNamespace = $this->entityNamespaceLookup->getEntityNamespace(
            Property::ENTITY_TYPE
        );
        if ( $propertyNamespace === null ) {
            throw new RuntimeException( __METHOD__ . ' can not run with no Property namespace defined.' );
        }

        $dbw = $this->propertyInfoTable->getDomainDb()->connections()->getWriteConnection();

        $total = 0;

        $queryBuilderTemplate = $dbw->newSelectQueryBuilder();
        $queryBuilderTemplate->select( [ 'page_title', 'page_id' ] )
            ->from( 'page' );

        if ( !$this->shouldUpdateAllEntities ) {
            $queryBuilderTemplate->leftJoin(
                $this->propertyInfoTable->getTableName(),
                null,
                $dbw->buildConcat( [ $dbw->addQuotes( 'P' ), 'pi_property_id' ] ) . ' = page_title'
            );
            $queryBuilderTemplate->where( [ 'pi_property_id' => null ] ); // only add missing entries
        }

        $queryBuilderTemplate->where( [ 'page_namespace' => $propertyNamespace ] )
            ->orderBy( 'page_id', $queryBuilderTemplate::SORT_ASC )
            ->limit( $this->batchSize )
            ->forUpdate()
            ->caller( __METHOD__ );

        $ticket = $this->propertyInfoTable->getDomainDb()->getEmptyTransactionTicket( __METHOD__ );
        $pageId = 1;

        while ( true ) {
            // Make sure we are not running too far ahead of the replicas,
            // as that would cause the site to be rendered read only.
            $this->propertyInfoTable->getDomainDb()->commitAndWaitForReplication( __METHOD__, $ticket );

            $dbw->startAtomic( __METHOD__ );

            $queryBuilder = clone $queryBuilderTemplate;
            $queryBuilder->where( $dbw->expr( 'page_id', '>', $pageId ) );
            $props = $queryBuilder->fetchResultSet();

            $c = 0;

            foreach ( $props as $row ) {
                $this->updatePropertyInfo( new NumericPropertyId( $row->page_title ) );
                $pageId = (int)$row->page_id;
                $c++;
            }

            $dbw->endAtomic( __METHOD__ );

            $this->reportMessage( "Updated $c properties, up to page ID $pageId." );
            $total += $c;

            if ( $c < $this->batchSize ) {
                break;
            }
        }

        return $total;
    }

    /**
     * Updates the property info entry for the given property.
     * The property is loaded in full using the EntityLookup
     * provide to the constructor.
     *
     * @throws RuntimeException
     *
     * @param NumericPropertyId $id the Property to process
     */
    private function updatePropertyInfo( NumericPropertyId $id ) {
        $property = $this->propertyLookup->getPropertyForId( $id );

        if ( $property === null ) {
            throw new RuntimeException(
                'Did not find Property with id ' . $id->getSerialization()
            );
        }

        $info = $this->propertyInfoBuilder->buildPropertyInfo( $property );

        $this->propertyInfoTable->setPropertyInfo(
            // @phan-suppress-next-line PhanTypeMismatchArgumentSuperType
            $property->getId(),
            $info
        );
    }

    /**
     * @param string $msg
     */
    private function reportMessage( $msg ) {
        if ( $this->reporter ) {
            $this->reporter->reportMessage( $msg );
        }
    }

}