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