Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.25% covered (success)
92.25%
512 / 555
77.50% covered (warning)
77.50%
31 / 40
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZObjectStore
92.25% covered (success)
92.25%
512 / 555
77.50% covered (warning)
77.50%
31 / 40
103.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getNextAvailableZid
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 fetchZObjectByTitle
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 fetchBatchZObjects
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
3
 createNewZObject
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 updateZObject
72.41% covered (warning)
72.41%
42 / 58
0.00% covered (danger)
0.00%
0 / 1
15.02
 updateZObjectAsSystemUser
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 deleteZObjectLabelsByZid
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 deleteZObjectLabelConflictsByZid
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 findZObjectLabelConflicts
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
4
 insertZObjectLabels
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
3
 insertZLanguageToLanguagesCache
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 insertZObjectLabelConflicts
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 insertZObjectAliases
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
 fetchZidsOfType
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 fetchAllZids
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 fetchAllZLanguageObjects
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 fetchAllZLanguageCodes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 searchZObjectLabels
88.10% covered (warning)
88.10%
37 / 42
0.00% covered (danger)
0.00%
0 / 1
10.17
 fetchZObjectLabel
97.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
8
 fetchZFunctionReturnType
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 findFirstZImplementationFunction
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 findReferencedZObjectsByZFunctionIdAsList
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 findReferencedZObjectsByZFunctionId
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
2.00
 insertZFunctionReference
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 deleteZFunctionReference
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 findZTesterResult
84.00% covered (warning)
84.00%
42 / 50
0.00% covered (danger)
0.00%
0 / 1
10.41
 insertZTesterResult
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
1
 deleteZFunctionFromZTesterResultsCache
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 deleteZImplementationFromZTesterResultsCache
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 deleteZTesterFromZTesterResultsCache
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 deleteZLanguageFromLanguagesCache
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 deleteFromLabelsSecondaryTables
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 deleteFromFunctionsSecondaryTables
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 deleteFromLanguageCacheSecondaryTables
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 deleteFromTesterResultsSecondaryTables
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 clearLabelsSecondaryTables
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 clearFunctionsSecondaryTables
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 clearTesterResultsSecondaryTables
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 clearLanguageCacheSecondaryTables
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * WikiLambda Data Access Object service
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\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
14use MediaWiki\Extension\WikiLambda\Registry\ZLangRegistry;
15use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
16use MediaWiki\Extension\WikiLambda\ZObjects\ZPersistentObject;
17use MediaWiki\Extension\WikiLambda\ZObjects\ZResponseEnvelope;
18use MediaWiki\Languages\LanguageFallback;
19use MediaWiki\MediaWikiServices;
20use MediaWiki\Page\WikiPageFactory;
21use MediaWiki\Revision\RevisionRecord;
22use MediaWiki\Revision\RevisionStore;
23use MediaWiki\Revision\SlotRecord;
24use MediaWiki\Title\Title;
25use MediaWiki\Title\TitleArrayFromResult;
26use MediaWiki\Title\TitleFactory;
27use MediaWiki\User\User;
28use MediaWiki\User\UserGroupManager;
29use Psr\Log\LoggerInterface;
30use Wikimedia\Rdbms\IConnectionProvider;
31use Wikimedia\Rdbms\IDatabase;
32use Wikimedia\Rdbms\IResultWrapper;
33use Wikimedia\Rdbms\SelectQueryBuilder;
34use WikiPage;
35
36class ZObjectStore {
37
38    private IConnectionProvider $dbProvider;
39    private TitleFactory $titleFactory;
40    private WikiPageFactory $wikiPageFactory;
41    private RevisionStore $revisionStore;
42    private UserGroupManager $userGroupManager;
43    private LoggerInterface $logger;
44
45    /**
46     * @param IConnectionProvider $dbProvider
47     * @param TitleFactory $titleFactory
48     * @param WikiPageFactory $wikiPageFactory
49     * @param RevisionStore $revisionStore
50     * @param UserGroupManager $userGroupManager
51     * @param LoggerInterface $logger
52     */
53    public function __construct(
54        IConnectionProvider $dbProvider,
55        TitleFactory $titleFactory,
56        WikiPageFactory $wikiPageFactory,
57        RevisionStore $revisionStore,
58        UserGroupManager $userGroupManager,
59        LoggerInterface $logger
60    ) {
61        $this->dbProvider = $dbProvider;
62        $this->titleFactory = $titleFactory;
63        $this->wikiPageFactory = $wikiPageFactory;
64        $this->revisionStore = $revisionStore;
65        $this->userGroupManager = $userGroupManager;
66        $this->logger = $logger;
67    }
68
69    /**
70     * Find next available ZID in the database to create a new ZObject
71     *
72     * @return string Next available ZID
73     */
74    public function getNextAvailableZid(): string {
75        // Intentionally use DB_PRIMARY as we need the latest data here.
76        $dbr = $this->dbProvider->getPrimaryDatabase();
77        $res = $dbr->newSelectQueryBuilder()
78            ->select( [ 'page_title' ] )
79            ->from( 'page' )
80            ->where( [
81                'page_namespace' => NS_MAIN,
82                'LENGTH( page_title ) > 5'
83            ] )
84            ->orderBy( 'page_id', SelectQueryBuilder::SORT_DESC )
85            ->groupBy( [ 'page_id', 'page_title' ] )
86            ->limit( 1 )
87            ->caller( __METHOD__ )
88            ->fetchResultSet();
89
90        // NOTE: This hard-codes user-provided content as starting from 'Z10000'. Now that the
91        // wiki has launched, change it will have unpredicatable effects.
92        $highestZid = $res->numRows() > 0 ? $res->fetchRow()[ 0 ] : 'Z9999';
93        $targetZid = 'Z' . ( max( intval( substr( $highestZid, 1 ) ) + 1, 10000 ) );
94
95        return $targetZid;
96    }
97
98    /**
99     * Fetch the ZObject given its title and return it wrapped in a ZObjectContent object
100     *
101     * @param Title $title The ZObject to fetch
102     * @param int|null $requestedRevision The revision ID of the page to fetch. If unset, the latest is returned.
103     * @return ZObjectContent|bool Found ZObject
104     */
105    public function fetchZObjectByTitle( Title $title, ?int $requestedRevision = null ) {
106        if ( $requestedRevision ) {
107            $revision = $this->revisionStore->getRevisionByTitle( $title, $requestedRevision, 0 );
108        } else {
109            $revision = $this->revisionStore->getKnownCurrentRevision( $title );
110        }
111
112        if ( !$revision ) {
113            // TODO (T300521): Handle errors by creating and returning Z5
114            return false;
115        }
116
117        // NOTE: Hard-coding use of MAIN slot; if we're going the MCR route, we may wish to change this (or not).
118        $slot = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
119        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
120        return $slot->getContent();
121    }
122
123    /**
124     * Returns an array of ZPersistentObjects fetched from the DB given an array of their Zids
125     *
126     * Note that this will only fetch the latest revision of a ZObject; if you want a specific
127     * revision, you need to use ZObjectStore::fetchZObjectByTitle() instead.
128     *
129     * @param string[] $zids
130     * @return ZPersistentObject[]
131     */
132    public function fetchBatchZObjects( $zids ) {
133        $dbr = $this->dbProvider->getReplicaDatabase();
134        $query = WikiPage::getQueryInfo();
135
136        $res = $dbr->newSelectQueryBuilder()
137            ->select( $query['fields'] )
138            ->from( 'page' )
139            ->where( [
140                'page_namespace' => NS_MAIN,
141                'page_title' => $zids
142            ] )
143            ->caller( __METHOD__ )
144            ->fetchResultSet();
145
146        $titleArray = new TitleArrayFromResult( $res );
147
148        $dataArray = [];
149        foreach ( $titleArray as $title ) {
150            // TODO (T300521): Handle error from fetchZObjectByTitle
151            $content = $this->fetchZObjectByTitle( $title );
152            if ( $content->isValid() ) {
153                $dataArray[ $title->getBaseText() ] = $content->getZObject();
154            }
155        }
156        return $dataArray;
157    }
158
159    /**
160     * Create a new ZObject, with a newly assigned ZID, and store it in the Database
161     *
162     * @param string $data
163     * @param string $summary
164     * @param User $user
165     * @return ZObjectPage
166     */
167    public function createNewZObject( string $data, string $summary, User $user ) {
168        // Find all placeholder ZIDs and ZKeys and replace those with the next available ZID
169        $zid = $this->getNextAvailableZid();
170        $zPlaceholderRegex = '/\"' . ZTypeRegistry::Z_NULL_REFERENCE . '(K[1-9]\d*)?\"/';
171        $zObjectString = preg_replace( $zPlaceholderRegex, "\"$zid$1\"", $data );
172        return $this->updateZObject( $zid, $zObjectString, $summary, $user, EDIT_NEW );
173    }
174
175    /**
176     * Create or update a ZObject it in the Database
177     *
178     * @param string $zid The ZID of the page to create/update, e.g. 'Z12345'
179     * @param string $data The ZObject's JSON to store, in string form, i.e. "{ Z1K1: "Z2", Z2K1: … }"
180     * @param string $summary An edit summary to display in the page's history, Recent Changes, watchlists, etc.
181     * @param User $user The user making the edit.
182     * @param int $flags Either EDIT_UPDATE (default) if editing or EDIT_NEW if creating a page
183     * @return ZObjectPage
184     */
185    public function updateZObject( string $zid, string $data, string $summary, User $user, int $flags = EDIT_UPDATE ) {
186        $title = $this->titleFactory->newFromText( $zid, NS_MAIN );
187
188        // ERROR: Title is empty or invalid
189        if ( !( $title instanceof Title ) ) {
190            $error = ZErrorFactory::createZErrorInstance(
191                ZErrorTypeRegistry::Z_ERROR_INVALID_TITLE,
192                [ 'title' => $zid ]
193            );
194            return ZObjectPage::newFatal( $error );
195        }
196
197        // If edit flag or title did not exist, we are creating a new object
198        $creating = ( $flags === EDIT_NEW ) || !( $title->exists() );
199
200        try {
201            $content = ZObjectContentHandler::makeContent( $data, $title );
202        } catch ( ZErrorException $e ) {
203            return ZObjectPage::newFatal( $e->getZError() );
204        }
205
206        // Error: ZObject validation errors.
207        if ( !( $content->isValid() ) ) {
208            return ZObjectPage::newFatal( $content->getErrors() );
209        }
210
211        // Validate that $zid and zObject[Z2K1] are the same
212        $zObjectId = $content->getZid();
213        if ( $zObjectId !== $zid ) {
214            $error = ZErrorFactory::createZErrorInstance(
215                ZErrorTypeRegistry::Z_ERROR_UNMATCHING_ZID,
216                [
217                    'zid' => $zObjectId,
218                    'title' => $zid
219                ]
220            );
221            return ZObjectPage::newFatal( $error );
222        }
223
224        $ztype = $content->getZType();
225
226        // Stop from creating and editing any types form DISALLOWED_ROOT_ZOBJECT
227        if ( in_array( $ztype, ZTypeRegistry::DISALLOWED_ROOT_ZOBJECTS ) ) {
228            $error = ZErrorFactory::createZErrorInstance(
229                ZErrorTypeRegistry::Z_ERROR_DISALLOWED_ROOT_ZOBJECT,
230                [ 'data' => $content->getZType() ]
231            );
232            return ZObjectPage::newFatal( $error );
233        }
234
235        // Find the label conflicts.
236        $labels = $content->getLabels()->getValueAsList();
237        $clashes = $this->findZObjectLabelConflicts( $zid, $ztype, $labels );
238
239        if ( count( $clashes ) > 0 ) {
240            $error = ZErrorFactory::createLabelClashZErrors( $clashes );
241            return ZObjectPage::newFatal( $error );
242        }
243
244        // Use ZObjectAuthorization service to check that the user has the required permissions
245        // while creating or editing an object
246        $fromContent = null;
247        if ( !$creating ) {
248            $currentRevision = $this->revisionStore->getKnownCurrentRevision( $title );
249            $fromContent = $currentRevision->getSlots()->getContent( SlotRecord::MAIN );
250            '@phan-var ZObjectContent $fromContent';
251        }
252        $authorizationService = WikiLambdaServices::getZObjectAuthorization();
253        $status = $authorizationService->authorize( $fromContent, $content, $user, $title );
254
255        // Return AuthorizationStatus->error if authorization service failed
256        if ( !$status->isValid() ) {
257            return ZObjectPage::newFatal( $status->getErrors() );
258        }
259
260        // We prepare the content to be saved
261        $page = $this->wikiPageFactory->newFromTitle( $title );
262        try {
263            $status = $page->doUserEditContent( $content, $user, $summary, $flags );
264        } catch ( \Exception $e ) {
265            // Error: Database or a deeper MediaWiki error, e.g. a general editing rate limit
266            $error = ZErrorFactory::createZErrorInstance(
267                ZErrorTypeRegistry::Z_ERROR_UNKNOWN,
268                // TODO (T362236): Add the rendering language as a parameter, don't default to English
269                [ 'message' => $e->getMessage() ]
270            );
271            return ZObjectPage::newFatal( $error );
272        }
273
274        if ( !$status->isOK() ) {
275            // Error: Other doUserEditContent related errors
276            $error = ZErrorFactory::createZErrorInstance(
277                ZErrorTypeRegistry::Z_ERROR_UNKNOWN,
278                [ 'message' => $status->getMessage() ]
279            );
280            return ZObjectPage::newFatal( $error );
281        }
282
283        // Success: return WikiPage
284        return ZObjectPage::newSuccess( $page );
285    }
286
287    /**
288     * Create or update a ZObject it in the Database as a System User
289     *
290     * @param string $zid
291     * @param string $data
292     * @param string $summary
293     * @param int $flags
294     * @return ZObjectPage
295     */
296    public function updateZObjectAsSystemUser( string $zid, string $data, string $summary, int $flags = EDIT_UPDATE ) {
297        $creatingUserName = wfMessage( 'wikilambda-systemuser' )->inLanguage( 'en' )->text();
298        // System user must belong to all privileged groups in order to
299        // perform all zobject creation and editing actions:
300        $user = User::newSystemUser( $creatingUserName, [ 'steal' => true ] );
301        $this->userGroupManager->addUserToGroup( $user, 'sysop' );
302        $this->userGroupManager->addUserToGroup( $user, 'functionmaintainer' );
303        $this->userGroupManager->addUserToGroup( $user, 'functioneer' );
304        $this->userGroupManager->addUserToGroup( $user, 'wikifunctions-staff' );
305        return $this->updateZObject( $zid, $data, $summary, $user, $flags );
306    }
307
308    /**
309     * Delete the labels from the wikilambda_zobject_labels database that correspond
310     * to the given ZID.
311     *
312     * @param string $zid
313     */
314    public function deleteZObjectLabelsByZid( string $zid ) {
315        $dbw = $this->dbProvider->getPrimaryDatabase();
316
317        $dbw->newDeleteQueryBuilder()
318            ->deleteFrom( 'wikilambda_zobject_labels' )
319            ->where( [ 'wlzl_zobject_zid' => $zid ] )
320            ->caller( __METHOD__ )->execute();
321    }
322
323    /**
324     * Delete the label conflicts from the wikilambda_zobject_label_conflicts database
325     * that correspond to the given ZID.
326     *
327     * @param string $zid
328     */
329    public function deleteZObjectLabelConflictsByZid( string $zid ) {
330        $dbw = $this->dbProvider->getPrimaryDatabase();
331
332        $dbw->newDeleteQueryBuilder()
333            ->deleteFrom( 'wikilambda_zobject_label_conflicts' )
334            ->where( $dbw->makeList(
335                    [
336                        'wlzlc_existing_zid' => $zid,
337                        'wlzlc_conflicting_zid' => $zid
338                    ],
339                    $dbw::LIST_OR
340                )
341            )
342            ->caller( __METHOD__ )->execute();
343    }
344
345    /**
346     * Query the wikilambda_zobject_labels database for primary labels that have
347     * the same combination of language code and value for a different ZID
348     * than the given in the parameters. These will be considered conflicting labels.
349     *
350     * @param string $zid
351     * @param string $ztype
352     * @param array $labels Array of labels, where the key is the language code and the value
353     * is the string representation of the label in that language
354     * @return array Conflicts found in the wikilambda_zobject_labels database
355     */
356    public function findZObjectLabelConflicts( $zid, $ztype, $labels ): array {
357        $dbr = $this->dbProvider->getReplicaDatabase();
358
359        // remove labels with an undefined value
360        $labels = array_filter(
361            $labels, static function ( $value ) {
362                return $value !== "";
363            } );
364
365        if ( $labels === [] ) {
366            return [];
367        }
368
369        $labelConflictConditions = [];
370        foreach ( $labels as $language => $value ) {
371            $labelConflictConditions[] = $dbr->makeList( [
372                'wlzl_language' => $language,
373                'wlzl_label' => $value,
374                'wlzl_label_primary' => true
375            ], $dbr::LIST_AND );
376        }
377
378        $res = $dbr->newSelectQueryBuilder()
379            ->select( [ 'wlzl_zobject_zid', 'wlzl_language' ] )
380            ->from( 'wikilambda_zobject_labels' )
381            ->where( [
382                'wlzl_zobject_zid != ' . $dbr->addQuotes( $zid ),
383                // TODO (T357552): Check against type, once we properly implement that.
384                // 'wlzl_type' => $dbr->addQuotes( $ztype ),
385                $dbr->makeList( $labelConflictConditions, $dbr::LIST_OR )
386            ] )
387            ->caller( __METHOD__ )
388            ->fetchResultSet();
389
390        $conflicts = [];
391        foreach ( $res as $row ) {
392            // TODO (T362247): What if more than one conflicts with us on each language?
393            $conflicts[ $row->wlzl_language ] = $row->wlzl_zobject_zid;
394        }
395
396        return $conflicts;
397    }
398
399    /**
400     * Insert labels into the wikilambda_zobject_labels database for a given ZID and Type
401     *
402     * @param string $zid
403     * @param string $ztype
404     * @param array $labels Array of labels, where the key is the language code and the value
405     * is the string representation of the label in that language
406     * @param string|null $returnType
407     * @return void|bool
408     */
409    public function insertZObjectLabels( $zid, $ztype, $labels, $returnType = null ) {
410        $dbw = $this->dbProvider->getPrimaryDatabase();
411
412        $updates = [];
413        foreach ( $labels as $language => $value ) {
414            $updates[] = [
415                'wlzl_zobject_zid' => $zid,
416                'wlzl_language' => $language,
417                'wlzl_type' => $ztype,
418                'wlzl_label' => $value,
419                'wlzl_label_normalised' => ZObjectUtils::comparableString( $value ),
420                'wlzl_label_primary' => true,
421                'wlzl_return_type' => $returnType
422            ];
423        }
424
425        // Exit early if there are no updates to make.
426        if ( count( $updates ) === 0 ) {
427            return;
428        }
429
430        $dbw->newInsertQueryBuilder()
431            ->insertInto( 'wikilambda_zobject_labels' )
432            ->rows( $updates )
433            ->caller( __METHOD__ )->execute();
434    }
435
436    /**
437     * Insert language code into the wikilambda_zlanguages database for a given ZID
438     *
439     * @param string $zid
440     * @param string $languageCode
441     * @return void|bool
442     */
443    public function insertZLanguageToLanguagesCache( string $zid, string $languageCode ) {
444        $dbw = $this->dbProvider->getPrimaryDatabase();
445
446        $dbw->newInsertQueryBuilder()
447            ->insertInto( 'wikilambda_zlanguages' )
448            ->row( [ 'wlzlangs_zid' => $zid, 'wlzlangs_language' => $languageCode ] )
449            ->caller( __METHOD__ )->execute();
450    }
451
452    /**
453     * Insert label conflicts into the wikilambda_zobject_label_conflicts database for a given ZID
454     *
455     * @param string $zid
456     * @param array $conflicts Array of labels, where the key is the language code and the value
457     * is the other ZID for which this label is repeated
458     * @return void|bool
459     */
460    public function insertZObjectLabelConflicts( $zid, $conflicts ) {
461        $dbw = $this->dbProvider->getPrimaryDatabase();
462
463        $updates = [];
464        foreach ( $conflicts as $language => $existingZid ) {
465            $updates[] = [
466                'wlzlc_existing_zid' => $existingZid,
467                'wlzlc_conflicting_zid' => $zid,
468                'wlzlc_language' => $language,
469            ];
470        }
471
472        // Exit early if there are no updates to make.
473        if ( count( $updates ) === 0 ) {
474            return;
475        }
476
477        $dbw->newInsertQueryBuilder()
478            ->insertInto( 'wikilambda_zobject_label_conflicts' )
479            ->rows( $updates )
480            ->caller( __METHOD__ )->execute();
481    }
482
483    /**
484     * Insert alias (secondary labels) into the wikilambda_zobject_labels database for a given ZID and Type
485     *
486     * @param string $zid
487     * @param string $ztype
488     * @param array $aliases Set of labels, where the key is the language code
489     * and the value is an array of strings
490     * @param string|null $returnType
491     * @return void|bool
492     */
493    public function insertZObjectAliases( $zid, $ztype, $aliases, $returnType = null ) {
494        $dbw = $this->dbProvider->getPrimaryDatabase();
495
496        $updates = [];
497        foreach ( $aliases as $language => $stringset ) {
498            foreach ( $stringset as $value ) {
499                $updates[] = [
500                    'wlzl_zobject_zid' => $zid,
501                    'wlzl_language' => $language,
502                    'wlzl_type' => $ztype,
503                    'wlzl_label' => $value,
504                    'wlzl_label_normalised' => ZObjectUtils::comparableString( $value ),
505                    'wlzl_label_primary' => false,
506                    'wlzl_return_type' => $returnType
507                ];
508            }
509        }
510
511        if ( count( $updates ) === 0 ) {
512            return;
513        }
514
515        $dbw->newInsertQueryBuilder()
516            ->insertInto( 'wikilambda_zobject_labels' )
517            ->rows( $updates )
518            ->caller( __METHOD__ )->execute();
519    }
520
521    /**
522     * Gets from the secondary database a list of all Zids belonging to a given type
523     *
524     * @param string $ztype
525     * @return string[]
526     */
527    public function fetchZidsOfType( $ztype ) {
528        $dbr = $this->dbProvider->getReplicaDatabase();
529        $res = $dbr->newSelectQueryBuilder()
530            ->select( [ 'wlzl_zobject_zid' ] )
531            ->from( 'wikilambda_zobject_labels' )
532            ->where( [
533                'wlzl_type' => $ztype
534            ] )
535            ->orderBy( 'wlzl_zobject_zid', SelectQueryBuilder::SORT_ASC )
536            ->caller( __METHOD__ )
537            ->fetchResultSet();
538
539        $zids = [];
540        foreach ( $res as $row ) {
541            $zids[] = $row->wlzl_zobject_zid;
542        }
543        return $zids;
544    }
545
546    /**
547     * Get a list of all Zids persisted in the database
548     *
549     * @return string[] All persisted Zids
550     */
551    public function fetchAllZids() {
552        $dbr = $this->dbProvider->getReplicaDatabase();
553        $res = $dbr->newSelectQueryBuilder()
554            ->select( [ 'page_title' ] )
555            ->from( 'page' )
556            ->where( [
557                'page_namespace' => NS_MAIN
558            ] )
559            ->caller( __METHOD__ )
560            ->fetchResultSet();
561
562        $zids = [];
563        foreach ( $res as $row ) {
564            $zid = $row->page_title;
565            if ( ZObjectUtils::isValidZObjectReference( $zid ) ) {
566                $zids[] = $zid;
567            }
568        }
569        return $zids;
570    }
571
572    /**
573     * Gets from the secondary database a list of all natural language ZIDs,
574     * mapping from ZID to BCP47 (or MediaWiki) language code
575     *
576     * @return array<string,string>
577     */
578    public function fetchAllZLanguageObjects() {
579        $dbr = $this->dbProvider->getReplicaDatabase();
580        $res = $dbr->newSelectQueryBuilder()
581            ->select( [ 'wlzlangs_zid', 'wlzlangs_language' ] )
582            ->from( 'wikilambda_zlanguages' )
583            ->orderBy( 'wlzlangs_zid', SelectQueryBuilder::SORT_ASC )
584            ->caller( __METHOD__ )
585            ->fetchResultSet();
586
587        $languages = [];
588        foreach ( $res as $row ) {
589            $languages[ $row->wlzlangs_zid ] = $row->wlzlangs_language;
590        }
591        return $languages;
592    }
593
594    /**
595     * Gets from the secondary database a list of all supported natural languages,
596     * mapping from ZID to BCP47 (or MediaWiki) language code
597     *
598     * @return array<string>
599     */
600    public function fetchAllZLanguageCodes() {
601        return array_values( $this->fetchAllZLanguageObjects() );
602    }
603
604    /**
605     * Search labels in the secondary database, filtering by language Zids, type or label string.
606     *
607     * @param string $label Term to search in the label database
608     * @param bool $exact Whether to search by exact match
609     * @param string[] $languages List of language Zids to filter by
610     * @param string|null $type Zid of the type to filter by. If null, don't filter by type.
611     * @param string|null $returnType Zid of the return type to filter by. If null, don't filter by return type.
612     * @param bool $strictReturnType Whether to exclude Z1s as return type.
613     * @param string|null $continue Id to start. If null, start from the first result.
614     * @param int|null $limit Maximum number of results to return.
615     * @return IResultWrapper
616     */
617    public function searchZObjectLabels(
618        $label,
619        $exact,
620        $languages,
621        $type,
622        $returnType,
623        $strictReturnType,
624        $continue,
625        $limit
626    ) {
627        $dbr = $this->dbProvider->getReplicaDatabase();
628
629        // Set language filter if any
630        if ( count( $languages ) > 0 ) {
631            $conditions = [ 'wlzl_language' => $languages ];
632        }
633
634        // Set type filter
635        $typeConditions = [];
636        if ( $type != null ) {
637            $typeConditions[] = 'wlzl_type = ' . $dbr->addQuotes( $type );
638        }
639
640        // Set returntype filter
641        if ( $returnType != null ) {
642            $typeConditions[] = 'wlzl_return_type = ' . $dbr->addQuotes( $returnType );
643            if ( !$strictReturnType ) {
644                $typeConditions[] = 'wlzl_return_type = ' . $dbr->addQuotes( ZTypeRegistry::Z_OBJECT );
645            }
646        }
647
648        // Set type conditions
649        if ( count( $typeConditions ) > 0 ) {
650            $conditions[] = $dbr->makeList( $typeConditions, $dbr::LIST_OR );
651        }
652
653        // Set minimum id bound if we are continuing a paged result
654        if ( $continue != null ) {
655            $conditions[] = "wlzl_id >= $continue";
656        }
657
658        // Set search Term and column
659        if ( ZObjectUtils::isValidZObjectReference( $label ) ) {
660            $searchedColumn = 'wlzl_zobject_zid';
661            $searchTerm = $label;
662        } elseif ( $exact ) {
663            $searchedColumn = 'wlzl_label';
664            $searchTerm = $label;
665        } else {
666            $searchedColumn = 'wlzl_label_normalised';
667            $searchTerm = ZObjectUtils::comparableString( $label );
668        }
669
670        $conditions[] = $searchedColumn . $dbr->buildLike( $dbr->anyString(), $searchTerm, $dbr->anyString() );
671
672        // Create query builder
673        $queryBuilder = $dbr->newSelectQueryBuilder()
674            ->select( [
675                'wlzl_zobject_zid',
676                'wlzl_type',
677                'wlzl_return_type',
678                'wlzl_language',
679                'wlzl_label',
680                'wlzl_label_primary',
681                'wlzl_id'
682            ] )
683            ->from( 'wikilambda_zobject_labels' )
684            ->where( $conditions )
685            ->orderBy( 'wlzl_id', SelectQueryBuilder::SORT_ASC )
686            ->orderBy( 'wlzl_label_primary', SelectQueryBuilder::SORT_DESC );
687
688        // Set limit if not null
689        if ( $limit ) {
690            $queryBuilder->limit( $limit );
691        }
692
693        return $queryBuilder
694            ->caller( __METHOD__ )
695            ->fetchResultSet();
696    }
697
698    /**
699     * Fetch the label in the secondary database for a given object by Zid, in a given language.
700     * Returns null if no labels are found in the given language or any other language in that
701     * language's fallback chain, including English (Z1002). If the language code given is not
702     * recognised, this will fall back to returning the English label, if available.
703     *
704     * @param string $zid Term to search in the label database
705     * @param string $languageCode Code of the language in which to fetch the label
706     * @param bool $fallback Whether to only match in the given language, or use
707     *   the language fallback change (default behaviour).
708     * @return string|null
709     */
710    public function fetchZObjectLabel( $zid, $languageCode, $fallback = true ) {
711        $dbr = $this->dbProvider->getReplicaDatabase();
712
713        $conditions = [    'wlzl_zobject_zid' => $zid ];
714
715        $zLangRegistry = ZLangRegistry::singleton();
716
717        // Provided language code is not known, so fall back to English.
718        $languageCode = $zLangRegistry->isLanguageKnownGivenCode( $languageCode ) ? $languageCode : 'en';
719        $languageZid = $zLangRegistry->getLanguageZidFromCode( $languageCode );
720
721        // Set language filter
722        $languages = [ $languageZid ];
723        if ( $fallback ) {
724            // TODO (T362246): Dependency-inject
725            $fallbackLanguages = MediaWikiServices::getInstance()->getLanguageFallback()->getAll(
726                $languageCode,
727                // Note: We intentionally fall all the way back to English as this will likely be the most
728                // common entry language, and so doing this will result in a marginally better UX behaviour
729                // for the circumstance where no label exists.
730                LanguageFallback::MESSAGES
731            );
732
733            foreach ( $fallbackLanguages as $index => $languageCode ) {
734                $languages[] = $zLangRegistry->getLanguageZidFromCode( $languageCode );
735            }
736        }
737        $conditions[ 'wlzl_language' ] = $languages;
738
739        // We only want primary labels, not aliases
740        $conditions[ 'wlzl_label_primary' ] = '1';
741
742        $res = $dbr->newSelectQueryBuilder()
743            ->select( [
744                'wlzl_zobject_zid',
745                'wlzl_language',
746                'wlzl_label'
747            ] )
748            ->from( 'wikilambda_zobject_labels' )
749            ->where( $conditions )
750            ->orderBy( 'wlzl_id', SelectQueryBuilder::SORT_ASC )
751            // Hard-coded performance limit just in case there's a very long / circular language fallback chain.
752            ->limit( 5 )
753            ->caller( __METHOD__ )
754            ->fetchResultSet();
755
756        // No hits at all; allow callers to give a fallback message or trigger a DB fetch if they want.
757        if ( $res->numRows() === 0 ) {
758            return null;
759        }
760
761        // Collapse labels into a simple array
762        $labels = [];
763        foreach ( $res as $row ) {
764            $labels[$row->wlzl_language] = $row->wlzl_label;
765        }
766
767        // Walk the labels in order of the language chain, so that language preference is preserved
768        foreach ( $languages as $index => $languageZid ) {
769            if ( array_key_exists( $languageZid, $labels ) ) {
770                return $labels[ $languageZid ];
771            }
772        }
773
774        // Somehow we've reached this point without a hit? Oh well.
775        return null;
776    }
777
778    /**
779     * Get the return type of a given Function Zid or null if not available
780     *
781     * @param string $zid
782     * @return string|null
783     */
784    public function fetchZFunctionReturnType( $zid ) {
785        $dbr = $this->dbProvider->getReplicaDatabase();
786        $res = $dbr->newSelectQueryBuilder()
787            ->select( [ 'wlzl_return_type' ] )
788            ->from( 'wikilambda_zobject_labels' )
789            ->where( [
790                'wlzl_zobject_zid' => $zid,
791                'wlzl_type' => ZTypeRegistry::Z_FUNCTION,
792            ] )
793            ->limit( 1 )
794            ->caller( __METHOD__ )
795            ->fetchField();
796
797        return $res ? (string)$res : null;
798    }
799
800    /**
801     * Search implementations in the secondary database, return the first one
802     * This function is primarily used for the example API request
803     *
804     * @return string
805     */
806    public function findFirstZImplementationFunction(): string {
807        $dbr = $this->dbProvider->getReplicaDatabase();
808        $res = $dbr->newSelectQueryBuilder()
809            ->select( [ 'wlzf_zfunction_zid' ] )
810            ->from( 'wikilambda_zobject_function_join' )
811            ->where( [
812                'wlzf_type' => ZTypeRegistry::Z_IMPLEMENTATION,
813            ] )
814            ->limit( 1 )
815            ->caller( __METHOD__ )
816            ->fetchField();
817
818        return $res ? (string)$res : '';
819    }
820
821    /**
822     * Converts findReferencedZObjectsByZFunctionId into a list of zids
823     *
824     * @param string $zid the ZID of the ZFunction
825     * @param string $type the type of the ZFunction reference
826     * @return string[] All ZIDs of referenced ZObjects associated to the ZFunction
827     */
828    public function findReferencedZObjectsByZFunctionIdAsList(
829        $zid,
830        $type
831    ) {
832        $res = $this->findReferencedZObjectsByZFunctionId( $zid, $type );
833        $zids = [];
834        foreach ( $res as $row ) {
835            $zids[] = $row->wlzf_ref_zid;
836        }
837
838        return $zids;
839    }
840
841    /**
842     * Search implementations in the secondary database and return all matching a given ZID
843     *
844     * @param string $zid the ZID of the ZFunction
845     * @param string $type the type of the ZFunction reference
846     * @param string|null $continue Id to start. If null (the default), start from the first result.
847     * @param int|null $limit Maximum number of results to return. Defaults to 10
848     * @return IResultWrapper
849     */
850    public function findReferencedZObjectsByZFunctionId(
851            $zid,
852            $type,
853            $continue = null,
854            $limit = 10
855        ) {
856        $dbr = $this->dbProvider->getReplicaDatabase();
857
858        $conditions = [
859            'wlzf_zfunction_zid' => $zid,
860            'wlzf_type' => $type
861        ];
862
863        // Set minimum id bound if we are continuing a paged result
864        if ( $continue != null ) {
865            $conditions[] = "wlzf_id >= $continue";
866        }
867        $res = $dbr->newSelectQueryBuilder()
868            ->select( [ 'wlzf_ref_zid', 'wlzf_id' ] )
869            ->from( 'wikilambda_zobject_function_join' )
870            ->where( $conditions )
871            ->orderBy( 'wlzf_id', SelectQueryBuilder::SORT_ASC )
872            ->limit( $limit )
873            ->caller( __METHOD__ )
874            ->fetchResultSet();
875
876        return $res;
877    }
878
879    /**
880     * Add a record to the database for a given ZObject ID and ZFunction ID
881     *
882     * @param string $refId the ZObject ref ID
883     * @param string $zFunctionId the ZFunction ID
884     * @param string $type the type of the ZFunction reference
885     * @return void|bool
886     */
887    public function insertZFunctionReference( $refId, $zFunctionId, $type ) {
888        $dbw = $this->dbProvider->getPrimaryDatabase();
889
890        $dbw->newInsertQueryBuilder()
891            ->insertInto( 'wikilambda_zobject_function_join' )
892            ->rows( [
893                [
894                    'wlzf_ref_zid' => $refId,
895                    'wlzf_zfunction_zid' => $zFunctionId,
896                    'wlzf_type' => $type
897                ]
898            ] )
899            ->caller( __METHOD__ )->execute();
900    }
901
902    /**
903     * Remove a given ZObject ref from the secondary database
904     *
905     * @param string $refId the ZObject ID
906     * @return void
907     */
908    public function deleteZFunctionReference( $refId ): void {
909        $dbw = $this->dbProvider->getPrimaryDatabase();
910
911        $dbw->newDeleteQueryBuilder()
912            ->deleteFrom( 'wikilambda_zobject_function_join' )
913            ->where( [ 'wlzf_ref_zid' => $refId ] )
914            ->caller( __METHOD__ )->execute();
915    }
916
917    /**
918     * Search test run results in the secondary tester results table; the latest result (highest
919     * database ID) will be used.
920     *
921     * @param ?string $functionZID The ZID of the ZFunction
922     * @param ?int $functionRevision The revision ID of the ZFunction
923     * @param ?string $implementationZID The ZID of the ZImplementation
924     * @param ?int $implementationRevision The revision ID of the ZImplementation
925     * @param ?string $testerZID The ZID of the ZTester
926     * @param ?int $testerRevision The revision ID of the ZTester
927     * @return ?ZResponseEnvelope
928     */
929    public function findZTesterResult(
930        ?string $functionZID,
931        ?int $functionRevision,
932        ?string $implementationZID,
933        ?int $implementationRevision,
934        ?string $testerZID,
935        ?int $testerRevision
936    ) {
937        $dbr = $this->dbProvider->getReplicaDatabase();
938
939        $purgeableResults = [];
940        $conditions = [];
941        if ( $functionZID !== null ) {
942            $conditions[] = "wlztr_zfunction_zid = " . $dbr->addQuotes( $functionZID );
943            $purgeableResults[] = $functionZID;
944        }
945        if ( $functionRevision !== null ) {
946            $conditions[] = "wlztr_zfunction_revision = " . $dbr->addQuotes( $functionRevision );
947        }
948        if ( $implementationZID !== null ) {
949            $conditions[] = "wlztr_zimplementation_zid = " . $dbr->addQuotes( $implementationZID );
950            $purgeableResults[] = $implementationZID;
951        }
952        if ( $implementationRevision !== null ) {
953            $conditions[] = "wlztr_zimplementation_revision = " . $dbr->addQuotes( $implementationRevision );
954        }
955        if ( $testerZID !== null ) {
956            $conditions[] = "wlztr_ztester_zid = " . $dbr->addQuotes( $testerZID );
957            $purgeableResults[] = $testerZID;
958        }
959        if ( $testerRevision !== null ) {
960            $conditions[] = "wlztr_ztester_revision = " . $dbr->addQuotes( $testerRevision );
961        }
962
963        $res = $dbr->newSelectQueryBuilder()
964            ->select( [
965                'wlztr_id',
966                'wlztr_zfunction_zid', 'wlztr_zfunction_revision',
967                'wlztr_zimplementation_zid', 'wlztr_zimplementation_revision',
968                'wlztr_ztester_zid', 'wlztr_ztester_revision',
969                'wlztr_pass',
970                'wlztr_returnobject'
971            ] )
972            ->from( 'wikilambda_ztester_results' )
973            ->where( $conditions )
974            ->orderBy( 'wlztr_id', SelectQueryBuilder::SORT_DESC )
975            ->limit( 1 )
976            ->caller( __METHOD__ )
977            ->fetchResultSet();
978
979        if ( $res->numRows() == 0 ) {
980            return null;
981        }
982
983        $result = $res->fetchRow();
984        try {
985            $responseObjectString = $result['wlztr_returnobject'];
986            $responseObject = ZObjectFactory::create( json_decode( $responseObjectString ) );
987            if ( $responseObject instanceof ZResponseEnvelope ) {
988                return $responseObject;
989            }
990
991            // Something's gone wrong, somehow
992            $this->logger->error(
993                __METHOD__ . ' retrieved a non-ZResponseEnvelope: ' . $responseObjectString,
994                [ 'responseObject' => $responseObjectString ]
995            );
996        } catch ( \Throwable $th ) {
997            // Something's gone differently wrong, somehow
998            $this->logger->error(
999                __METHOD__ . ' threw from ZObjectFactory',
1000                [ 'throwable' => $th ]
1001            );
1002        }
1003
1004        return null;
1005    }
1006
1007    /**
1008     * Cache a test run result in the secondary tester results table
1009     *
1010     * @param string $functionZID The ZID of the ZFunction
1011     * @param int $functionRevision The revision ID of the ZFunction
1012     * @param string $implementationZID The ZID of the ZImplementation
1013     * @param int $implementationRevision The revision ID of the ZImplementation
1014     * @param string $testerZID The ZID of the ZTester
1015     * @param int $testerRevision The revision ID of the ZTester
1016     * @param bool $testerResult Whether the test run passed (true) or failed (false)
1017     * @param string $testerResponse The test run response JSON object
1018     * @return bool
1019     */
1020    public function insertZTesterResult(
1021        string $functionZID,
1022        int $functionRevision,
1023        string $implementationZID,
1024        int $implementationRevision,
1025        string $testerZID,
1026        int $testerRevision,
1027        bool $testerResult,
1028        string $testerResponse
1029    ): bool {
1030        $dbw = $this->dbProvider->getPrimaryDatabase();
1031
1032        $dbw->newInsertQueryBuilder()
1033            ->insertInto( 'wikilambda_ztester_results' )
1034            ->row( [
1035                'wlztr_zfunction_zid' => $functionZID,
1036                'wlztr_zfunction_revision' => $functionRevision,
1037                'wlztr_zimplementation_zid' => $implementationZID,
1038                'wlztr_zimplementation_revision' => $implementationRevision,
1039                'wlztr_ztester_zid' => $testerZID,
1040                'wlztr_ztester_revision' => $testerRevision,
1041                'wlztr_pass' => $testerResult,
1042                'wlztr_returnobject' => $testerResponse,
1043            ] )
1044            ->onDuplicateKeyUpdate()
1045            ->uniqueIndexFields( [
1046                'wlztr_zfunction_revision',
1047                'wlztr_zimplementation_revision',
1048                'wlztr_ztester_revision'
1049            ] )
1050            ->set( [
1051                'wlztr_pass' => $testerResult,
1052                'wlztr_returnobject' => $testerResponse,
1053            ] )
1054            ->caller( __METHOD__ )->execute();
1055
1056        return (bool)$dbw->affectedRows();
1057    }
1058
1059    /**
1060     * Remove a given ZFunction's results from the secondary tester results table.
1061     *
1062     * @param string $refId the ZID of the ZFunction whose results you wish to delete
1063     * @return void
1064     */
1065    public function deleteZFunctionFromZTesterResultsCache( string $refId ): void {
1066        $dbw = $this->dbProvider->getPrimaryDatabase();
1067
1068        $dbw->newDeleteQueryBuilder()
1069            ->deleteFrom( 'wikilambda_ztester_results' )
1070            ->where( [ 'wlztr_zfunction_zid' => $refId ] )
1071            ->caller( __METHOD__ )->execute();
1072    }
1073
1074    /**
1075     * Remove a given ZImplementation's results from the secondary tester results table.
1076     *
1077     * @param string $refId the ZID of the ZImplementation whose results you wish to delete
1078     * @return void
1079     */
1080    public function deleteZImplementationFromZTesterResultsCache( string $refId ): void {
1081        $dbw = $this->dbProvider->getPrimaryDatabase();
1082
1083        $dbw->newDeleteQueryBuilder()
1084            ->deleteFrom( 'wikilambda_ztester_results' )
1085            ->where( [ 'wlztr_zimplementation_zid' => $refId ] )
1086            ->caller( __METHOD__ )->execute();
1087    }
1088
1089    /**
1090     * Remove a given ZTester's results from the secondary tester results table.
1091     *
1092     * @param string $refId the ZID of the ZTester whose results you wish to delete
1093     * @return void
1094     */
1095    public function deleteZTesterFromZTesterResultsCache( string $refId ): void {
1096        $dbw = $this->dbProvider->getPrimaryDatabase();
1097
1098        $dbw->newDeleteQueryBuilder()
1099            ->deleteFrom( 'wikilambda_ztester_results' )
1100            ->where( [ 'wlztr_ztester_zid' => $refId ] )
1101            ->caller( __METHOD__ )->execute();
1102    }
1103
1104    /**
1105     * Remove a given ZNaturalLanguage's entry from the secondary languages table.
1106     *
1107     * @param string $zid the ZID of the ZNaturalLanguage you wish to delete
1108     * @return void
1109     */
1110    public function deleteZLanguageFromLanguagesCache( string $zid ): void {
1111        $dbw = $this->dbProvider->getPrimaryDatabase();
1112
1113        $dbw->newDeleteQueryBuilder()
1114            ->deleteFrom( 'wikilambda_zlanguages' )
1115            ->where( [ 'wlzlangs_id' => $zid ] )
1116            ->caller( __METHOD__ )->execute();
1117    }
1118
1119    /**
1120     * Delete data related to the given zids from the secondary labels table.
1121     * This method is only for its use by reloadBuiltinData with the --force flag.
1122     *
1123     * @param string[] $zids list of zids to clear
1124     * @return void
1125     */
1126    public function deleteFromLabelsSecondaryTables( array $zids ): void {
1127        $dbw = $this->dbProvider->getPrimaryDatabase();
1128
1129        $dbw->newDeleteQueryBuilder()
1130            ->deleteFrom( 'wikilambda_zobject_labels' )
1131            ->where( [ 'wlzl_zobject_zid' => $zids ] )
1132            ->caller( __METHOD__ )->execute();
1133
1134        $dbw->newDeleteQueryBuilder()
1135            ->deleteFrom( 'wikilambda_zobject_label_conflicts' )
1136            ->where( [ 'wlzlc_existing_zid' => $zids ] )
1137            ->caller( __METHOD__ )->execute();
1138    }
1139
1140    /**
1141     * Delete data related to the given zids from the secondary function joins table.
1142     * This method is only for its use by reloadBuiltinData with the --force flag.
1143     *
1144     * @param string[] $zids list of zids to clear
1145     * @return void
1146     */
1147    public function deleteFromFunctionsSecondaryTables( array $zids ): void {
1148        $dbw = $this->dbProvider->getPrimaryDatabase();
1149
1150        $dbw->newDeleteQueryBuilder()
1151            ->deleteFrom( 'wikilambda_zobject_function_join' )
1152            ->where( $dbw->makeList(
1153                    [
1154                        'wlzf_zfunction_zid' => $zids,
1155                        'wlzf_ref_zid' => $zids
1156                    ],
1157                    $dbw::LIST_OR
1158                )
1159            )
1160            ->caller( __METHOD__ )->execute();
1161    }
1162
1163    /**
1164     * Delete data related to the given zids from the secondary languages table.
1165     * This method is only for its use by reloadBuiltinData with the --force flag.
1166     *
1167     * @param string[] $zids list of zids to clear
1168     * @return void
1169     */
1170    public function deleteFromLanguageCacheSecondaryTables( array $zids ): void {
1171        $dbw = $this->dbProvider->getPrimaryDatabase();
1172
1173        $dbw->newDeleteQueryBuilder()
1174            ->deleteFrom( 'wikilambda_zlanguages' )
1175            ->where( [ 'wlzlangs_zid' => $zids ] )
1176            ->caller( __METHOD__ )->execute();
1177    }
1178
1179    /**
1180     * Delete data related to the given zids from the secondary tester results table.
1181     * This method is only for its use by reloadBuiltinData with the --force flag.
1182     *
1183     * @param string[] $zids list of zids to clear
1184     * @return void
1185     */
1186    public function deleteFromTesterResultsSecondaryTables( array $zids ): void {
1187        $dbw = $this->dbProvider->getPrimaryDatabase();
1188
1189        $dbw->newDeleteQueryBuilder()
1190            ->deleteFrom( 'wikilambda_ztester_results' )
1191            ->where(
1192                $dbw->makeList(
1193                    [
1194                        'wlztr_zfunction_zid' => $zids,
1195                        'wlztr_zimplementation_zid' => $zids,
1196                        'wlztr_ztester_zid' => $zids
1197                    ],
1198                    $dbw::LIST_OR
1199                )
1200            )
1201            ->caller( __METHOD__ )->execute();
1202    }
1203
1204    /**
1205     * Clear all data from the secondary labels table. This method
1206     * is only for its use by reloadBuiltinData with the --force flag.
1207     *
1208     * @return void
1209     */
1210    public function clearLabelsSecondaryTables(): void {
1211        $dbw = $this->dbProvider->getPrimaryDatabase();
1212
1213        $dbw->newDeleteQueryBuilder()
1214            ->deleteFrom( 'wikilambda_zobject_labels' )
1215            ->where( IDatabase::ALL_ROWS )
1216            ->caller( __METHOD__ )->execute();
1217
1218        $dbw->newDeleteQueryBuilder()
1219            ->deleteFrom( 'wikilambda_zobject_label_conflicts' )
1220            ->where( IDatabase::ALL_ROWS )
1221            ->caller( __METHOD__ )->execute();
1222    }
1223
1224    /**
1225     * Clear all data from the secondary function joins table. This method
1226     * is only for its use by reloadBuiltinData with the --force flag.
1227     *
1228     * @return void
1229     */
1230    public function clearFunctionsSecondaryTables(): void {
1231        $dbw = $this->dbProvider->getPrimaryDatabase();
1232
1233        $dbw->newDeleteQueryBuilder()
1234            ->deleteFrom( 'wikilambda_zobject_function_join' )
1235            ->where( IDatabase::ALL_ROWS )
1236            ->caller( __METHOD__ )->execute();
1237    }
1238
1239    /**
1240     * Clear all data from the secondary tester results table. This method
1241     * is only for its use by reloadBuiltinData with the --force flag.
1242     *
1243     * @return void
1244     */
1245    public function clearTesterResultsSecondaryTables(): void {
1246        $dbw = $this->dbProvider->getPrimaryDatabase();
1247
1248        $dbw->newDeleteQueryBuilder()
1249            ->deleteFrom( 'wikilambda_ztester_results' )
1250            ->where( IDatabase::ALL_ROWS )
1251            ->caller( __METHOD__ )->execute();
1252    }
1253
1254    /**
1255     * Clear all data from the secondary languages table. This method
1256     * is only for its use by reloadBuiltinData with the --force flag.
1257     *
1258     * @return void
1259     */
1260    public function clearLanguageCacheSecondaryTables(): void {
1261        $dbw = $this->dbProvider->getPrimaryDatabase();
1262
1263        $dbw->newDeleteQueryBuilder()
1264            ->deleteFrom( 'wikilambda_zlanguages' )
1265            ->where( IDatabase::ALL_ROWS )
1266            ->caller( __METHOD__ )->execute();
1267    }
1268
1269}