Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 104
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
MigrateLinksTable
0.00% covered (danger)
0.00%
0 / 104
0.00% covered (danger)
0.00%
0 / 5
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 getUpdateKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doDBUpdates
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
56
 handlePageBatch
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
56
 updateProgress
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3// @codeCoverageIgnoreStart
4require_once __DIR__ . '/Maintenance.php';
5// @codeCoverageIgnoreEnd
6
7use MediaWiki\Maintenance\LoggedUpdateMaintenance;
8use MediaWiki\Title\TitleValue;
9
10/**
11 * Maintenance script that populates normalization column in links tables.
12 *
13 * @ingroup Maintenance
14 * @since 1.39
15 */
16class MigrateLinksTable extends LoggedUpdateMaintenance {
17    /** @var int */
18    private $totalUpdated = 0;
19    /** @var int */
20    private $lastProgress = 0;
21
22    public function __construct() {
23        parent::__construct();
24        $this->addDescription(
25            'Populates normalization column in links tables.'
26        );
27        $this->addOption(
28            'table',
29            'Table name. Like pagelinks.',
30            true,
31            true
32        );
33        $this->addOption(
34            'sleep',
35            'Sleep time (in seconds) between every batch. Default: 0',
36            false,
37            true
38        );
39        $this->setBatchSize( 1000 );
40    }
41
42    /** @inheritDoc */
43    protected function getUpdateKey() {
44        return __CLASS__ . $this->getOption( 'table', '' );
45    }
46
47    /** @inheritDoc */
48    protected function doDBUpdates() {
49        $dbw = $this->getDB( DB_PRIMARY );
50        $mapping = \MediaWiki\Linker\LinksMigration::$mapping;
51        $table = $this->getOption( 'table', '' );
52        if ( !isset( $mapping[$table] ) ) {
53            $this->output( "Mapping for this table doesn't exist yet.\n" );
54            return false;
55        }
56        $targetColumn = $mapping[$table]['target_id'];
57        if ( !$dbw->fieldExists( $table, $mapping[$table]['title'], __METHOD__ ) ) {
58            $this->output( "Old fields don't exist. There is no need to run this script\n" );
59            return true;
60        }
61        if ( !$dbw->fieldExists( $table, $targetColumn, __METHOD__ ) ) {
62            $this->output( "Run update.php to create the $targetColumn column.\n" );
63            return false;
64        }
65        if ( !$dbw->tableExists( 'linktarget', __METHOD__ ) ) {
66            $this->output( "Run update.php to create the linktarget table.\n" );
67            return true;
68        }
69
70        $this->output( "Populating the $targetColumn column\n" );
71        $updated = 0;
72
73        $highestPageId = $dbw->newSelectQueryBuilder()
74            ->select( 'page_id' )
75            ->from( 'page' )
76            ->caller( __METHOD__ )
77            ->orderBy( 'page_id', 'DESC' )
78            ->fetchField();
79        if ( !$highestPageId ) {
80            $this->output( "Page table is empty.\n" );
81            return true;
82        }
83        $pageId = 0;
84        while ( $pageId <= $highestPageId ) {
85            // Given the indexes and the structure of links tables,
86            // we need to split the update into batches of pages.
87            // Otherwise the queries will take a really long time in production and cause read-only.
88            $this->handlePageBatch( $pageId, $mapping, $table );
89            $pageId += $this->getBatchSize();
90        }
91
92        $this->output( "Completed normalization of $table{$this->totalUpdated} rows updated.\n" );
93
94        return true;
95    }
96
97    private function handlePageBatch( int $lowPageId, array $mapping, string $table ) {
98        $batchSize = $this->getBatchSize();
99        $targetColumn = $mapping[$table]['target_id'];
100        $pageIdColumn = $mapping[$table]['page_id'];
101        // range is inclusive, let's subtract one.
102        $highPageId = $lowPageId + $batchSize - 1;
103        $dbw = $this->getPrimaryDB();
104
105        // Check if 'ns' is an integer constant (like NS_CATEGORY) or a column name
106        $nsColumn = $mapping[$table]['ns'];
107        $nsIsConstant = is_int( $nsColumn );
108        $nsValue = $nsIsConstant ? $nsColumn : null;
109
110        while ( true ) {
111            // Build SELECT clause: if ns is a constant, don't select it; if it's a column, select it
112            $selectFields = $nsIsConstant
113                ? [ $mapping[$table]['title'] ]
114                : [ $mapping[$table]['ns'], $mapping[$table]['title'] ];
115
116            $res = $dbw->newSelectQueryBuilder()
117                ->select( $selectFields )
118                ->from( $table )
119                ->where( [
120                    $targetColumn => [ null, 0 ],
121                    $dbw->expr( $pageIdColumn, '>=', $lowPageId ),
122                    $dbw->expr( $pageIdColumn, '<=', $highPageId ),
123                ] )
124                ->limit( 1 )
125                ->caller( __METHOD__ )
126                ->fetchResultSet();
127            if ( !$res->numRows() ) {
128                break;
129            }
130            $row = $res->fetchRow();
131            $ns = $nsIsConstant ? $nsValue : (int)$row[$nsColumn];
132            $titleString = $row[$mapping[$table]['title']];
133            $title = new TitleValue( $ns, $titleString );
134            $id = $this->getServiceContainer()->getLinkTargetLookup()->acquireLinkTargetId( $title, $dbw );
135            // Build WHERE clause: if ns is a constant, don't add it as a condition
136            $whereConditions = [
137                $targetColumn => [ null, 0 ],
138                $mapping[$table]['title'] => $titleString,
139                $dbw->expr( $pageIdColumn, '>=', $lowPageId ),
140                $dbw->expr( $pageIdColumn, '<=', $highPageId ),
141            ];
142            if ( !$nsIsConstant ) {
143                $whereConditions[$nsColumn] = $ns;
144            }
145
146            $dbw->newUpdateQueryBuilder()
147                ->update( $table )
148                ->set( [ $targetColumn => $id ] )
149                ->where( $whereConditions )
150                ->caller( __METHOD__ )->execute();
151            $this->updateProgress( $dbw->affectedRows(), $lowPageId, $highPageId, $ns, $titleString );
152        }
153    }
154
155    /**
156     * Update the total progress metric. If enough progress has been made,
157     * report to the user and do a replication wait.
158     *
159     * @param int $updatedInThisBatch
160     * @param int $lowPageId
161     * @param int $highPageId
162     * @param int $ns
163     * @param string $titleString
164     */
165    private function updateProgress( $updatedInThisBatch, $lowPageId, $highPageId, $ns, $titleString ) {
166        $this->totalUpdated += $updatedInThisBatch;
167        if ( $this->totalUpdated >= $this->lastProgress + $this->getBatchSize() ) {
168            $this->lastProgress = $this->totalUpdated;
169            $this->output( "Updated {$this->totalUpdated} rows, " .
170                "at page_id $lowPageId-$highPageId title $ns:$titleString\n" );
171            $this->waitForReplication();
172            // Sleep between batches for replication to catch up
173            $sleep = (int)$this->getOption( 'sleep', 0 );
174            if ( $sleep > 0 ) {
175                sleep( $sleep );
176            }
177        }
178    }
179
180}
181
182// @codeCoverageIgnoreStart
183$maintClass = MigrateLinksTable::class;
184require_once RUN_MAINTENANCE_IF_MAIN;
185// @codeCoverageIgnoreEnd