Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
88.31% |
68 / 77 |
|
66.67% |
4 / 6 |
CRAP | |
0.00% |
0 / 1 |
UpdateConstraintsTableJob | |
88.31% |
68 / 77 |
|
66.67% |
4 / 6 |
12.23 | |
0.00% |
0 / 1 |
newFromGlobalState | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
extractParametersFromQualifiers | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
extractConstraintFromStatement | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
importConstraintsForProperty | |
53.33% |
8 / 15 |
|
0.00% |
0 / 1 |
5.63 | |||
run | |
90.00% |
18 / 20 |
|
0.00% |
0 / 1 |
3.01 |
1 | <?php |
2 | |
3 | namespace WikibaseQuality\ConstraintReport\Job; |
4 | |
5 | use Job; |
6 | use JobQueueGroup; |
7 | use MediaWiki\Config\Config; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MediaWiki\Title\Title; |
10 | use Serializers\Serializer; |
11 | use Wikibase\DataModel\Entity\NumericPropertyId; |
12 | use Wikibase\DataModel\Entity\Property; |
13 | use Wikibase\DataModel\Snak\SnakList; |
14 | use Wikibase\DataModel\Statement\Statement; |
15 | use Wikibase\Lib\Store\EntityRevisionLookup; |
16 | use Wikibase\Lib\Store\LookupConstants; |
17 | use Wikibase\Repo\Store\Store; |
18 | use Wikibase\Repo\WikibaseRepo; |
19 | use WikibaseQuality\ConstraintReport\Constraint; |
20 | use WikibaseQuality\ConstraintReport\ConstraintsServices; |
21 | use WikibaseQuality\ConstraintReport\ConstraintStore; |
22 | use Wikimedia\Assert\Assert; |
23 | use 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 | */ |
32 | class 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 | } |