Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.77% covered (success)
90.77%
59 / 65
84.62% covered (warning)
84.62%
11 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
GenericPageLinksTable
90.77% covered (success)
90.77%
59 / 65
84.62% covered (warning)
84.62%
11 / 13
27.57
0.00% covered (danger)
0.00%
0 / 1
 getNamespaceField
n/a
0 / 0
n/a
0 / 0
0
 getTitleField
n/a
0 / 0
n/a
0 / 0
0
 getTargetIdField
n/a
0 / 0
n/a
0 / 0
0
 getFromNamespaceField
n/a
0 / 0
n/a
0 / 0
0
 getExistingFields
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getExistingLinks
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 fetchExistingRows
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getNewLinkIDs
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getExistingLinkIDs
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 isExisting
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isInNewSet
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 insertLink
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 deleteLink
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 needForcedLinkRefresh
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makePageReferenceValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deduplicateLinkIds
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Deferred\LinksUpdate;
4
5use MediaWiki\DAO\WikiAwareEntity;
6use MediaWiki\Page\PageReferenceValue;
7use MediaWiki\Title\Title;
8use Wikimedia\Rdbms\IResultWrapper;
9
10/**
11 * Shared code for pagelinks and templatelinks. They are very similar tables
12 * since they both link to an arbitrary page identified by namespace and title.
13 *
14 * Link ID format: string[]:
15 *   - 0: namespace ID
16 *   - 1: title DB key
17 *
18 * @since 1.38
19 */
20abstract class GenericPageLinksTable extends TitleLinksTable {
21    /**
22     * A 2d array representing the new links, with the namespace ID in the
23     * first key, the DB key in the second key, and the value arbitrary.
24     *
25     * @var array
26     */
27    protected $newLinks = [];
28
29    /**
30     * The existing links in the same format as self::$newLinks, or null if it
31     * has not been loaded yet.
32     *
33     * @var array|null
34     */
35    private $existingLinks;
36
37    /**
38     * Get the namespace field name
39     *
40     * @return string
41     */
42    abstract protected function getNamespaceField();
43
44    /**
45     * Get the title (DB key) field name
46     *
47     * @return string
48     */
49    abstract protected function getTitleField();
50
51    /**
52     * Get the link target id (DB key) field name
53     *
54     * @return string
55     */
56    abstract protected function getTargetIdField();
57
58    /**
59     * @return mixed
60     */
61    abstract protected function getFromNamespaceField();
62
63    protected function getExistingFields() {
64        if ( $this->linksTargetNormalizationStage() & SCHEMA_COMPAT_WRITE_OLD ) {
65            return [
66                'ns' => $this->getNamespaceField(),
67                'title' => $this->getTitleField()
68            ];
69        }
70        return [
71            'ns' => 'lt_namespace',
72            'title' => 'lt_title',
73        ];
74    }
75
76    /**
77     * Get existing links as an associative array
78     *
79     * @return array
80     */
81    private function getExistingLinks() {
82        if ( $this->existingLinks === null ) {
83            $this->existingLinks = [];
84            foreach ( $this->fetchExistingRows() as $row ) {
85                $this->existingLinks[$row->ns][$row->title] = 1;
86            }
87        }
88
89        return $this->existingLinks;
90    }
91
92    protected function fetchExistingRows(): IResultWrapper {
93        $queryBuilder = $this->getDB()->newSelectQueryBuilder()
94            ->select( $this->getExistingFields() )
95            ->from( $this->getTableName() )
96            ->where( $this->getFromConds() );
97
98        // This read is for updating, it's conceptually better to use the write config
99        if ( !( $this->linksTargetNormalizationStage() & SCHEMA_COMPAT_WRITE_OLD ) ) {
100            $queryBuilder->join( 'linktarget', null, [ $this->getTargetIdField() . '=lt_id' ] );
101        }
102
103        return $queryBuilder
104            ->caller( __METHOD__ )
105            ->fetchResultSet();
106    }
107
108    protected function getNewLinkIDs() {
109        foreach ( $this->newLinks as $ns => $links ) {
110            foreach ( $links as $dbk => $unused ) {
111                yield [ $ns, (string)$dbk ];
112            }
113        }
114    }
115
116    protected function getExistingLinkIDs() {
117        foreach ( $this->getExistingLinks() as $ns => $links ) {
118            foreach ( $links as $dbk => $unused ) {
119                yield [ $ns, (string)$dbk ];
120            }
121        }
122    }
123
124    protected function isExisting( $linkId ) {
125        [ $ns, $dbk ] = $linkId;
126        return isset( $this->getExistingLinks()[$ns][$dbk] );
127    }
128
129    protected function isInNewSet( $linkId ) {
130        [ $ns, $dbk ] = $linkId;
131        return isset( $this->newLinks[$ns][$dbk] );
132    }
133
134    protected function insertLink( $linkId ) {
135        $row = [
136            $this->getFromNamespaceField() => $this->getSourcePage()->getNamespace(),
137        ];
138        if ( $this->linksTargetNormalizationStage() & SCHEMA_COMPAT_WRITE_OLD ) {
139            $row[$this->getNamespaceField()] = $linkId[0];
140            $row[$this->getTitleField()] = $linkId[1];
141        }
142        if ( $this->linksTargetNormalizationStage() & SCHEMA_COMPAT_WRITE_NEW ) {
143            $row[$this->getTargetIdField()] = $this->linkTargetLookup->acquireLinkTargetId(
144                $this->makeTitle( $linkId ),
145                $this->getDB()
146            );
147        }
148        $this->insertRow( $row );
149    }
150
151    protected function deleteLink( $linkId ) {
152        if ( $this->linksTargetNormalizationStage() & SCHEMA_COMPAT_WRITE_OLD ) {
153            $this->deleteRow( [
154                $this->getNamespaceField() => $linkId[0],
155                $this->getTitleField() => $linkId[1]
156            ] );
157        } elseif ( $this->linksTargetNormalizationStage() & SCHEMA_COMPAT_WRITE_NEW ) {
158            $this->deleteRow( [
159                $this->getTargetIdField() => $this->linkTargetLookup->acquireLinkTargetId(
160                    $this->makeTitle( $linkId ),
161                    $this->getDB()
162                )
163            ] );
164        }
165    }
166
167    protected function needForcedLinkRefresh() {
168        return $this->isCrossNamespaceMove();
169    }
170
171    protected function makePageReferenceValue( $linkId ): PageReferenceValue {
172        return new PageReferenceValue( $linkId[0], $linkId[1], WikiAwareEntity::LOCAL );
173    }
174
175    protected function makeTitle( $linkId ): Title {
176        return Title::makeTitle( $linkId[0], $linkId[1] );
177    }
178
179    protected function deduplicateLinkIds( $linkIds ) {
180        $seen = [];
181        foreach ( $linkIds as $linkId ) {
182            if ( !isset( $seen[$linkId[0]][$linkId[1]] ) ) {
183                $seen[$linkId[0]][$linkId[1]] = true;
184                yield $linkId;
185            }
186        }
187    }
188}