3 * Populate ar_rev_id in pre-1.5 rows
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.
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.
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
21 * @ingroup Maintenance
24 use Wikimedia\Rdbms\DBQueryError
;
25 use Wikimedia\Rdbms\IDatabase
;
27 require_once __DIR__
. '/Maintenance.php';
30 * Maintenance script that populares archive.ar_rev_id in old rows
32 * @ingroup Maintenance
35 class PopulateArchiveRevId
extends LoggedUpdateMaintenance
{
37 /** @var array|null Dummy revision row */
38 private static $dummyRev = null;
40 public function __construct() {
41 parent
::__construct();
42 $this->addDescription( 'Populate ar_rev_id in pre-1.5 rows' );
43 $this->setBatchSize( 100 );
47 * @param IDatabase $dbw
50 public static function isNewInstall( IDatabase
$dbw ) {
51 return $dbw->selectRowCount( 'archive' ) === 0 &&
52 $dbw->selectRowCount( 'revision' ) === 1;
55 protected function getUpdateKey() {
59 protected function doDBUpdates() {
60 $this->output( "Populating ar_rev_id...\n" );
61 $dbw = $this->getDB( DB_MASTER
);
62 self
::checkMysqlAutoIncrementBug( $dbw );
64 // Quick exit if there are no rows needing updates.
65 $any = $dbw->selectField(
68 [ 'ar_rev_id' => null ],
72 $this->output( "Completed ar_rev_id population, 0 rows updated.\n" );
80 $arIds = $dbw->selectFieldValues(
83 [ 'ar_rev_id' => null ],
85 [ 'LIMIT' => $this->getBatchSize(), 'ORDER BY' => [ 'ar_id' ] ]
88 $this->output( "Completed ar_rev_id population, $count rows updated.\n" );
92 $count +
= self
::reassignArRevIds( $dbw, $arIds, [ 'ar_rev_id' => null ] );
96 $this->output( " ... $min-$max\n" );
101 * Check for (and work around) a MySQL auto-increment bug
103 * (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34
104 * don't save the auto-increment value to disk, so on server restart it
105 * might reuse IDs from deleted revisions. We can fix that with an insert
106 * with an explicit rev_id value, if necessary.
108 * @param IDatabase $dbw
110 public static function checkMysqlAutoIncrementBug( IDatabase
$dbw ) {
111 if ( $dbw->getType() !== 'mysql' ) {
115 if ( !self
::$dummyRev ) {
116 self
::$dummyRev = self
::makeDummyRevisionRow( $dbw );
122 $dbw->doAtomicSection( __METHOD__
, function ( IDatabase
$dbw, $fname ) {
123 $dbw->insert( 'revision', self
::$dummyRev, $fname );
124 $id = $dbw->insertId();
128 (int)$dbw->selectField( 'archive', 'MAX(ar_rev_id)', [], $fname ),
129 (int)$dbw->selectField( 'slots', 'MAX(slot_revision_id)', [], $fname )
131 if ( $id <= $maxId ) {
132 $dbw->insert( 'revision', [ 'rev_id' => $maxId +
1 ] + self
::$dummyRev, $fname );
133 $toDelete[] = $maxId +
1;
136 $dbw->delete( 'revision', [ 'rev_id' => $toDelete ], $fname );
139 } catch ( DBQueryError
$e ) {
140 if ( $e->errno
!= 1062 ) { // 1062 is "duplicate entry", ignore it and retry
148 * Assign new ar_rev_ids to a set of ar_ids.
149 * @param IDatabase $dbw
150 * @param int[] $arIds
151 * @param array $conds Extra conditions for the update
152 * @return int Number of updated rows
154 public static function reassignArRevIds( IDatabase
$dbw, array $arIds, array $conds = [] ) {
155 if ( !self
::$dummyRev ) {
156 self
::$dummyRev = self
::makeDummyRevisionRow( $dbw );
159 $updates = $dbw->doAtomicSection( __METHOD__
, function ( IDatabase
$dbw, $fname ) use ( $arIds ) {
160 // Create new rev_ids by inserting dummy rows into revision and then deleting them.
161 $dbw->insert( 'revision', array_fill( 0, count( $arIds ), self
::$dummyRev ), $fname );
162 $revIds = $dbw->selectFieldValues(
165 [ 'rev_timestamp' => self
::$dummyRev['rev_timestamp'] ],
168 if ( !is_array( $revIds ) ) {
169 throw new UnexpectedValueException( 'Failed to insert dummy revisions' );
171 if ( count( $revIds ) !== count( $arIds ) ) {
172 throw new UnexpectedValueException(
173 'Tried to insert ' . count( $arIds ) . ' dummy revisions, but found '
174 . count( $revIds ) . ' matching rows.'
177 $dbw->delete( 'revision', [ 'rev_id' => $revIds ], $fname );
179 return array_combine( $arIds, $revIds );
183 foreach ( $updates as $arId => $revId ) {
186 [ 'ar_rev_id' => $revId ],
187 [ 'ar_id' => $arId ] +
$conds,
190 $count +
= $dbw->affectedRows();
196 * Construct a dummy revision table row to use for reserving IDs
198 * The row will have a wildly unlikely timestamp, and possibly a generic
199 * user and comment, but will otherwise be derived from a revision on the
200 * wiki's main page or some other revision in the database.
202 * @param IDatabase $dbw
205 private static function makeDummyRevisionRow( IDatabase
$dbw ) {
206 $ts = $dbw->timestamp( '11111111111111' );
209 $mainPage = Title
::newMainPage();
210 $pageId = $mainPage ?
$mainPage->getArticleID() : null;
212 $rev = $dbw->selectRow(
215 [ 'rev_page' => $pageId ],
217 [ 'ORDER BY' => 'rev_timestamp ASC' ]
222 // No main page? Let's see if there are any revisions at all
223 $rev = $dbw->selectRow(
228 [ 'ORDER BY' => 'rev_timestamp ASC' ]
232 // Since no revisions are available to copy, generate a dummy
233 // revision to a dummy page, then rollback the commit
234 wfDebug( __METHOD__
. ": No revisions are available to copy\n" );
238 // Make a title and revision and insert them
239 $title = Title
::newFromText( "PopulateArchiveRevId_4b05b46a81e29" );
240 $page = WikiPage
::factory( $title );
241 $updater = $page->newPageUpdater(
242 User
::newSystemUser( 'Maintenance script', [ 'steal' => true ] )
244 $updater->setContent(
246 ContentHandler
::makeContent( "Content for dummy rev", $title )
248 $updater->saveRevision(
249 CommentStoreComment
::newUnsavedComment( 'dummy rev summary' ),
250 EDIT_NEW | EDIT_SUPPRESS_RC
253 // get the revision row just inserted
254 $rev = $dbw->selectRow(
259 [ 'ORDER BY' => 'rev_timestamp ASC' ]
265 // This should never happen.
266 throw new UnexpectedValueException(
267 'No revisions are available to copy, and one couldn\'t be created'
271 unset( $rev->rev_id
);
273 $rev['rev_timestamp'] = $ts;
274 if ( isset( $rev['rev_user'] ) ) {
275 $rev['rev_user'] = 0;
276 $rev['rev_user_text'] = '0.0.0.0';
278 if ( isset( $rev['rev_comment'] ) ) {
279 $rev['rev_comment'] = 'Dummy row';
282 $any = $dbw->selectField(
285 [ 'rev_timestamp' => $ts ],
289 throw new UnexpectedValueException( "... Why does your database contain a revision dated $ts?" );
296 $maintClass = "PopulateArchiveRevId";
297 require_once RUN_MAINTENANCE_IF_MAIN
;