Avoid INSERT..SELECT in LocalFileDeleteBatch
[lhc/web/wiklou.git] / includes / filerepo / file / LocalFile.php
index e35af75..40141c9 100644 (file)
@@ -117,6 +117,9 @@ class LocalFile extends File {
        /** @var bool Whether the row was upgraded on load */
        private $upgraded;
 
+       /** @var bool Whether the row was scheduled to upgrade on load */
+       private $upgrading;
+
        /** @var bool True if the image row is locked */
        private $locked;
 
@@ -559,37 +562,43 @@ class LocalFile extends File {
         */
        function maybeUpgradeRow() {
                global $wgUpdateCompatibleMetadata;
-               if ( wfReadOnly() ) {
+
+               if ( wfReadOnly() || $this->upgrading ) {
                        return;
                }
 
                $upgrade = false;
-               if ( is_null( $this->media_type ) ||
-                       $this->mime == 'image/svg'
-               ) {
+               if ( is_null( $this->media_type ) || $this->mime == 'image/svg' ) {
                        $upgrade = true;
                } else {
                        $handler = $this->getHandler();
                        if ( $handler ) {
                                $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
-                               if ( $validity === MediaHandler::METADATA_BAD
-                                       || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
-                               ) {
+                               if ( $validity === MediaHandler::METADATA_BAD ) {
                                        $upgrade = true;
+                               } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE ) {
+                                       $upgrade = $wgUpdateCompatibleMetadata;
                                }
                        }
                }
 
                if ( $upgrade ) {
-                       try {
-                               $this->upgradeRow();
-                       } catch ( LocalFileLockError $e ) {
-                               // let the other process handle it (or do it next time)
-                       }
-                       $this->upgraded = true; // avoid rework/retries
+                       $this->upgrading = true;
+                       // Defer updates unless in auto-commit CLI mode
+                       DeferredUpdates::addCallableUpdate( function() {
+                               $this->upgrading = false; // avoid duplicate updates
+                               try {
+                                       $this->upgradeRow();
+                               } catch ( LocalFileLockError $e ) {
+                                       // let the other process handle it (or do it next time)
+                               }
+                       } );
                }
        }
 
+       /**
+        * @return bool Whether upgradeRow() ran for this object
+        */
        function getUpgraded() {
                return $this->upgraded;
        }
@@ -639,7 +648,7 @@ class LocalFile extends File {
                $this->invalidateCache();
 
                $this->unlock(); // done
-
+               $this->upgraded = true; // avoid rework/retries
        }
 
        /**
@@ -964,6 +973,33 @@ class LocalFile extends File {
                DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
        }
 
+       /**
+        * Prerenders a configurable set of thumbnails
+        *
+        * @since 1.28
+        */
+       public function prerenderThumbnails() {
+               global $wgUploadThumbnailRenderMap;
+
+               $jobs = [];
+
+               $sizes = $wgUploadThumbnailRenderMap;
+               rsort( $sizes );
+
+               foreach ( $sizes as $size ) {
+                       if ( $this->isVectorized() || $this->getWidth() > $size ) {
+                               $jobs[] = new ThumbnailRenderJob(
+                                       $this->getTitle(),
+                                       [ 'transformParams' => [ 'width' => $size ] ]
+                               );
+                       }
+               }
+
+               if ( $jobs ) {
+                       JobQueueGroup::singleton()->lazyPush( $jobs );
+               }
+       }
+
        /**
         * Delete a list of thumbnails visible at urls
         * @param string $dir Base dir of the files.
@@ -1422,97 +1458,106 @@ class LocalFile extends File {
                # Do some cache purges after final commit so that:
                # a) Changes are more likely to be seen post-purge
                # b) They won't cause rollback of the log publish/update above
-               $that = $this;
-               $dbw->onTransactionIdle( function () use (
-                       $that, $reupload, $wikiPage, $newPageContent, $comment, $user, $logEntry, $logId, $descId, $tags
-               ) {
-                       # Update memcache after the commit
-                       $that->invalidateCache();
-
-                       $updateLogPage = false;
-                       if ( $newPageContent ) {
-                               # New file page; create the description page.
-                               # There's already a log entry, so don't make a second RC entry
-                               # CDN and file cache for the description page are purged by doEditContent.
-                               $status = $wikiPage->doEditContent(
-                                       $newPageContent,
-                                       $comment,
-                                       EDIT_NEW | EDIT_SUPPRESS_RC,
-                                       false,
-                                       $user
-                               );
-
-                               if ( isset( $status->value['revision'] ) ) {
-                                       // Associate new page revision id
-                                       $logEntry->setAssociatedRevId( $status->value['revision']->getId() );
-                               }
-                               // This relies on the resetArticleID() call in WikiPage::insertOn(),
-                               // which is triggered on $descTitle by doEditContent() above.
-                               if ( isset( $status->value['revision'] ) ) {
-                                       /** @var $rev Revision */
-                                       $rev = $status->value['revision'];
-                                       $updateLogPage = $rev->getPage();
-                               }
-                       } else {
-                               # Existing file page: invalidate description page cache
-                               $wikiPage->getTitle()->invalidateCache();
-                               $wikiPage->getTitle()->purgeSquid();
-                               # Allow the new file version to be patrolled from the page footer
-                               Article::purgePatrolFooterCache( $descId );
-                       }
+               DeferredUpdates::addUpdate(
+                       new AutoCommitUpdate(
+                               $dbw,
+                               __METHOD__,
+                               function () use (
+                                       $reupload, $wikiPage, $newPageContent, $comment, $user,
+                                       $logEntry, $logId, $descId, $tags
+                               ) {
+                                       # Update memcache after the commit
+                                       $this->invalidateCache();
+
+                                       $updateLogPage = false;
+                                       if ( $newPageContent ) {
+                                               # New file page; create the description page.
+                                               # There's already a log entry, so don't make a second RC entry
+                                               # CDN and file cache for the description page are purged by doEditContent.
+                                               $status = $wikiPage->doEditContent(
+                                                       $newPageContent,
+                                                       $comment,
+                                                       EDIT_NEW | EDIT_SUPPRESS_RC,
+                                                       false,
+                                                       $user
+                                               );
+
+                                               if ( isset( $status->value['revision'] ) ) {
+                                                       // Associate new page revision id
+                                                       $logEntry->setAssociatedRevId( $status->value['revision']->getId() );
+                                               }
+                                               // This relies on the resetArticleID() call in WikiPage::insertOn(),
+                                               // which is triggered on $descTitle by doEditContent() above.
+                                               if ( isset( $status->value['revision'] ) ) {
+                                                       /** @var $rev Revision */
+                                                       $rev = $status->value['revision'];
+                                                       $updateLogPage = $rev->getPage();
+                                               }
+                                       } else {
+                                               # Existing file page: invalidate description page cache
+                                               $wikiPage->getTitle()->invalidateCache();
+                                               $wikiPage->getTitle()->purgeSquid();
+                                               # Allow the new file version to be patrolled from the page footer
+                                               Article::purgePatrolFooterCache( $descId );
+                                       }
 
-                       # Update associated rev id. This should be done by $logEntry->insert() earlier,
-                       # but setAssociatedRevId() wasn't called at that point yet...
-                       $logParams = $logEntry->getParameters();
-                       $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
-                       $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
-                       if ( $updateLogPage ) {
-                               # Also log page, in case where we just created it above
-                               $update['log_page'] = $updateLogPage;
-                       }
-                       $that->getRepo()->getMasterDB()->update(
-                               'logging',
-                               $update,
-                               [ 'log_id' => $logId ],
-                               __METHOD__
-                       );
-                       $that->getRepo()->getMasterDB()->insert(
-                               'log_search',
-                               [
-                                       'ls_field' => 'associated_rev_id',
-                                       'ls_value' => $logEntry->getAssociatedRevId(),
-                                       'ls_log_id' => $logId,
-                               ],
-                               __METHOD__
-                       );
+                                       # Update associated rev id. This should be done by $logEntry->insert() earlier,
+                                       # but setAssociatedRevId() wasn't called at that point yet...
+                                       $logParams = $logEntry->getParameters();
+                                       $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
+                                       $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
+                                       if ( $updateLogPage ) {
+                                               # Also log page, in case where we just created it above
+                                               $update['log_page'] = $updateLogPage;
+                                       }
+                                       $this->getRepo()->getMasterDB()->update(
+                                               'logging',
+                                               $update,
+                                               [ 'log_id' => $logId ],
+                                               __METHOD__
+                                       );
+                                       $this->getRepo()->getMasterDB()->insert(
+                                               'log_search',
+                                               [
+                                                       'ls_field' => 'associated_rev_id',
+                                                       'ls_value' => $logEntry->getAssociatedRevId(),
+                                                       'ls_log_id' => $logId,
+                                               ],
+                                               __METHOD__
+                                       );
+
+                                       # Add change tags, if any
+                                       if ( $tags ) {
+                                               $logEntry->setTags( $tags );
+                                       }
 
-                       # Add change tags, if any
-                       if ( $tags ) {
-                               $logEntry->setTags( $tags );
-                       }
+                                       # Uploads can be patrolled
+                                       $logEntry->setIsPatrollable( true );
 
-                       # Uploads can be patrolled
-                       $logEntry->setIsPatrollable( true );
+                                       # Now that the log entry is up-to-date, make an RC entry.
+                                       $logEntry->publish( $logId );
 
-                       # Now that the log entry is up-to-date, make an RC entry.
-                       $logEntry->publish( $logId );
+                                       # Run hook for other updates (typically more cache purging)
+                                       Hooks::run( 'FileUpload', [ $this, $reupload, !$newPageContent ] );
 
-                       # Run hook for other updates (typically more cache purging)
-                       Hooks::run( 'FileUpload', [ $that, $reupload, !$newPageContent ] );
+                                       if ( $reupload ) {
+                                               # Delete old thumbnails
+                                               $this->purgeThumbnails();
+                                               # Remove the old file from the CDN cache
+                                               DeferredUpdates::addUpdate(
+                                                       new CdnCacheUpdate( [ $this->getUrl() ] ),
+                                                       DeferredUpdates::PRESEND
+                                               );
+                                       } else {
+                                               # Update backlink pages pointing to this title if created
+                                               LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
+                                       }
 
-                       if ( $reupload ) {
-                               # Delete old thumbnails
-                               $that->purgeThumbnails();
-                               # Remove the old file from the CDN cache
-                               DeferredUpdates::addUpdate(
-                                       new CdnCacheUpdate( [ $that->getUrl() ] ),
-                                       DeferredUpdates::PRESEND
-                               );
-                       } else {
-                               # Update backlink pages pointing to this title if created
-                               LinksUpdate::queueRecursiveJobsForTable( $that->getTitle(), 'imagelinks' );
-                       }
-               } );
+                                       $this->prerenderThumbnails();
+                               }
+                       ),
+                       DeferredUpdates::PRESEND
+               );
 
                if ( !$reupload ) {
                        # This is a new file, so update the image count
@@ -1913,14 +1958,36 @@ class LocalFile extends File {
                && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
        }
 
+       /**
+        * @return Status
+        * @since 1.28
+        */
+       public function acquireFileLock() {
+               return $this->getRepo()->getBackend()->lockFiles(
+                       [ $this->getPath() ], LockManager::LOCK_EX, 10
+               );
+       }
+
+       /**
+        * @return Status
+        * @since 1.28
+        */
+       public function releaseFileLock() {
+               return $this->getRepo()->getBackend()->unlockFiles(
+                       [ $this->getPath() ], LockManager::LOCK_EX
+               );
+       }
+
        /**
         * Start an atomic DB section and lock the image for update
         * or increments a reference counter if the lock is already held
         *
+        * This method should not be used outside of LocalFile/LocalFile*Batch
+        *
         * @throws LocalFileLockError Throws an error if the lock was not acquired
         * @return bool Whether the file lock owns/spawned the DB transaction
         */
-       function lock() {
+       public function lock() {
                if ( !$this->locked ) {
                        $logger = LoggerFactory::getInstance( 'LocalFile' );
 
@@ -1930,9 +1997,7 @@ class LocalFile extends File {
                        // Bug 54736: use simple lock to handle when the file does not exist.
                        // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
                        // Also, that would cause contention on INSERT of similarly named rows.
-                       $backend = $this->getRepo()->getBackend();
-                       $lockPaths = [ $this->getPath() ]; // represents all versions of the file
-                       $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 10 );
+                       $status = $this->acquireFileLock(); // represents all versions of the file
                        if ( !$status->isGood() ) {
                                $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
                                $logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
@@ -1941,8 +2006,8 @@ class LocalFile extends File {
                        }
                        // Release the lock *after* commit to avoid row-level contention.
                        // Make sure it triggers on rollback() as well as commit() (T132921).
-                       $dbw->onTransactionResolution( function () use ( $backend, $lockPaths, $logger ) {
-                               $status = $backend->unlockFiles( $lockPaths, LockManager::LOCK_EX );
+                       $dbw->onTransactionResolution( function () use ( $logger ) {
+                               $status = $this->releaseFileLock();
                                if ( !$status->isGood() ) {
                                        $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
                                }
@@ -1959,10 +2024,12 @@ class LocalFile extends File {
        /**
         * Decrement the lock reference count and end the atomic section if it reaches zero
         *
+        * This method should not be used outside of LocalFile/LocalFile*Batch
+        *
         * The commit and loc release will happen when no atomic sections are active, which
         * may happen immediately or at some point after calling this
         */
-       function unlock() {
+       public function unlock() {
                if ( $this->locked ) {
                        --$this->locked;
                        if ( !$this->locked ) {
@@ -1973,16 +2040,6 @@ class LocalFile extends File {
                }
        }
 
-       /**
-        * Roll back the DB transaction and mark the image unlocked
-        */
-       function unlockAndRollback() {
-               $this->locked = false;
-               $dbw = $this->repo->getMasterDB();
-               $dbw->rollback( __METHOD__ );
-               $this->lockedOwnTrx = false;
-       }
-
        /**
         * @return Status
         */
@@ -2159,8 +2216,9 @@ class LocalFileDeleteBatch {
        }
 
        protected function doDBInserts() {
+               $now = time();
                $dbw = $this->file->repo->getMasterDB();
-               $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
+               $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
                $encUserId = $dbw->addQuotes( $this->user->getId() );
                $encReason = $dbw->addQuotes( $this->reason );
                $encGroup = $dbw->addQuotes( 'deleted' );
@@ -2182,15 +2240,15 @@ class LocalFileDeleteBatch {
                }
 
                if ( $deleteCurrent ) {
-                       $concat = $dbw->buildConcat( [ "img_sha1", $encExt ] );
-                       $where = [ 'img_name' => $this->file->getName() ];
-                       $dbw->insertSelect( 'filearchive', 'image',
+                       $dbw->insertSelect(
+                               'filearchive',
+                               'image',
                                [
                                        'fa_storage_group' => $encGroup,
                                        'fa_storage_key' => $dbw->conditional(
                                                [ 'img_sha1' => '' ],
                                                $dbw->addQuotes( '' ),
-                                               $concat
+                                               $dbw->buildConcat( [ "img_sha1", $encExt ] )
                                        ),
                                        'fa_deleted_user' => $encUserId,
                                        'fa_deleted_timestamp' => $encTimestamp,
@@ -2211,44 +2269,56 @@ class LocalFileDeleteBatch {
                                        'fa_user' => 'img_user',
                                        'fa_user_text' => 'img_user_text',
                                        'fa_timestamp' => 'img_timestamp',
-                                       'fa_sha1' => 'img_sha1',
-                               ], $where, __METHOD__ );
+                                       'fa_sha1' => 'img_sha1'
+                               ],
+                               [ 'img_name' => $this->file->getName() ],
+                               __METHOD__
+                       );
                }
 
                if ( count( $oldRels ) ) {
-                       $concat = $dbw->buildConcat( [ "oi_sha1", $encExt ] );
-                       $where = [
-                               'oi_name' => $this->file->getName(),
-                               'oi_archive_name' => array_keys( $oldRels ) ];
-                       $dbw->insertSelect( 'filearchive', 'oldimage',
+                       $res = $dbw->select(
+                               'oldimage',
+                               OldLocalFile::selectFields(),
                                [
-                                       'fa_storage_group' => $encGroup,
-                                       'fa_storage_key' => $dbw->conditional(
-                                               [ 'oi_sha1' => '' ],
-                                               $dbw->addQuotes( '' ),
-                                               $concat
-                                       ),
-                                       'fa_deleted_user' => $encUserId,
-                                       'fa_deleted_timestamp' => $encTimestamp,
-                                       'fa_deleted_reason' => $encReason,
-                                       'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
-
-                                       'fa_name' => 'oi_name',
-                                       'fa_archive_name' => 'oi_archive_name',
-                                       'fa_size' => 'oi_size',
-                                       'fa_width' => 'oi_width',
-                                       'fa_height' => 'oi_height',
-                                       'fa_metadata' => 'oi_metadata',
-                                       'fa_bits' => 'oi_bits',
-                                       'fa_media_type' => 'oi_media_type',
-                                       'fa_major_mime' => 'oi_major_mime',
-                                       'fa_minor_mime' => 'oi_minor_mime',
-                                       'fa_description' => 'oi_description',
-                                       'fa_user' => 'oi_user',
-                                       'fa_user_text' => 'oi_user_text',
-                                       'fa_timestamp' => 'oi_timestamp',
-                                       'fa_sha1' => 'oi_sha1',
-                               ], $where, __METHOD__ );
+                                       'oi_name' => $this->file->getName(),
+                                       'oi_archive_name' => array_keys( $oldRels )
+                               ],
+                               __METHOD__,
+                               [ 'FOR UPDATE' ]
+                       );
+                       $rowsInsert = [];
+                       foreach ( $res as $row ) {
+                               $rowsInsert[] = [
+                                       // Deletion-specific fields
+                                       'fa_storage_group' => 'deleted',
+                                       'fa_storage_key' => ( $row->oi_sha1 === '' )
+                                               ? ''
+                                               : "{$row->oi_sha1}{$dotExt}",
+                                       'fa_deleted_user' => $this->user->getId(),
+                                       'fa_deleted_timestamp' => $dbw->timestamp( $now ),
+                                       'fa_deleted_reason' => $this->reason,
+                                       // Counterpart fields
+                                       'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
+                                       'fa_name' => $row->oi_name,
+                                       'fa_archive_name' => $row->oi_archive_name,
+                                       'fa_size' => $row->oi_size,
+                                       'fa_width' => $row->oi_width,
+                                       'fa_height' => $row->oi_height,
+                                       'fa_metadata' => $row->oi_metadata,
+                                       'fa_bits' => $row->oi_bits,
+                                       'fa_media_type' => $row->oi_media_type,
+                                       'fa_major_mime' => $row->oi_major_mime,
+                                       'fa_minor_mime' => $row->oi_minor_mime,
+                                       'fa_description' => $row->oi_description,
+                                       'fa_user' => $row->oi_user,
+                                       'fa_user_text' => $row->oi_user_text,
+                                       'fa_timestamp' => $row->oi_timestamp,
+                                       'fa_sha1' => $row->oi_sha1
+                               ];
+                       }
+
+                       $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
                }
        }
 
@@ -2539,8 +2609,9 @@ class LocalFileRestoreBatch {
 
                                // The live (current) version cannot be hidden!
                                if ( !$this->unsuppress && $row->fa_deleted ) {
-                                       $storeBatch[] = [ $deletedUrl, 'public', $destRel ];
-                                       $this->cleanupBatch[] = $row->fa_storage_key;
+                                       $status->fatal( 'undeleterevdel' );
+                                       $this->file->unlock();
+                                       return $status;
                                }
                        } else {
                                $archiveName = $row->fa_archive_name;
@@ -2858,33 +2929,30 @@ class LocalFileMoveBatch {
        public function execute() {
                $repo = $this->file->repo;
                $status = $repo->newGood();
+               $destFile = wfLocalFile( $this->target );
+
+               $this->file->lock(); // begin
+               $destFile->lock(); // quickly fail if destination is not available
 
                $triplets = $this->getMoveTriplets();
                $checkStatus = $this->removeNonexistentFiles( $triplets );
                if ( !$checkStatus->isGood() ) {
-                       $status->merge( $checkStatus );
+                       $destFile->unlock();
+                       $this->file->unlock();
+                       $status->merge( $checkStatus ); // couldn't talk to file backend
                        return $status;
                }
                $triplets = $checkStatus->value;
-               $destFile = wfLocalFile( $this->target );
 
-               $this->file->lock(); // begin
-               $destFile->lock(); // quickly fail if destination is not available
-               // Rename the file versions metadata in the DB.
-               // This implicitly locks the destination file, which avoids race conditions.
-               // If we moved the files from A -> C before DB updates, another process could
-               // move files from B -> C at this point, causing storeBatch() to fail and thus
-               // cleanupTarget() to trigger. It would delete the C files and cause data loss.
-               $statusDb = $this->doDBUpdates();
+               // Verify the file versions metadata in the DB.
+               $statusDb = $this->verifyDBUpdates();
                if ( !$statusDb->isGood() ) {
                        $destFile->unlock();
-                       $this->file->unlockAndRollback();
+                       $this->file->unlock();
                        $statusDb->ok = false;
 
                        return $statusDb;
                }
-               wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
-                       "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
 
                if ( !$repo->hasSha1Storage() ) {
                        // Copy the files into their new location.
@@ -2897,7 +2965,7 @@ class LocalFileMoveBatch {
                                // Delete any files copied over (while the destination is still locked)
                                $this->cleanupTarget( $triplets );
                                $destFile->unlock();
-                               $this->file->unlockAndRollback(); // unlocks the destination
+                               $this->file->unlock();
                                wfDebugLog( 'imagemove', "Error in moving files: "
                                        . $statusMove->getWikiText( false, false, 'en' ) );
                                $statusMove->ok = false;
@@ -2907,6 +2975,12 @@ class LocalFileMoveBatch {
                        $status->merge( $statusMove );
                }
 
+               // Rename the file versions metadata in the DB.
+               $this->doDBUpdates();
+
+               wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
+                       "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
+
                $destFile->unlock();
                $this->file->unlock(); // done
 
@@ -2919,33 +2993,62 @@ class LocalFileMoveBatch {
        }
 
        /**
-        * Do the database updates and return a new FileRepoStatus indicating how
-        * many rows where updated.
+        * Verify the database updates and return a new FileRepoStatus indicating how
+        * many rows would be updated.
         *
         * @return FileRepoStatus
         */
-       protected function doDBUpdates() {
+       protected function verifyDBUpdates() {
                $repo = $this->file->repo;
                $status = $repo->newGood();
                $dbw = $this->db;
 
-               // Update current image
-               $dbw->update(
+               $hasCurrent = $dbw->selectField(
                        'image',
-                       [ 'img_name' => $this->newName ],
+                       '1',
                        [ 'img_name' => $this->oldName ],
-                       __METHOD__
+                       __METHOD__,
+                       [ 'FOR UPDATE' ]
+               );
+               $oldRowCount = $dbw->selectField(
+                       'oldimage',
+                       'COUNT(*)',
+                       [ 'oi_name' => $this->oldName ],
+                       __METHOD__,
+                       [ 'FOR UPDATE' ]
                );
 
-               if ( $dbw->affectedRows() ) {
+               if ( $hasCurrent ) {
                        $status->successCount++;
                } else {
                        $status->failCount++;
-                       $status->fatal( 'imageinvalidfilename' );
-
-                       return $status;
                }
+               $status->successCount += $oldRowCount;
+               // Bug 34934: oldCount is based on files that actually exist.
+               // There may be more DB rows than such files, in which case $affected
+               // can be greater than $total. We use max() to avoid negatives here.
+               $status->failCount += max( 0, $this->oldCount - $oldRowCount );
+               if ( $status->failCount ) {
+                       $status->error( 'imageinvalidfilename' );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Do the database updates and return a new FileRepoStatus indicating how
+        * many rows where updated.
+        */
+       protected function doDBUpdates() {
+               $dbw = $this->db;
 
+               // Update current image
+               $dbw->update(
+                       'image',
+                       [ 'img_name' => $this->newName ],
+                       [ 'img_name' => $this->oldName ],
+                       __METHOD__
+               );
                // Update old images
                $dbw->update(
                        'oldimage',
@@ -2957,19 +3060,6 @@ class LocalFileMoveBatch {
                        [ 'oi_name' => $this->oldName ],
                        __METHOD__
                );
-
-               $affected = $dbw->affectedRows();
-               $total = $this->oldCount;
-               $status->successCount += $affected;
-               // Bug 34934: $total is based on files that actually exist.
-               // There may be more DB rows than such files, in which case $affected
-               // can be greater than $total. We use max() to avoid negatives here.
-               $status->failCount += max( 0, $total - $affected );
-               if ( $status->failCount ) {
-                       $status->error( 'imageinvalidfilename' );
-               }
-
-               return $status;
        }
 
        /**