Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.31% covered (warning)
88.31%
68 / 77
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateConstraintsTableJob
88.31% covered (warning)
88.31%
68 / 77
66.67% covered (warning)
66.67%
4 / 6
12.23
0.00% covered (danger)
0.00%
0 / 1
 newFromGlobalState
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 extractParametersFromQualifiers
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 extractConstraintFromStatement
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 importConstraintsForProperty
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
5.63
 run
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php
2
3namespace WikibaseQuality\ConstraintReport\Job;
4
5use Job;
6use JobQueueGroup;
7use MediaWiki\Config\Config;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Title\Title;
10use Serializers\Serializer;
11use Wikibase\DataModel\Entity\NumericPropertyId;
12use Wikibase\DataModel\Entity\Property;
13use Wikibase\DataModel\Snak\SnakList;
14use Wikibase\DataModel\Statement\Statement;
15use Wikibase\Lib\Store\EntityRevisionLookup;
16use Wikibase\Lib\Store\LookupConstants;
17use Wikibase\Repo\Store\Store;
18use Wikibase\Repo\WikibaseRepo;
19use WikibaseQuality\ConstraintReport\Constraint;
20use WikibaseQuality\ConstraintReport\ConstraintsServices;
21use WikibaseQuality\ConstraintReport\ConstraintStore;
22use Wikimedia\Assert\Assert;
23use Wikimedia\Rdbms\ILBFactory;
24
25/**
26 * A job that updates the constraints table
27 * when changes were made on a property.
28 *
29 * @author Lucas Werkmeister
30 * @license GPL-2.0-or-later
31 */
32class UpdateConstraintsTableJob extends Job {
33
34    /**
35     * How many constraints to write in one transaction before waiting for replication.
36     * Properties with more constraints than this will not be updated atomically
37     * (they will appear to have an incomplete set of constraints for a time).
38     */
39    private const BATCH_SIZE = 50;
40
41    public static function newFromGlobalState( Title $title, array $params ) {
42        Assert::parameterType( 'string', $params['propertyId'], '$params["propertyId"]' );
43        $services = MediaWikiServices::getInstance();
44        return new UpdateConstraintsTableJob(
45            $title,
46            $params,
47            $params['propertyId'],
48            $params['revisionId'] ?? null,
49            $services->getMainConfig(),
50            ConstraintsServices::getConstraintStore(),
51            $services->getDBLoadBalancerFactory(),
52            WikibaseRepo::getStore()->getEntityRevisionLookup( Store::LOOKUP_CACHING_DISABLED ),
53            WikibaseRepo::getBaseDataModelSerializerFactory( $services )
54                ->newSnakSerializer(),
55            $services->getJobQueueGroup()
56        );
57    }
58
59    /**
60     * @var string
61     */
62    private $propertyId;
63
64    /**
65     * @var int|null
66     */
67    private $revisionId;
68
69    /**
70     * @var Config
71     */
72    private $config;
73
74    /**
75     * @var ConstraintStore
76     */
77    private $constraintStore;
78
79    /** @var ILBFactory */
80    private $lbFactory;
81
82    /**
83     * @var EntityRevisionLookup
84     */
85    private $entityRevisionLookup;
86
87    /**
88     * @var Serializer
89     */
90    private $snakSerializer;
91
92    /**
93     * @var JobQueueGroup
94     */
95    private $jobQueueGroup;
96
97    /**
98     * @param Title $title
99     * @param string[] $params should contain 'propertyId' => 'P...'
100     * @param string $propertyId property ID of the property for this job (which has the constraint statements)
101     * @param int|null $revisionId revision ID that triggered this job, if any
102     * @param Config $config
103     * @param ConstraintStore $constraintStore
104     * @param ILBFactory $lbFactory
105     * @param EntityRevisionLookup $entityRevisionLookup
106     * @param Serializer $snakSerializer
107     * @param JobQueueGroup $jobQueueGroup
108     */
109    public function __construct(
110        Title $title,
111        array $params,
112        $propertyId,
113        $revisionId,
114        Config $config,
115        ConstraintStore $constraintStore,
116        ILBFactory $lbFactory,
117        EntityRevisionLookup $entityRevisionLookup,
118        Serializer $snakSerializer,
119        JobQueueGroup $jobQueueGroup
120    ) {
121        parent::__construct( 'constraintsTableUpdate', $title, $params );
122
123        $this->propertyId = $propertyId;
124        $this->revisionId = $revisionId;
125        $this->config = $config;
126        $this->constraintStore = $constraintStore;
127        $this->lbFactory = $lbFactory;
128        $this->entityRevisionLookup = $entityRevisionLookup;
129        $this->snakSerializer = $snakSerializer;
130        $this->jobQueueGroup = $jobQueueGroup;
131    }
132
133    public function extractParametersFromQualifiers( SnakList $qualifiers ) {
134        $parameters = [];
135        foreach ( $qualifiers as $qualifier ) {
136            $qualifierId = $qualifier->getPropertyId()->getSerialization();
137            $paramSerialization = $this->snakSerializer->serialize( $qualifier );
138            $parameters[$qualifierId][] = $paramSerialization;
139        }
140        return $parameters;
141    }
142
143    public function extractConstraintFromStatement(
144        NumericPropertyId $propertyId,
145        Statement $constraintStatement
146    ) {
147        $constraintId = $constraintStatement->getGuid();
148        '@phan-var string $constraintId'; // we know the statement has a non-null GUID
149        $snak = $constraintStatement->getMainSnak();
150        '@phan-var \Wikibase\DataModel\Snak\PropertyValueSnak $snak';
151        $dataValue = $snak->getDataValue();
152        '@phan-var \Wikibase\DataModel\Entity\EntityIdValue $dataValue';
153        $entityId = $dataValue->getEntityId();
154        $constraintTypeQid = $entityId->getSerialization();
155        $parameters = $this->extractParametersFromQualifiers( $constraintStatement->getQualifiers() );
156        return new Constraint(
157            $constraintId,
158            $propertyId,
159            $constraintTypeQid,
160            $parameters
161        );
162    }
163
164    public function importConstraintsForProperty(
165        Property $property,
166        ConstraintStore $constraintStore,
167        NumericPropertyId $propertyConstraintPropertyId
168    ) {
169        $constraintsStatements = $property->getStatements()
170            ->getByPropertyId( $propertyConstraintPropertyId )
171            ->getByRank( [ Statement::RANK_PREFERRED, Statement::RANK_NORMAL ] );
172        $constraints = [];
173        foreach ( $constraintsStatements->getIterator() as $constraintStatement ) {
174            // @phan-suppress-next-line PhanTypeMismatchArgumentSuperType
175            $constraints[] = $this->extractConstraintFromStatement( $property->getId(), $constraintStatement );
176            if ( count( $constraints ) >= self::BATCH_SIZE ) {
177                $constraintStore->insertBatch( $constraints );
178                // interrupt transaction and wait for replication
179                $connection = $this->lbFactory->getMainLB()->getConnection( DB_PRIMARY );
180                $connection->endAtomic( __CLASS__ );
181                if ( !$connection->explicitTrxActive() ) {
182                    $this->lbFactory->waitForReplication();
183                }
184                $connection->startAtomic( __CLASS__ );
185                $constraints = [];
186            }
187        }
188        $constraintStore->insertBatch( $constraints );
189    }
190
191    /**
192     * @see Job::run
193     *
194     * @return bool
195     */
196    public function run() {
197        // TODO in the future: only touch constraints affected by the edit (requires T163465)
198
199        $propertyId = new NumericPropertyId( $this->propertyId );
200        $propertyRevision = $this->entityRevisionLookup->getEntityRevision(
201            $propertyId,
202            0, // latest
203            LookupConstants::LATEST_FROM_REPLICA
204        );
205
206        if ( $this->revisionId !== null && $propertyRevision->getRevisionId() < $this->revisionId ) {
207            $this->jobQueueGroup->push( $this );
208            return true;
209        }
210
211        $connection = $this->lbFactory->getMainLB()->getConnection( DB_PRIMARY );
212        // start transaction (if not started yet) – using __CLASS__, not __METHOD__,
213        // because importConstraintsForProperty() can interrupt the transaction
214        $connection->startAtomic( __CLASS__ );
215
216        $this->constraintStore->deleteForProperty( $propertyId );
217
218        /** @var Property $property */
219        $property = $propertyRevision->getEntity();
220        '@phan-var Property $property';
221        $this->importConstraintsForProperty(
222            $property,
223            $this->constraintStore,
224            new NumericPropertyId( $this->config->get( 'WBQualityConstraintsPropertyConstraintId' ) )
225        );
226
227        $connection->endAtomic( __CLASS__ );
228
229        return true;
230    }
231
232}