Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
PruneUnusedLinkTargetRows
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 2
90
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3use MediaWiki\Maintenance\Maintenance;
4
5// @codeCoverageIgnoreStart
6require_once __DIR__ . '/Maintenance.php';
7// @codeCoverageIgnoreEnd
8
9/**
10 * Maintenance script that cleans unused rows in linktarget table
11 *
12 * @ingroup Maintenance
13 * @since 1.39
14 */
15class PruneUnusedLinkTargetRows extends Maintenance {
16    public function __construct() {
17        parent::__construct();
18        $this->addDescription(
19            'Clean unused rows in linktarget table'
20        );
21        $this->addOption(
22            'sleep',
23            'Sleep time (in seconds) between every batch. Default: 0',
24            false,
25            true
26        );
27        $this->addOption( 'dry', 'Dry run', false );
28        $this->addOption( 'start', 'Start after this lt_id', false, true );
29        $this->setBatchSize( 50 );
30    }
31
32    public function execute() {
33        $dbw = $this->getPrimaryDB();
34        $dbr = $this->getReplicaDB();
35        $maxLtId = (int)$dbr->newSelectQueryBuilder()
36            ->select( 'MAX(lt_id)' )
37            ->from( 'linktarget' )
38            ->caller( __METHOD__ )
39            ->fetchField();
40        // To avoid race condition of newly added linktarget rows
41        // being deleted before getting a chance to be used, let's ignore the newest ones.
42        $maxLtId = min( [ $maxLtId - 1, (int)( $maxLtId * 0.99 ) ] );
43
44        $ltCounter = (int)$this->getOption( 'start', 0 );
45
46        $this->output( "Deleting unused linktarget rows...\n" );
47        $deleted = 0;
48        $linksMigration = $this->getServiceContainer()->getLinksMigration();
49        while ( $ltCounter < $maxLtId ) {
50            $batchMaxLtId = min( $ltCounter + $this->getBatchSize(), $maxLtId ) + 1;
51            $this->output( "Checking lt_id between $ltCounter and $batchMaxLtId...\n" );
52            $queryBuilder = $dbr->newSelectQueryBuilder()
53                ->select( [ 'lt_id' ] )
54                ->from( 'linktarget' );
55            $queryBuilder->where( [
56                $dbr->expr( 'lt_id', '<', $batchMaxLtId ),
57                $dbr->expr( 'lt_id', '>', $ltCounter )
58            ] );
59            foreach ( $linksMigration::$mapping as $table => $tableData ) {
60                $queryBuilder->leftJoin( $table, null, $tableData['target_id'] . '=lt_id' );
61                $queryBuilder->andWhere( [
62                    $tableData['target_id'] => null
63                ] );
64            }
65            $ltIdsToDelete = $queryBuilder->caller( __METHOD__ )->fetchFieldValues();
66            if ( !$ltIdsToDelete ) {
67                $ltCounter += $this->getBatchSize();
68                continue;
69            }
70
71            // Run against primary as well with a faster query plan, just to be safe.
72            // Also having a bit of time in between helps in cases of immediate removal and insertion of use.
73            $queryBuilder = $dbr->newSelectQueryBuilder()
74                ->select( [ 'lt_id' ] )
75                ->from( 'linktarget' )
76                ->where( [
77                    'lt_id' => $ltIdsToDelete,
78                ] );
79            foreach ( $linksMigration::$mapping as $table => $tableData ) {
80                $queryBuilder->leftJoin( $table, null, $tableData['target_id'] . '=lt_id' );
81                $queryBuilder->andWhere( [
82                    $tableData['target_id'] => null
83                ] );
84            }
85            $ltIdsToDelete = $queryBuilder->caller( __METHOD__ )->fetchFieldValues();
86            if ( !$ltIdsToDelete ) {
87                $ltCounter += $this->getBatchSize();
88                continue;
89            }
90
91            if ( !$this->getOption( 'dry' ) ) {
92                $dbw->newDeleteQueryBuilder()
93                    ->deleteFrom( 'linktarget' )
94                    ->where( [ 'lt_id' => $ltIdsToDelete ] )
95                    ->caller( __METHOD__ )->execute();
96            }
97            $deleted += count( $ltIdsToDelete );
98            $ltCounter += $this->getBatchSize();
99
100            // Sleep between batches for replication to catch up
101            $this->waitForReplication();
102            $sleep = (int)$this->getOption( 'sleep', 0 );
103            if ( $sleep > 0 ) {
104                sleep( $sleep );
105            }
106        }
107
108        $this->output(
109            "Completed clean up linktarget table, "
110            . "$deleted rows deleted.\n"
111        );
112
113        return true;
114    }
115
116}
117
118// @codeCoverageIgnoreStart
119$maintClass = PruneUnusedLinkTargetRows::class;
120require_once RUN_MAINTENANCE_IF_MAIN;
121// @codeCoverageIgnoreEnd