Add ability to override mb_strtoupper in Language::ucfirst
[lhc/web/wiklou.git] / maintenance / populateArchiveRevId.php
1 <?php
2 /**
3 * Populate ar_rev_id in pre-1.5 rows
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 use Wikimedia\Rdbms\DBQueryError;
25 use Wikimedia\Rdbms\IDatabase;
26
27 require_once __DIR__ . '/Maintenance.php';
28
29 /**
30 * Maintenance script that populares archive.ar_rev_id in old rows
31 *
32 * @ingroup Maintenance
33 * @since 1.31
34 */
35 class PopulateArchiveRevId extends LoggedUpdateMaintenance {
36
37 /** @var array|null Dummy revision row */
38 private static $dummyRev = null;
39
40 public function __construct() {
41 parent::__construct();
42 $this->addDescription( 'Populate ar_rev_id in pre-1.5 rows' );
43 $this->setBatchSize( 100 );
44 }
45
46 protected function getUpdateKey() {
47 return __CLASS__;
48 }
49
50 protected function doDBUpdates() {
51 $this->output( "Populating ar_rev_id...\n" );
52 $dbw = $this->getDB( DB_MASTER );
53 self::checkMysqlAutoIncrementBug( $dbw );
54
55 // Quick exit if there are no rows needing updates.
56 $any = $dbw->selectField(
57 'archive',
58 'ar_id',
59 [ 'ar_rev_id' => null ],
60 __METHOD__
61 );
62 if ( !$any ) {
63 $this->output( "Completed ar_rev_id population, 0 rows updated.\n" );
64 return true;
65 }
66
67 $count = 0;
68 while ( true ) {
69 wfWaitForSlaves();
70
71 $arIds = $dbw->selectFieldValues(
72 'archive',
73 'ar_id',
74 [ 'ar_rev_id' => null ],
75 __METHOD__,
76 [ 'LIMIT' => $this->getBatchSize(), 'ORDER BY' => [ 'ar_id' ] ]
77 );
78 if ( !$arIds ) {
79 $this->output( "Completed ar_rev_id population, $count rows updated.\n" );
80 return true;
81 }
82
83 $count += self::reassignArRevIds( $dbw, $arIds, [ 'ar_rev_id' => null ] );
84
85 $min = min( $arIds );
86 $max = max( $arIds );
87 $this->output( " ... $min-$max\n" );
88 }
89 }
90
91 /**
92 * Check for (and work around) a MySQL auto-increment bug
93 *
94 * (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34
95 * don't save the auto-increment value to disk, so on server restart it
96 * might reuse IDs from deleted revisions. We can fix that with an insert
97 * with an explicit rev_id value, if necessary.
98 *
99 * @param IDatabase $dbw
100 */
101 public static function checkMysqlAutoIncrementBug( IDatabase $dbw ) {
102 if ( $dbw->getType() !== 'mysql' ) {
103 return;
104 }
105
106 if ( !self::$dummyRev ) {
107 self::$dummyRev = self::makeDummyRevisionRow( $dbw );
108 }
109
110 $ok = false;
111 while ( !$ok ) {
112 try {
113 $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
114 $dbw->insert( 'revision', self::$dummyRev, $fname );
115 $id = $dbw->insertId();
116 $toDelete[] = $id;
117
118 $maxId = max(
119 (int)$dbw->selectField( 'archive', 'MAX(ar_rev_id)', [], $fname ),
120 (int)$dbw->selectField( 'slots', 'MAX(slot_revision_id)', [], $fname )
121 );
122 if ( $id <= $maxId ) {
123 $dbw->insert( 'revision', [ 'rev_id' => $maxId + 1 ] + self::$dummyRev, $fname );
124 $toDelete[] = $maxId + 1;
125 }
126
127 $dbw->delete( 'revision', [ 'rev_id' => $toDelete ], $fname );
128 } );
129 $ok = true;
130 } catch ( DBQueryError $e ) {
131 if ( $e->errno != 1062 ) { // 1062 is "duplicate entry", ignore it and retry
132 throw $e;
133 }
134 }
135 }
136 }
137
138 /**
139 * Assign new ar_rev_ids to a set of ar_ids.
140 * @param IDatabase $dbw
141 * @param int[] $arIds
142 * @param array $conds Extra conditions for the update
143 * @return int Number of updated rows
144 */
145 public static function reassignArRevIds( IDatabase $dbw, array $arIds, array $conds = [] ) {
146 if ( !self::$dummyRev ) {
147 self::$dummyRev = self::makeDummyRevisionRow( $dbw );
148 }
149
150 $updates = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $arIds ) {
151 // Create new rev_ids by inserting dummy rows into revision and then deleting them.
152 $dbw->insert( 'revision', array_fill( 0, count( $arIds ), self::$dummyRev ), $fname );
153 $revIds = $dbw->selectFieldValues(
154 'revision',
155 'rev_id',
156 [ 'rev_timestamp' => self::$dummyRev['rev_timestamp'] ],
157 $fname
158 );
159 if ( !is_array( $revIds ) ) {
160 throw new UnexpectedValueException( 'Failed to insert dummy revisions' );
161 }
162 if ( count( $revIds ) !== count( $arIds ) ) {
163 throw new UnexpectedValueException(
164 'Tried to insert ' . count( $arIds ) . ' dummy revisions, but found '
165 . count( $revIds ) . ' matching rows.'
166 );
167 }
168 $dbw->delete( 'revision', [ 'rev_id' => $revIds ], $fname );
169
170 return array_combine( $arIds, $revIds );
171 } );
172
173 $count = 0;
174 foreach ( $updates as $arId => $revId ) {
175 $dbw->update(
176 'archive',
177 [ 'ar_rev_id' => $revId ],
178 [ 'ar_id' => $arId ] + $conds,
179 __METHOD__
180 );
181 $count += $dbw->affectedRows();
182 }
183 return $count;
184 }
185
186 /**
187 * Construct a dummy revision table row to use for reserving IDs
188 *
189 * The row will have a wildly unlikely timestamp, and possibly a generic
190 * user and comment, but will otherwise be derived from a revision on the
191 * wiki's main page or some other revision in the database.
192 *
193 * @param IDatabase $dbw
194 * @return array
195 */
196 private static function makeDummyRevisionRow( IDatabase $dbw ) {
197 $ts = $dbw->timestamp( '11111111111111' );
198 $rev = null;
199
200 $mainPage = Title::newMainPage();
201 $pageId = $mainPage ? $mainPage->getArticleID() : null;
202 if ( $pageId ) {
203 $rev = $dbw->selectRow(
204 'revision',
205 '*',
206 [ 'rev_page' => $pageId ],
207 __METHOD__,
208 [ 'ORDER BY' => 'rev_timestamp ASC' ]
209 );
210 }
211
212 if ( !$rev ) {
213 // No main page? Let's see if there are any revisions at all
214 $rev = $dbw->selectRow(
215 'revision',
216 '*',
217 [],
218 __METHOD__,
219 [ 'ORDER BY' => 'rev_timestamp ASC' ]
220 );
221 }
222 if ( !$rev ) {
223 // Since no revisions are available to copy, generate a dummy
224 // revision to a dummy page, then rollback the commit
225 wfDebug( __METHOD__ . ": No revisions are available to copy\n" );
226
227 $dbw->begin();
228
229 // Make a title and revision and insert them
230 $title = Title::newFromText( "PopulateArchiveRevId_4b05b46a81e29" );
231 $page = WikiPage::factory( $title );
232 $updater = $page->newPageUpdater(
233 User::newSystemUser( 'Maintenance script', [ 'steal' => true ] )
234 );
235 $updater->setContent(
236 'main',
237 ContentHandler::makeContent( "Content for dummy rev", $title )
238 );
239 $updater->saveRevision(
240 CommentStoreComment::newUnsavedComment( 'dummy rev summary' ),
241 EDIT_NEW | EDIT_SUPPRESS_RC
242 );
243
244 // get the revision row just inserted
245 $rev = $dbw->selectRow(
246 'revision',
247 '*',
248 [],
249 __METHOD__,
250 [ 'ORDER BY' => 'rev_timestamp ASC' ]
251 );
252
253 $dbw->rollback();
254 }
255 if ( !$rev ) {
256 // This should never happen.
257 throw new UnexpectedValueException(
258 'No revisions are available to copy, and one couldn\'t be created'
259 );
260 }
261
262 unset( $rev->rev_id );
263 $rev = (array)$rev;
264 $rev['rev_timestamp'] = $ts;
265 if ( isset( $rev['rev_user'] ) ) {
266 $rev['rev_user'] = 0;
267 $rev['rev_user_text'] = '0.0.0.0';
268 }
269 if ( isset( $rev['rev_comment'] ) ) {
270 $rev['rev_comment'] = 'Dummy row';
271 }
272
273 $any = $dbw->selectField(
274 'revision',
275 'rev_id',
276 [ 'rev_timestamp' => $ts ],
277 __METHOD__
278 );
279 if ( $any ) {
280 throw new UnexpectedValueException( "... Why does your database contain a revision dated $ts?" );
281 }
282
283 return $rev;
284 }
285 }
286
287 $maintClass = "PopulateArchiveRevId";
288 require_once RUN_MAINTENANCE_IF_MAIN;