* @ingroup Maintenance
*/
+use Wikimedia\Rdbms\DBQueryError;
use Wikimedia\Rdbms\IDatabase;
require_once __DIR__ . '/Maintenance.php';
* @since 1.31
*/
class PopulateArchiveRevId extends LoggedUpdateMaintenance {
+
+ /** @var array|null Dummy revision row */
+ private static $dummyRev = null;
+
public function __construct() {
parent::__construct();
$this->addDescription( 'Populate ar_rev_id in pre-1.5 rows' );
protected function doDBUpdates() {
$this->output( "Populating ar_rev_id...\n" );
$dbw = $this->getDB( DB_MASTER );
+ self::checkMysqlAutoIncrementBug( $dbw );
// Quick exit if there are no rows needing updates.
$any = $dbw->selectField(
return true;
}
- $rev = $this->makeDummyRevisionRow( $dbw );
$count = 0;
while ( true ) {
wfWaitForSlaves();
return true;
}
+ $count += self::reassignArRevIds( $dbw, $arIds, [ 'ar_rev_id' => null ] );
+
+ $min = min( $arIds );
+ $max = max( $arIds );
+ $this->output( " ... $min-$max\n" );
+ }
+ }
+
+ /**
+ * Check for (and work around) a MySQL auto-increment bug
+ *
+ * (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34
+ * don't save the auto-increment value to disk, so on server restart it
+ * might reuse IDs from deleted revisions. We can fix that with an insert
+ * with an explicit rev_id value, if necessary.
+ *
+ * @param IDatabase $dbw
+ */
+ public static function checkMysqlAutoIncrementBug( IDatabase $dbw ) {
+ if ( $dbw->getType() !== 'mysql' ) {
+ return;
+ }
+
+ if ( !self::$dummyRev ) {
+ self::$dummyRev = self::makeDummyRevisionRow( $dbw );
+ }
+
+ $ok = false;
+ while ( !$ok ) {
try {
- $updates = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $arIds, $rev ) {
- // Create new rev_ids by inserting dummy rows into revision and then deleting them.
- $dbw->insert( 'revision', array_fill( 0, count( $arIds ), $rev ), $fname );
- $revIds = $dbw->selectFieldValues(
- 'revision',
- 'rev_id',
- [ 'rev_timestamp' => $rev['rev_timestamp'] ],
- $fname
+ $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
+ $dbw->insert( 'revision', self::$dummyRev, $fname );
+ $id = $dbw->insertId();
+ $toDelete[] = $id;
+
+ $maxId = max(
+ (int)$dbw->selectField( 'archive', 'MAX(ar_rev_id)', [], __METHOD__ ),
+ (int)$dbw->selectField( 'slots', 'MAX(slot_revision_id)', [], __METHOD__ )
);
- if ( !is_array( $revIds ) ) {
- throw new UnexpectedValueException( 'Failed to insert dummy revisions' );
+ if ( $id <= $maxId ) {
+ $dbw->insert( 'revision', [ 'rev_id' => $maxId + 1 ] + self::$dummyRev, $fname );
+ $toDelete[] = $maxId + 1;
}
- if ( count( $revIds ) !== count( $arIds ) ) {
- throw new UnexpectedValueException(
- 'Tried to insert ' . count( $arIds ) . ' dummy revisions, but found '
- . count( $revIds ) . ' matching rows.'
- );
- }
- $dbw->delete( 'revision', [ 'rev_id' => $revIds ], $fname );
- return array_combine( $arIds, $revIds );
+ $dbw->delete( 'revision', [ 'rev_id' => $toDelete ], $fname );
} );
- } catch ( UnexpectedValueException $ex ) {
- $this->fatalError( $ex->getMessage() );
+ $ok = true;
+ } catch ( DBQueryError $e ) {
+ if ( $e->errno != 1062 ) { // 1062 is "duplicate entry", ignore it and retry
+ throw $e;
+ }
}
+ }
+ }
- foreach ( $updates as $arId => $revId ) {
- $dbw->update(
- 'archive',
- [ 'ar_rev_id' => $revId ],
- [ 'ar_id' => $arId, 'ar_rev_id' => null ],
- __METHOD__
+ /**
+ * Assign new ar_rev_ids to a set of ar_ids.
+ * @param IDatabase $dbw
+ * @param int[] $arIds
+ * @param array $conds Extra conditions for the update
+ * @return int Number of updated rows
+ */
+ public static function reassignArRevIds( IDatabase $dbw, array $arIds, array $conds = [] ) {
+ if ( !self::$dummyRev ) {
+ self::$dummyRev = self::makeDummyRevisionRow( $dbw );
+ }
+
+ $updates = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $arIds ) {
+ // Create new rev_ids by inserting dummy rows into revision and then deleting them.
+ $dbw->insert( 'revision', array_fill( 0, count( $arIds ), self::$dummyRev ), $fname );
+ $revIds = $dbw->selectFieldValues(
+ 'revision',
+ 'rev_id',
+ [ 'rev_timestamp' => self::$dummyRev['rev_timestamp'] ],
+ $fname
+ );
+ if ( !is_array( $revIds ) ) {
+ throw new UnexpectedValueException( 'Failed to insert dummy revisions' );
+ }
+ if ( count( $revIds ) !== count( $arIds ) ) {
+ throw new UnexpectedValueException(
+ 'Tried to insert ' . count( $arIds ) . ' dummy revisions, but found '
+ . count( $revIds ) . ' matching rows.'
);
- $count += $dbw->affectedRows();
}
+ $dbw->delete( 'revision', [ 'rev_id' => $revIds ], $fname );
- $min = min( array_keys( $updates ) );
- $max = max( array_keys( $updates ) );
- $this->output( " ... $min-$max\n" );
+ return array_combine( $arIds, $revIds );
+ } );
+
+ $count = 0;
+ foreach ( $updates as $arId => $revId ) {
+ $dbw->update(
+ 'archive',
+ [ 'ar_rev_id' => $revId ],
+ [ 'ar_id' => $arId ] + $conds,
+ __METHOD__
+ );
+ $count += $dbw->affectedRows();
}
+ return $count;
}
/**
*
* The row will have a wildly unlikely timestamp, and possibly a generic
* user and comment, but will otherwise be derived from a revision on the
- * wiki's main page.
+ * wiki's main page or some other revision in the database.
*
* @param IDatabase $dbw
* @return array
*/
- private function makeDummyRevisionRow( IDatabase $dbw ) {
+ private static function makeDummyRevisionRow( IDatabase $dbw ) {
$ts = $dbw->timestamp( '11111111111111' );
+ $rev = null;
+
$mainPage = Title::newMainPage();
- if ( !$mainPage ) {
- $this->fatalError( 'Main page does not exist' );
+ $pageId = $mainPage ? $mainPage->getArticleId() : null;
+ if ( $pageId ) {
+ $rev = $dbw->selectRow(
+ 'revision',
+ '*',
+ [ 'rev_page' => $pageId ],
+ __METHOD__,
+ [ 'ORDER BY' => 'rev_timestamp ASC' ]
+ );
}
- $pageId = $mainPage->getArticleId();
- if ( !$pageId ) {
- $this->fatalError( $mainPage->getPrefixedText() . ' has no ID' );
+
+ if ( !$rev ) {
+ // No main page? Let's see if there are any revisions at all
+ $rev = $dbw->selectRow(
+ 'revision',
+ '*',
+ [],
+ __METHOD__,
+ [ 'ORDER BY' => 'rev_timestamp ASC' ]
+ );
}
- $rev = $dbw->selectRow(
- 'revision',
- '*',
- [ 'rev_page' => $pageId ],
- __METHOD__,
- [ 'ORDER BY' => 'rev_timestamp ASC' ]
- );
if ( !$rev ) {
- $this->fatalError( $mainPage->getPrefixedText() . ' has no revisions' );
+ throw new UnexpectedValueException( 'No revisions are available to copy' );
}
+
unset( $rev->rev_id );
$rev = (array)$rev;
$rev['rev_timestamp'] = $ts;
__METHOD__
);
if ( $any ) {
- $this->fatalError( "... Why does your database contain a revision dated $ts?" );
+ throw new UnexpectedValueException( "... Why does your database contain a revision dated $ts?" );
}
return $rev;