Merge "MimeAnalyzer: Add testcases for mp3 detection"
[lhc/web/wiklou.git] / maintenance / recountCategories.php
1 <?php
2 /**
3 * Refreshes category counts.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Maintenance
22 */
23
24 require_once __DIR__ . '/Maintenance.php';
25
26 use MediaWiki\MediaWikiServices;
27
28 /**
29 * Maintenance script that refreshes category membership counts in the category
30 * table.
31 *
32 * (The populateCategory.php script will also recalculate counts, but
33 * recountCategories only updates rows that need to be updated, making it more
34 * efficient.)
35 *
36 * @ingroup Maintenance
37 */
38 class RecountCategories extends Maintenance {
39 public function __construct() {
40 parent::__construct();
41 $this->addDescription( <<<'TEXT'
42 This script refreshes the category membership counts stored in the category
43 table. As time passes, these counts often drift from the actual number of
44 category members. The script identifies rows where the value in the category
45 table does not match the number of categorylinks rows for that category, and
46 updates the category table accordingly.
47
48 To fully refresh the data in the category table, you need to run this script
49 three times: once in each mode. Alternatively, just one mode can be run if
50 required.
51 TEXT
52 );
53 $this->addOption(
54 'mode',
55 '(REQUIRED) Which category count column to recompute: "pages", "subcats" or "files".',
56 true,
57 true
58 );
59 $this->addOption(
60 'begin',
61 'Only recount categories with cat_id greater than the given value',
62 false,
63 true
64 );
65 $this->addOption(
66 'throttle',
67 'Wait this many milliseconds after each batch. Default: 0',
68 false,
69 true
70 );
71
72 $this->setBatchSize( 500 );
73 }
74
75 public function execute() {
76 $this->mode = $this->getOption( 'mode' );
77 if ( !in_array( $this->mode, [ 'pages', 'subcats', 'files' ] ) ) {
78 $this->error( 'Please specify a valid mode: one of "pages", "subcats" or "files".', 1 );
79 }
80
81 $this->minimumId = intval( $this->getOption( 'begin', 0 ) );
82
83 // do the work, batch by batch
84 $affectedRows = 0;
85 while ( ( $result = $this->doWork() ) !== false ) {
86 $affectedRows += $result;
87 usleep( $this->getOption( 'throttle', 0 ) * 1000 );
88 }
89
90 $this->output( "Done! Updated the {$this->mode} counts of $affectedRows categories.\n" .
91 "Now run the script using the other --mode options if you haven't already.\n" );
92 if ( $this->mode === 'pages' ) {
93 $this->output(
94 "Also run 'php cleanupEmptyCategories.php --mode remove' to remove empty,\n" .
95 "nonexistent categories from the category table.\n\n" );
96 }
97 }
98
99 protected function doWork() {
100 $this->output( "Finding up to {$this->mBatchSize} drifted rows " .
101 "starting at cat_id {$this->minimumId}...\n" );
102
103 $countingConds = [ 'cl_to = cat_title' ];
104 if ( $this->mode === 'subcats' ) {
105 $countingConds['cl_type'] = 'subcat';
106 } elseif ( $this->mode === 'files' ) {
107 $countingConds['cl_type'] = 'file';
108 }
109
110 $dbr = $this->getDB( DB_REPLICA, 'vslow' );
111 $countingSubquery = $dbr->selectSQLText( 'categorylinks',
112 'COUNT(*)',
113 $countingConds,
114 __METHOD__ );
115
116 // First, let's find out which categories have drifted and need to be updated.
117 // The query counts the categorylinks for each category on the replica DB,
118 // but this data can't be used for updating the master, so we don't include it
119 // in the results.
120 $idsToUpdate = $dbr->selectFieldValues( 'category',
121 'cat_id',
122 [
123 'cat_id > ' . $this->minimumId,
124 "cat_{$this->mode} != ($countingSubquery)"
125 ],
126 __METHOD__,
127 [ 'LIMIT' => $this->mBatchSize ]
128 );
129 if ( !$idsToUpdate ) {
130 return false;
131 }
132 $this->output( "Updating cat_{$this->mode} field on " .
133 count( $idsToUpdate ) . " rows...\n" );
134
135 // In the next batch, start where this query left off. The rows selected
136 // in this iteration shouldn't be selected again after being updated, but
137 // we still keep track of where we are up to, as extra protection against
138 // infinite loops.
139 $this->minimumId = end( $idsToUpdate );
140
141 // Now, on master, find the correct counts for these categories.
142 $dbw = $this->getDB( DB_MASTER );
143 $res = $dbw->select( 'category',
144 [ 'cat_id', 'count' => "($countingSubquery)" ],
145 [ 'cat_id' => $idsToUpdate ],
146 __METHOD__ );
147
148 // Update the category counts on the rows we just identified.
149 // This logic is equivalent to Category::refreshCounts, except here, we
150 // don't remove rows when cat_pages is zero and the category description page
151 // doesn't exist - instead we print a suggestion to run
152 // cleanupEmptyCategories.php.
153 $affectedRows = 0;
154 foreach ( $res as $row ) {
155 $dbw->update( 'category',
156 [ "cat_{$this->mode}" => $row->count ],
157 [
158 'cat_id' => $row->cat_id,
159 "cat_{$this->mode} != {$row->count}",
160 ],
161 __METHOD__ );
162 $affectedRows += $dbw->affectedRows();
163 }
164
165 MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->waitForReplication();
166
167 return $affectedRows;
168 }
169 }
170
171 $maintClass = 'RecountCategories';
172 require_once RUN_MAINTENANCE_IF_MAIN;