Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.23% covered (success)
90.23%
194 / 215
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZObjectSecondaryDataUpdate
90.23% covered (success)
90.23%
194 / 215
71.43% covered (warning)
71.43%
5 / 7
49.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 doUpdate
82.95% covered (warning)
82.95%
73 / 88
0.00% covered (danger)
0.00%
0 / 1
18.43
 getRelatedZObjects
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getRelatedZObjectsOfFunctionCall
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 getRelatedZObjectsOfFunction
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
1 / 1
13
 getRelatedZObjectsOfType
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
5
 getRelatedZObjectsOfInstance
76.00% covered (warning)
76.00%
19 / 25
0.00% covered (danger)
0.00%
0 / 1
5.35
1<?php
2/**
3 * WikiLambda ZObject secondary data updater for when ZObjects are edited
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\WikiLambda;
12
13use MediaWiki\Deferred\DataUpdate;
14use MediaWiki\Extension\WikiLambda\Cache\MemcachedWrapper;
15use MediaWiki\Extension\WikiLambda\Registry\ZLangRegistry;
16use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
17use MediaWiki\Extension\WikiLambda\ZObjects\ZFunction;
18use MediaWiki\Extension\WikiLambda\ZObjects\ZFunctionCall;
19use MediaWiki\Extension\WikiLambda\ZObjects\ZObject;
20use MediaWiki\Extension\WikiLambda\ZObjects\ZReference;
21use MediaWiki\Extension\WikiLambda\ZObjects\ZType;
22use MediaWiki\Extension\WikiLambda\ZObjects\ZTypedList;
23use MediaWiki\Logger\LoggerFactory;
24use MediaWiki\Title\Title;
25use Psr\Log\LoggerInterface;
26
27class ZObjectSecondaryDataUpdate extends DataUpdate {
28
29    private Title $title;
30    private ZObjectContent $zObject;
31    private ?OrchestratorRequest $orchestrator;
32    private LoggerInterface $logger;
33
34    public const INSTANCEOFENUM_DB_KEY = 'instanceofenum';
35
36    /**
37     * @param Title $title
38     * @param ZObjectContent $zObject
39     * @param ZObjectStore $zObjectStore
40     * @param MemcachedWrapper $zObjectCache
41     * @param OrchestratorRequest|null $orchestrator
42     */
43    public function __construct(
44        Title $title,
45        ZObjectContent $zObject,
46        private readonly ZObjectStore $zObjectStore,
47        private readonly MemcachedWrapper $zObjectCache,
48        ?OrchestratorRequest $orchestrator = null
49    ) {
50        $this->title = $title;
51        $this->zObject = $zObject;
52        $this->orchestrator = $orchestrator;
53        $this->logger = LoggerFactory::getInstance( 'WikiLambda' );
54    }
55
56    public function doUpdate() {
57        // Given this title, gets ZID
58        // Given this zObject, gets ZType
59        // 1. Delete labels from wikilambda_zobject_labels for this ZID
60        // 2. Delete labels from wikilambda_zobject_label_conflicts for this ZID
61        // 3. Gets labels from this zObject (Z2K3 of the ZObjectContent)
62        // 4. Finds conflicting labels, e.g. existing labels from other ZIDs that have same language-value
63        // 5. Saves conflicting labels in wikilambda_zobject_label_conflicts and
64        // 6. Saves non-conflicting labels in wikilambda_zobject_labels
65        // 7. If appropriate, clear wikilambda_ztester_results for this ZID
66        // 8. If appropriate, add entry to wikilambda_zlanguages for this ZID
67        // 9. Add related zobjects, if any, to wikilambda_zobject_join for this ZID
68
69        // TODO (T300522): Only re-write the labels if they've changed.
70        // TODO (T300522): Use a single fancy upsert to remove/update/insert instead?
71
72        $zid = $this->title->getDBkey();
73
74        // (T380446) If the object is not valid there's nothing useful to do, except log an error and exit.
75        if ( !$this->zObject->isValid() ) {
76            $zerror = $this->zObject->getErrors();
77            $this->logger->error(
78                'ZObjectSecondaryDataUpdate unable to process, error thrown',
79                [
80                    'zid' => $zid,
81                    'message' => $zerror->getMessage()
82                ]
83            );
84            return;
85        }
86
87        // Object is valid, we go on!
88
89        // Delete all labels: primary ones and aliases
90        $this->zObjectStore->deleteZObjectLabelsByZid( $zid );
91        $this->zObjectStore->deleteZObjectLabelConflictsByZid( $zid );
92
93        // Delete language entries, if appropriate
94        $this->zObjectStore->deleteZLanguageFromLanguagesCache( $zid );
95
96        $labels = $this->zObject->getLabels()->getValueAsList();
97
98        // TODO (T357552): This should write the shortform, encoded type (e.g. `Z881(Z6)`)
99        $ztype = $this->zObject->getZType();
100
101        // Store the ZObject in the object cache, for faster retrieval here and (in future) in the orchestrator
102        $cacheKey = $this->zObjectCache->makeKey( ZObjectStore::ZOBJECT_CACHE_KEY_PREFIX, $zid );
103        $this->logger->debug(
104            __METHOD__ . ' writing new ZObject value to cache "' . $zid . '": type "' . $ztype . '".',
105            [ 'instance' => $zid, 'type' => $ztype ]
106        );
107        $cacheResult = $this->zObjectCache->set(
108            $cacheKey,
109            $this->zObject->getText(),
110            $this->zObjectCache::TTL_MONTH
111        );
112        if ( !$cacheResult ) {
113            $this->logger->warning( __METHOD__ . ' failed to cache new ZObject "' . $zid . '".', [ 'zid' => $zid ] );
114        }
115        if ( $this->orchestrator ) {
116            $queryZ2 = $this->zObject->getObject();
117            $this->orchestrator->persistToCache( $queryZ2 );
118        }
119
120        $innerZObject = $this->zObject->getInnerZObject();
121
122        $returnType = null;
123        // (T262089) Save output type in labels table for function and function call
124        // Get Z_FUNCTION_RETURN_TYPE if the ZObject is a Z8 Function
125        if ( $ztype === ZTypeRegistry::Z_FUNCTION ) {
126            $returnRef = $innerZObject->getValueByKey( ZTypeRegistry::Z_FUNCTION_RETURN_TYPE );
127            // Fallback, save output type as Object/Z1 to avoid NULL returning functions
128            $returnType = ZTypeRegistry::Z_OBJECT;
129            if ( ( $returnRef instanceof ZReference ) || ( $returnRef instanceof ZFunctionCall ) ) {
130                // ZReference->getZValue returns the reference Zid
131                // ZFunctionCall->getZValue returns the function call function Zid
132                $returnType = $returnRef->getZValue();
133            }
134        }
135
136        $conflicts = $this->zObjectStore->findZObjectLabelConflicts( $zid, $ztype, $labels );
137        $newLabels = array_filter( $labels, static function ( $value, $lang ) use ( $conflicts ) {
138            return !isset( $conflicts[$lang] );
139        }, ARRAY_FILTER_USE_BOTH );
140
141        $this->zObjectStore->insertZObjectLabels( $zid, $ztype, $newLabels, $returnType );
142        $this->zObjectStore->insertZObjectLabelConflicts( $zid, $conflicts );
143
144        // (T285368) Write aliases in the labels table
145        $aliases = $this->zObject->getAliases()->getValueAsList();
146        // (T358737) Add the zid as fake aliases under Z1360/MUL (multi-lingual value)
147        $aliases[ ZLangRegistry::MULTILINGUAL_VALUE ] = [ $zid ];
148        if ( count( $aliases ) > 0 ) {
149            $this->zObjectStore->insertZObjectAliases( $zid, $ztype, $aliases, $returnType );
150        }
151
152        // ========================================================
153        // General delete actions:
154        // ========================================================
155        // * Delete old function reference from wikilambda_zobject_function_join table
156        // * Delete related ZObjects from wikilambda_zobject_join table
157        $this->zObjectStore->deleteZFunctionReference( $zid );
158        $this->zObjectStore->deleteRelatedZObjects( $zid );
159
160        // ========================================================
161        // Type specific actions:
162        // ========================================================
163        // * Function:
164        //   - clear test results cache
165        // * Implementation:
166        //   - delete old function→implementation relations
167        //   - clear test results cache
168        //   - insert new function→implementation relation
169        // * Tester:
170        //   - delete old function→tester relations
171        //   - clear test results cache
172        //   - insert new function→tester relation
173        // * Type:
174        //   - remove all instanceofenum from wikilambda_zobject_join table
175        // * Language:
176        //   - remove old language codes from wikilambda_zlanguage
177        //   - add new language codes
178        //   - add as fake aliases under Z1360/MUL (multi-lingual value)
179        switch ( $ztype ) {
180            case ZTypeRegistry::Z_FUNCTION:
181                // TODO (T338247): Only clear test results cache for the old revision, not the new one
182                $this->zObjectStore->deleteZFunctionFromZTesterResultsCache( $zid );
183                break;
184
185            case ZTypeRegistry::Z_IMPLEMENTATION:
186                $zFunction = $innerZObject->getValueByKey( ZTypeRegistry::Z_IMPLEMENTATION_FUNCTION );
187                // TODO (T338247): Only clear test results cache for the old revision, not the new one
188                $this->zObjectStore->deleteZImplementationFromZTesterResultsCache( $zid );
189                // TODO (T362248): Have insertZFunctionReference do an update,
190                // and only delete if changing the type/target?
191                $this->zObjectStore->insertZFunctionReference( $zid, $zFunction->getZValue(), $ztype );
192                break;
193
194            case ZTypeRegistry::Z_TESTER:
195                $zFunction = $innerZObject->getValueByKey( ZTypeRegistry::Z_TESTER_FUNCTION );
196                // TODO (T338247): Only clear test results cache for the old revision, not the new one
197                $this->zObjectStore->deleteZTesterFromZTesterResultsCache( $zid );
198                // TODO (T362248): Have insertZFunctionReference do an update,
199                // and only delete if changing the type/target?
200                $this->zObjectStore->insertZFunctionReference( $zid, $zFunction->getZValue(), $ztype );
201                break;
202
203            case ZTypeRegistry::Z_TYPE:
204                // Remove all instanceofenum from wikilambda_zobject_join table
205                $this->zObjectStore->deleteRelatedZObjects( null, $zid, self::INSTANCEOFENUM_DB_KEY );
206                break;
207
208            case ZTypeRegistry::Z_LANGUAGE:
209                // Clear old values, if any
210                $this->zObjectStore->deleteZLanguageFromLanguagesCache( $zid );
211
212                // Set primary language code
213                $targetLanguage = $innerZObject->getValueByKey( ZTypeRegistry::Z_LANGUAGE_CODE )->getZValue();
214                $languageCodes = [ $targetLanguage ];
215                $this->zObjectStore->insertZLanguageToLanguagesCache( $zid, $targetLanguage );
216
217                // Set secondary language codes, if any
218                $secondaryLanguagesObject = $innerZObject->getValueByKey( ZTypeRegistry::Z_LANGUAGE_SECONDARYCODES );
219                if ( $secondaryLanguagesObject !== null ) {
220                    '@phan-var ZTypedList $secondaryLanguagesObject';
221                    $secondaryLanguages = $secondaryLanguagesObject->getAsArray();
222
223                    foreach ( $secondaryLanguages as $key => $secondaryLanguage ) {
224                        // $secondaryLanguage is a ZString but we want the actual string
225                        $secondaryLanguageString = $secondaryLanguage->getZValue();
226                        $languageCodes[] = $secondaryLanguageString;
227                        $this->zObjectStore->insertZLanguageToLanguagesCache( $zid, $secondaryLanguageString );
228                    }
229                }
230
231                // (T343465) Add the language codes as fake aliases under Z1360/MUL (multi-lingual value)
232                $this->zObjectStore->insertZObjectAliases(
233                    $zid,
234                    $ztype,
235                    [ ZLangRegistry::MULTILINGUAL_VALUE => $languageCodes ],
236                    $returnType
237                );
238                break;
239
240            default:
241                // No action.
242        }
243
244        // ========================================================
245        // General insert actions:
246        // ========================================================
247        // * Add related ZObjects to wikilambda_zobject_join table
248        $relatedZObjects = $this->getRelatedZObjects( $zid, $ztype, $innerZObject );
249        if ( count( $relatedZObjects ) > 0 ) {
250            $this->zObjectStore->insertRelatedZObjects( $relatedZObjects );
251        }
252    }
253
254    /**
255     * Return all important relations between the given object and others
256     * to insert in the wikilambda_zobject_join table. The relations are
257     * given by the key. Currently finds relations for:
258     * * Functions/Z8
259     * * Types/Z4
260     * * Instances of enum types
261     *
262     * @param string $zid
263     * @param string $ztype
264     * @param ZObject $innerZObject
265     * @return array Array of rows to insert in the join table
266     */
267    private function getRelatedZObjects( $zid, $ztype, $innerZObject ) {
268        if ( $innerZObject instanceof ZFunction ) {
269            return $this->getRelatedZObjectsOfFunction( $zid, $innerZObject );
270        }
271
272        if ( $innerZObject instanceof ZType ) {
273            return $this->getRelatedZObjectsOfType( $zid, $innerZObject );
274        }
275
276        if ( $innerZObject instanceof ZFunctionCall ) {
277            return $this->getRelatedZObjectsOfFunctionCall( $zid, $innerZObject );
278        }
279
280        return $this->getRelatedZObjectsOfInstance( $zid, $ztype );
281    }
282
283    /**
284     * Return all important relations between the given function and others.
285     * Currently returns:
286     * * key:Z7K1: Function
287     *
288     * @param string $zid
289     * @param ZFunctionCall $innerZObject
290     * @return array Array of rows to insert in the join table
291     */
292    private function getRelatedZObjectsOfFunctionCall( $zid, $innerZObject ) {
293        $relatedZObjects = [];
294
295        // Key:Z7K1: Get function zid
296        $functionZid = $innerZObject->getZValue();
297        if ( $functionZid ) {
298            $relatedZObjects[] = (object)[
299                'zid' => $zid,
300                'type' => ZTypeRegistry::Z_FUNCTIONCALL,
301                'key' => ZTypeRegistry::Z_FUNCTIONCALL_FUNCTION,
302                'related_zid' => $functionZid,
303                'related_type' => ZTypeRegistry::Z_FUNCTION
304            ];
305        }
306
307        return $relatedZObjects;
308    }
309
310    /**
311     * Return all important relations between the given function and others.
312     * Currently returns:
313     * * key:Z8K1: Type of every function input
314     * * key:Z8K2: Type of the function output
315     * * key:Z8K3: Test connected to the function
316     * * key:Z8K4: Implementation connected to the function
317     *
318     * @param string $zid
319     * @param ZFunction $innerZObject
320     * @return array Array of rows to insert in the join table
321     */
322    private function getRelatedZObjectsOfFunction( $zid, $innerZObject ) {
323        $relatedZObjects = [];
324
325        // Key:Z8K1: Get input types
326        $inputs = $innerZObject->getValueByKey( ZTypeRegistry::Z_FUNCTION_ARGUMENTS );
327        if ( $inputs instanceof ZTypedList ) {
328            $inputList = $inputs->getAsArray();
329            foreach ( $inputList as $key => $input ) {
330                $inputTypeObject = $input->getValueByKey( ZTypeRegistry::Z_ARGUMENTDECLARATION_TYPE );
331                $inputTypeString = ZObjectUtils::makeTypeFingerprint( $inputTypeObject->getSerialized() );
332                if ( $inputTypeString !== null ) {
333                    $relatedZObjects[] = (object)[
334                        'zid' => $zid,
335                        'type' => ZTypeRegistry::Z_FUNCTION,
336                        'key' => ZTypeRegistry::Z_FUNCTION_ARGUMENTS,
337                        'related_zid' => $inputTypeString,
338                        'related_type' => ZTypeRegistry::Z_TYPE
339                    ];
340                }
341            }
342        }
343
344        // Key:Z8K2: Get output type
345        $outputType = $innerZObject->getValueByKey( ZTypeRegistry::Z_FUNCTION_RETURN_TYPE );
346        $outputTypeString = ZObjectUtils::makeTypeFingerprint( $outputType->getSerialized() );
347        if ( $outputTypeString !== null ) {
348            $relatedZObjects[] = (object)[
349                'zid' => $zid,
350                'type' => ZTypeRegistry::Z_FUNCTION,
351                'key' => ZTypeRegistry::Z_FUNCTION_RETURN_TYPE,
352                'related_zid' => $outputTypeString,
353                'related_type' => ZTypeRegistry::Z_TYPE
354            ];
355        }
356
357        // Key:Z8K3: Get tests
358        $tests = $innerZObject->getValueByKey( ZTypeRegistry::Z_FUNCTION_TESTERS );
359        if ( $tests instanceof ZTypedList ) {
360            $testList = $tests->getAsArray();
361            foreach ( $testList as $key => $test ) {
362                if ( $test instanceof ZReference && $test->isValid() ) {
363                    $relatedZObjects[] = (object)[
364                        'zid' => $zid,
365                        'type' => ZTypeRegistry::Z_FUNCTION,
366                        'key' => ZTypeRegistry::Z_FUNCTION_TESTERS,
367                        'related_zid' => $test->getZValue(),
368                        'related_type' => ZTypeRegistry::Z_TESTER
369                    ];
370                }
371            }
372        }
373
374        // Key:Z8K4: Get implementations
375        $implementations = $innerZObject->getValueByKey( ZTypeRegistry::Z_FUNCTION_IMPLEMENTATIONS );
376        if ( $implementations instanceof ZTypedList ) {
377            $implementationList = $implementations->getAsArray();
378            foreach ( $implementationList as $key => $implementation ) {
379                if ( $implementation instanceof ZReference && $implementation->isValid() ) {
380                    $relatedZObjects[] = (object)[
381                        'zid' => $zid,
382                        'type' => ZTypeRegistry::Z_FUNCTION,
383                        'key' => ZTypeRegistry::Z_FUNCTION_IMPLEMENTATIONS,
384                        'related_zid' => $implementation->getZValue(),
385                        'related_type' => ZTypeRegistry::Z_IMPLEMENTATION
386                    ];
387                }
388            }
389        }
390
391        return $relatedZObjects;
392    }
393
394    /**
395     * Return all important relations between the given type and others.
396     * Currently returns:
397     * * key:Z4K5: Renderer function for the type
398     * * key:Z4K6: Parser function for the type
399     *
400     * @param string $zid
401     * @param ZType $innerZObject
402     * @return array Array of rows to insert in the join table
403     */
404    private function getRelatedZObjectsOfType( $zid, $innerZObject ) {
405        $relatedZObjects = [];
406
407        // Key:Z4K5: Get renderer function
408        $rendererFunction = $innerZObject->getRendererFunction();
409        if ( $rendererFunction ) {
410            $relatedZObjects[] = (object)[
411                'zid' => $zid,
412                'type' => ZTypeRegistry::Z_TYPE,
413                'key' => ZTypeRegistry::Z_TYPE_RENDERER,
414                'related_zid' => $rendererFunction,
415                'related_type' => ZTypeRegistry::Z_FUNCTION
416            ];
417        }
418
419        // Key:Z4K6: Get parser function
420        $parserFunction = $innerZObject->getParserFunction();
421        if ( $parserFunction ) {
422            $relatedZObjects[] = (object)[
423                'zid' => $zid,
424                'type' => ZTypeRegistry::Z_TYPE,
425                'key' => ZTypeRegistry::Z_TYPE_PARSER,
426                'related_zid' => $parserFunction,
427                'related_type' => ZTypeRegistry::Z_FUNCTION
428            ];
429        }
430
431        // Key:instanceofenum: Get all instances of enum type
432        if ( $innerZObject->isEnumType() ) {
433            // Gather all instances of this type,
434            // add them into the table
435            $instances = $this->zObjectStore->fetchZidsOfType( $zid );
436            foreach ( $instances as $instance ) {
437                $relatedZObjects[] = (object)[
438                    'zid' => $instance,
439                    'type' => $zid,
440                    'key' => self::INSTANCEOFENUM_DB_KEY,
441                    'related_zid' => $zid,
442                    'related_type' => ZTypeRegistry::Z_TYPE
443                ];
444            }
445        }
446
447        return $relatedZObjects;
448    }
449
450    /**
451     * Return all important relations between arbitrary objects.
452     * Currently returns:
453     * * key:instanceofenum: if the given object is an instance of an enum type.
454     *
455     * @param string $zid
456     * @param string $type
457     * @return array Array of rows to insert in the join table
458     */
459    private function getRelatedZObjectsOfInstance( $zid, $type ) {
460        $relatedZObjects = [];
461
462        $typeTitle = Title::newFromText( $type, NS_MAIN );
463        $typeContent = $this->zObjectStore->fetchZObjectByTitle( $typeTitle );
464        if ( !$typeContent ) {
465            // Error: type is not found, nothing we can do except log
466            $this->logger->warning(
467                __METHOD__ . ' failed to update relations for "' . $zid . '": type "' . $type . '" not found',
468                [ 'instance' => $zid, 'type' => $type ]
469            );
470            return [];
471        }
472
473        try {
474            $typeObject = $typeContent->getInnerZObject();
475        } catch ( ZErrorException $e ) {
476            // Error: type is not valid, nothing we can do except log
477            $this->logger->warning(
478                __METHOD__ . ' failed to update relations for "' . $zid . '": type "' . $type . '" not valid',
479                [ 'instance' => $zid, 'type' => $type, 'responseError' => $e ]
480            );
481            return [];
482        }
483
484        // Key:instanceofenum: If object is an instance of an Enum type
485        if ( $typeObject instanceof ZType && $typeObject->isEnumType() ) {
486            $relatedZObjects[] = (object)[
487                'zid' => $zid,
488                'type' => $type,
489                'key' => self::INSTANCEOFENUM_DB_KEY,
490                'related_zid' => $type,
491                'related_type' => ZTypeRegistry::Z_TYPE
492            ];
493        }
494
495        return $relatedZObjects;
496    }
497}