3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
21 namespace MediaWiki\Installer
;
24 use CleanupEmptyCategories
;
25 use DeleteDefaultMessages
;
27 use MediaWiki\HookContainer\HookContainer
;
28 use MediaWiki\HookContainer\HookRunner
;
29 use MediaWiki\HookContainer\StaticHookRegistry
;
30 use MediaWiki\Maintenance\FakeMaintenance
;
31 use MediaWiki\Maintenance\Maintenance
;
32 use MediaWiki\MediaWikiServices
;
33 use MediaWiki\Registration\ExtensionRegistry
;
34 use MediaWiki\ResourceLoader\MessageBlobStore
;
35 use MediaWiki\SiteStats\SiteStatsInit
;
36 use MigrateLinksTable
;
37 use RebuildLocalisationCache
;
38 use RefreshImageMetadata
;
40 use UnexpectedValueException
;
42 use Wikimedia\Rdbms\IDatabase
;
43 use Wikimedia\Rdbms\IMaintainableDatabase
;
44 use Wikimedia\Rdbms\LBFactory
;
45 use Wikimedia\Rdbms\Platform\ISQLPlatform
;
47 require_once __DIR__
. '/../../maintenance/Maintenance.php';
50 * Apply database changes after updating MediaWiki.
55 abstract class DatabaseUpdater
{
56 public const REPLICATION_WAIT_TIMEOUT
= 300;
59 * Array of updates to perform on the database
63 protected $updates = [];
66 * Array of updates that were skipped
70 protected $updatesSkipped = [];
73 * List of extension-provided database updates
76 protected $extensionUpdates = [];
79 * List of extension-provided database updates on virtual domain dbs
82 protected $extensionUpdatesWithVirtualDomains = [];
85 * Handle to the database subclass
87 * @var IMaintainableDatabase
94 protected $maintenance;
97 protected $shared = false;
99 /** @var HookContainer|null */
100 protected $autoExtensionHookContainer;
103 * @var string[] Scripts to run after database update
104 * Should be a subclass of LoggedUpdateMaintenance
106 protected $postDatabaseUpdateMaintenance = [
107 DeleteDefaultMessages
::class,
108 CleanupEmptyCategories
::class,
112 * File handle for SQL output.
116 protected $fileHandle = null;
119 * Flag specifying whether to skip schema (e.g., SQL-only) updates.
123 protected $skipSchema = false;
126 * The virtual domain currently being acted on
129 private $currentVirtualDomain = null;
132 * @param IMaintainableDatabase &$db To perform updates on
133 * @param bool $shared Whether to perform updates on shared tables
134 * @param Maintenance|null $maintenance Maintenance object which created us
136 protected function __construct(
137 IMaintainableDatabase
&$db,
139 ?Maintenance
$maintenance = null
142 $this->db
->setFlag( DBO_DDLMODE
);
143 $this->shared
= $shared;
144 if ( $maintenance ) {
145 $this->maintenance
= $maintenance;
146 $this->fileHandle
= $maintenance->fileHandle
;
148 $this->maintenance
= new FakeMaintenance
;
150 $this->maintenance
->setDB( $db );
154 * Cause extensions to register any updates they need to perform.
156 private function loadExtensionSchemaUpdates() {
157 $hookContainer = $this->loadExtensions();
158 ( new HookRunner( $hookContainer ) )->onLoadExtensionSchemaUpdates( $this );
162 * Loads LocalSettings.php, if needed, and initialises everything needed for
163 * LoadExtensionSchemaUpdates hook.
165 * @return HookContainer
167 private function loadExtensions() {
168 if ( $this->autoExtensionHookContainer
) {
169 // Already injected by installer
170 return $this->autoExtensionHookContainer
;
172 if ( defined( 'MW_EXTENSIONS_LOADED' ) ) {
173 throw new LogicException( __METHOD__
.
174 ' apparently called from installer but no hook container was injected' );
176 if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
177 // Running under update.php: use the global locator
178 return MediaWikiServices
::getInstance()->getHookContainer();
180 $vars = Installer
::getExistingLocalSettings();
182 $registry = ExtensionRegistry
::getInstance();
183 $queue = $registry->getQueue();
184 // Don't accidentally load extensions in the future
185 $registry->clearQueue();
187 // Read extension.json files
188 $extInfo = $registry->readFromQueue( $queue );
190 // Merge extension attribute hooks with hooks defined by a .php
191 // registration file included from LocalSettings.php
192 $legacySchemaHooks = $extInfo['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ??
[];
193 if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
194 $legacySchemaHooks = array_merge( $legacySchemaHooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] );
197 // Register classes defined by extensions that are loaded by including of a file that
198 // updates global variables, rather than having an extension.json manifest.
199 if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) {
200 AutoLoader
::registerClasses( $vars['wgAutoloadClasses'] );
203 // Register class definitions from extension.json files
204 if ( !isset( $extInfo['autoloaderPaths'] )
205 ||
!isset( $extInfo['autoloaderClasses'] )
206 ||
!isset( $extInfo['autoloaderNS'] )
208 // NOTE: protect against changes to the structure of $extInfo.
209 // It's volatile, and this usage is easy to miss.
210 throw new LogicException( 'Missing autoloader keys from extracted extension info' );
212 AutoLoader
::loadFiles( $extInfo['autoloaderPaths'] );
213 AutoLoader
::registerClasses( $extInfo['autoloaderClasses'] );
214 AutoLoader
::registerNamespaces( $extInfo['autoloaderNS'] );
216 $legacyHooks = $legacySchemaHooks ?
[ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ] : [];
217 return new HookContainer(
218 new StaticHookRegistry(
220 $extInfo['attributes']['Hooks'] ??
[],
221 $extInfo['attributes']['DeprecatedHooks'] ??
[]
223 MediaWikiServices
::getInstance()->getObjectFactory()
228 * @param IMaintainableDatabase $db
229 * @param bool $shared
230 * @param Maintenance|null $maintenance
231 * @return DatabaseUpdater
233 public static function newForDB(
234 IMaintainableDatabase
$db,
236 ?Maintenance
$maintenance = null
238 $type = $db->getType();
239 if ( in_array( $type, Installer
::getDBTypes() ) ) {
240 $class = '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Updater';
242 return new $class( $db, $shared, $maintenance );
245 throw new UnexpectedValueException( __METHOD__
. ' called for unsupported DB type' );
249 * Set the HookContainer to use for loading extension schema updates.
251 * @internal For use by DatabaseInstaller
253 * @param HookContainer $hookContainer
255 public function setAutoExtensionHookContainer( HookContainer
$hookContainer ) {
256 $this->autoExtensionHookContainer
= $hookContainer;
260 * Get a database connection to run updates
262 * @return IMaintainableDatabase
264 public function getDB() {
269 * Output some text. If we're running via the web, escape the text first.
271 * @param string $str Text to output
272 * @param-taint $str escapes_html
274 public function output( $str ) {
275 if ( $this->maintenance
->isQuiet() ) {
278 if ( MW_ENTRY_POINT
!== 'cli' ) {
279 $str = htmlspecialchars( $str );
286 * Add a new update coming from an extension.
287 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
291 * @param array $update The update to run. Format is [ $callback, $params... ]
292 * $callback is the method to call; either a DatabaseUpdater method name or a callable.
293 * Must be serializable (i.e., no anonymous functions allowed). The rest of the parameters
294 * (if any) will be passed to the callback. The first parameter passed to the callback
295 * is always this object.
297 public function addExtensionUpdate( array $update ) {
298 $this->extensionUpdates
[] = $update;
302 * Add a new update coming from an extension on virtual domain databases.
303 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
307 * @param array $update The update to run. The format is [ $virtualDomain, $callback, $params... ]
308 * similarly to addExtensionUpdate()
310 public function addExtensionUpdateOnVirtualDomain( array $update ) {
311 $this->extensionUpdatesWithVirtualDomains
[] = $update;
315 * Convenience wrapper for addExtensionUpdate() when adding a new table (which
316 * is the most common usage of updaters in an extension)
317 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
321 * @param string $tableName Name of table to create
322 * @param string $sqlPath Full path to the schema file
324 public function addExtensionTable( $tableName, $sqlPath ) {
325 $this->extensionUpdates
[] = [ 'addTable', $tableName, $sqlPath, true ];
329 * Add an index to an existing extension table.
330 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
334 * @param string $tableName
335 * @param string $indexName
336 * @param string $sqlPath
338 public function addExtensionIndex( $tableName, $indexName, $sqlPath ) {
339 $this->extensionUpdates
[] = [ 'addIndex', $tableName, $indexName, $sqlPath, true ];
343 * Add a field to an existing extension table.
344 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
348 * @param string $tableName
349 * @param string $columnName
350 * @param string $sqlPath
352 public function addExtensionField( $tableName, $columnName, $sqlPath ) {
353 $this->extensionUpdates
[] = [ 'addField', $tableName, $columnName, $sqlPath, true ];
357 * Drop a field from an extension table.
358 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
362 * @param string $tableName
363 * @param string $columnName
364 * @param string $sqlPath
366 public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
367 $this->extensionUpdates
[] = [ 'dropField', $tableName, $columnName, $sqlPath, true ];
371 * Drop an index from an extension table
372 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
376 * @param string $tableName
377 * @param string $indexName
378 * @param string $sqlPath The path to the SQL change path
380 public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
381 $this->extensionUpdates
[] = [ 'dropIndex', $tableName, $indexName, $sqlPath, true ];
385 * Drop an extension table.
386 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
390 * @param string $tableName
391 * @param string|bool $sqlPath
393 public function dropExtensionTable( $tableName, $sqlPath = false ) {
394 $this->extensionUpdates
[] = [ 'dropTable', $tableName, $sqlPath, true ];
398 * Rename an index on an extension table
399 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
403 * @param string $tableName
404 * @param string $oldIndexName
405 * @param string $newIndexName
406 * @param string $sqlPath The path to the SQL change file
407 * @param bool $skipBothIndexExistWarning Whether to warn if both the old
408 * and the new indexes exist. [facultative; by default, false]
410 public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
411 $sqlPath, $skipBothIndexExistWarning = false
413 $this->extensionUpdates
[] = [
418 $skipBothIndexExistWarning,
425 * Modify an existing field in an extension table.
426 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
430 * @param string $tableName
431 * @param string $fieldName The field to be modified
432 * @param string $sqlPath The path to the SQL patch
434 public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
435 $this->extensionUpdates
[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
439 * Modify an existing extension table.
440 * Intended for use in LoadExtensionSchemaUpdates hook handlers.
444 * @param string $tableName
445 * @param string $sqlPath The path to the SQL patch
447 public function modifyExtensionTable( $tableName, $sqlPath ) {
448 $this->extensionUpdates
[] = [ 'modifyTable', $tableName, $sqlPath, true ];
454 * @param string $tableName
457 public function tableExists( $tableName ) {
458 return ( $this->db
->tableExists( $tableName, __METHOD__
) );
464 * @param string $tableName
465 * @param string $fieldName
468 public function fieldExists( $tableName, $fieldName ) {
469 return ( $this->db
->fieldExists( $tableName, $fieldName, __METHOD__
) );
473 * Add a maintenance script to be run after the database updates are complete.
475 * Script should subclass LoggedUpdateMaintenance
479 * @param string $class Name of a Maintenance subclass
481 public function addPostDatabaseUpdateMaintenance( $class ) {
482 $this->postDatabaseUpdateMaintenance
[] = $class;
486 * Get the list of extension-defined updates
490 protected function getExtensionUpdates() {
491 return $this->extensionUpdates
;
499 public function getPostDatabaseUpdateMaintenance() {
500 return $this->postDatabaseUpdateMaintenance
;
506 * Writes the schema updates desired to a file for the DB Admin to run.
508 private function writeSchemaUpdateFile() {
509 $updates = $this->updatesSkipped
;
510 $this->updatesSkipped
= [];
512 foreach ( $updates as [ $func, $args, $origParams ] ) {
513 // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
516 $this->updatesSkipped
[] = $origParams;
521 * Get appropriate schema variables in the current database connection.
523 * This should be called after any request data has been imported, but before
524 * any write operations to the database. The result should be passed to the DB
525 * setSchemaVars() method.
530 public function getSchemaVars() {
531 return []; // DB-type specific
537 * @param array $what What updates to perform
539 public function doUpdates( array $what = [ 'core', 'extensions', 'stats' ] ) {
540 $this->db
->setSchemaVars( $this->getSchemaVars() );
542 $what = array_fill_keys( $what, true );
543 $this->skipSchema
= isset( $what['noschema'] ) ||
$this->fileHandle
!== null;
545 if ( isset( $what['initial'] ) ) {
546 $this->output( 'Inserting initial update keys...' );
547 $this->insertInitialUpdateKeys();
548 $this->output( "done.\n" );
550 if ( isset( $what['core'] ) ) {
551 $this->doCollationUpdate();
552 $this->runUpdates( $this->getCoreUpdateList(), false );
554 if ( isset( $what['extensions'] ) ) {
555 $this->loadExtensionSchemaUpdates();
556 $this->runUpdates( $this->getExtensionUpdates(), true );
557 $this->runUpdates( $this->extensionUpdatesWithVirtualDomains
, true, true );
560 if ( isset( $what['stats'] ) ) {
564 if ( $this->fileHandle
) {
565 $this->skipSchema
= false;
566 $this->writeSchemaUpdateFile();
571 * Helper function for doUpdates()
573 * @param array $updates Array of updates to run
574 * @param bool $passSelf Whether to pass this object when calling external functions
575 * @param bool $hasVirtualDomain Whether the updates' array include virtual domains
577 private function runUpdates( array $updates, $passSelf, $hasVirtualDomain = false ) {
578 $lbFactory = $this->getLBFactory();
580 $updatesSkipped = [];
581 foreach ( $updates as $params ) {
582 $origParams = $params;
584 $this->currentVirtualDomain
= null;
585 if ( $hasVirtualDomain === true ) {
586 $this->currentVirtualDomain
= array_shift( $params );
588 $virtualDb = $lbFactory->getPrimaryDatabase( $this->currentVirtualDomain
);
589 '@phan-var IMaintainableDatabase $virtualDb';
590 $this->maintenance
->setDB( $virtualDb );
591 $this->db
= $virtualDb;
593 $func = array_shift( $params );
594 if ( !is_array( $func ) && method_exists( $this, $func ) ) {
595 $func = [ $this, $func ];
596 } elseif ( $passSelf ) {
597 array_unshift( $params, $this );
599 $ret = $func( ...$params );
600 if ( $hasVirtualDomain === true && $oldDb ) {
602 $this->maintenance
->setDB( $oldDb );
603 $this->currentVirtualDomain
= null;
607 if ( $ret !== false ) {
608 $updatesDone[] = $origParams;
609 $lbFactory->waitForReplication( [ 'timeout' => self
::REPLICATION_WAIT_TIMEOUT
] );
611 if ( $hasVirtualDomain === true ) {
612 $params = $origParams;
613 $func = array_shift( $params );
615 $updatesSkipped[] = [ $func, $params, $origParams ];
618 $this->updatesSkipped
= array_merge( $this->updatesSkipped
, $updatesSkipped );
619 $this->updates
= array_merge( $this->updates
, $updatesDone );
622 private function getLBFactory(): LBFactory
{
623 return MediaWikiServices
::getInstance()->getDBLoadBalancerFactory();
627 * Helper function: check if the given key is present in the updatelog table.
629 * @param string $key Name of the key to check for
632 public function updateRowExists( $key ) {
633 // Return false if the updatelog table does not exist. This can occur if performing schema changes for tables
634 // that are on a virtual database domain.
635 if ( !$this->db
->tableExists( 'updatelog', __METHOD__
) ) {
639 $row = $this->db
->newSelectQueryBuilder()
640 ->select( '1 AS X' ) // T67813
641 ->from( 'updatelog' )
642 ->where( [ 'ul_key' => $key ] )
643 ->caller( __METHOD__
)->fetchRow();
649 * Helper function: Add a key to the updatelog table
651 * @note Extensions must only use this from within callbacks registered with
652 * addExtensionUpdate(). In particular, this method must not be called directly
653 * from a LoadExtensionSchemaUpdates handler.
655 * @param string $key Name of the key to insert
656 * @param string|null $val [optional] Value to insert along with the key
658 public function insertUpdateRow( $key, $val = null ) {
659 // We cannot insert anything to the updatelog table if it does not exist. This can occur for schema changes
660 // on tables that are on a virtual database domain.
661 if ( !$this->db
->tableExists( 'updatelog', __METHOD__
) ) {
665 $this->db
->clearFlag( DBO_DDLMODE
);
666 $values = [ 'ul_key' => $key ];
668 $values['ul_value'] = $val;
670 $this->db
->newInsertQueryBuilder()
671 ->insertInto( 'updatelog' )
674 ->caller( __METHOD__
)->execute();
675 $this->db
->setFlag( DBO_DDLMODE
);
679 * Add initial keys to the updatelog table. Should be called during installation.
681 public function insertInitialUpdateKeys() {
682 $this->db
->clearFlag( DBO_DDLMODE
);
683 $iqb = $this->db
->newInsertQueryBuilder()
684 ->insertInto( 'updatelog' )
686 ->caller( __METHOD__
);
687 foreach ( $this->getInitialUpdateKeys() as $key ) {
688 $iqb->row( [ 'ul_key' => $key ] );
691 $this->db
->setFlag( DBO_DDLMODE
);
695 * Returns whether updates should be executed on the database table $name.
696 * Updates will be prevented if the table is a shared table, and it is not
697 * specified to run updates on shared tables.
699 * @param string $name Table name
702 protected function doTable( $name ) {
703 global $wgSharedDB, $wgSharedTables;
705 if ( $this->shared
) {
706 // Shared updates are enabled
709 if ( $this->currentVirtualDomain
710 && $this->getLBFactory()->isSharedVirtualDomain( $this->currentVirtualDomain
)
712 $this->output( "...skipping update to table $name in shared virtual domain.\n" );
715 if ( $wgSharedDB !== null && in_array( $name, $wgSharedTables ) ) {
716 $this->output( "...skipping update to shared table $name.\n" );
724 * Get an array of updates to perform on the database. Should return a
725 * multidimensional array. The main key is the MediaWiki version (1.12,
726 * 1.13...) with the values being arrays of updates.
730 abstract protected function getCoreUpdateList();
733 * Get an array of update keys to insert into the updatelog table after a
734 * new installation. The named operations will then be skipped by a
737 * Add keys here to skip updates that are redundant or harmful on a new
738 * installation, for example reducing field sizes, adding constraints, etc.
742 abstract protected function getInitialUpdateKeys();
745 * Append an SQL fragment to the open file handle.
747 * @note protected since 1.35
749 * @param string $filename File name to open
751 protected function copyFile( $filename ) {
752 $this->db
->sourceFile(
758 return $this->appendLine( $line );
764 * Append a line to the open file handle. The line is assumed to
765 * be a complete SQL statement.
767 * This is used as a callback for sourceLine().
769 * @note protected since 1.35
771 * @param string $line Text to append to the file
772 * @return bool False to skip actually executing the file
774 protected function appendLine( $line ) {
775 $line = rtrim( $line ) . ";\n";
776 if ( fwrite( $this->fileHandle
, $line ) === false ) {
777 throw new RuntimeException( "trouble writing file" );
784 * Applies a SQL patch
786 * @note Do not use this in a LoadExtensionSchemaUpdates handler,
787 * use addExtensionUpdate instead!
789 * @param string $path Path to the patch file
790 * @param bool $isFullPath Whether to treat $path as a relative or not
791 * @param string|null $msg Description of the patch
792 * @return bool False if the patch was skipped.
794 protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
795 $msg ??
= "Applying $path patch";
796 if ( $this->skipSchema
) {
797 $this->output( "...skipping schema change ($msg).\n" );
802 $this->output( "{$msg}..." );
804 if ( !$isFullPath ) {
805 $path = $this->patchPath( $this->db
, $path );
807 if ( $this->fileHandle
!== null ) {
808 $this->copyFile( $path );
810 $this->db
->sourceFile( $path );
812 $this->output( "done.\n" );
818 * Get the full path to a patch file.
820 * @param IDatabase $db
821 * @param string $patch The basename of the patch, like patch-something.sql
822 * @return string Full path to patch file. It fails back to MySQL
823 * if no DB-specific patch exists.
825 public function patchPath( IDatabase
$db, $patch ) {
826 $baseDir = MW_INSTALL_PATH
;
828 $dbType = $db->getType();
829 if ( file_exists( "$baseDir/sql/$dbType/$patch" ) ) {
830 return "$baseDir/sql/$dbType/$patch";
833 // TODO: Is the fallback still needed after the changes from T382030?
834 return "$baseDir/sql/mysql/$patch";
838 * Add a new table to the database
840 * @note Code in a LoadExtensionSchemaUpdates handler should
841 * use addExtensionTable instead!
843 * @param string $name Name of the new table
844 * @param string $patch Path to the patch file
845 * @param bool $fullpath Whether to treat $patch path as a relative or not
846 * @return bool False if this was skipped because schema changes are skipped
848 protected function addTable( $name, $patch, $fullpath = false ) {
849 if ( !$this->doTable( $name ) ) {
853 if ( $this->db
->tableExists( $name, __METHOD__
) ) {
854 $this->output( "...$name table already exists.\n" );
858 return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
862 * Add a new field to an existing table
864 * @note Code in a LoadExtensionSchemaUpdates handler should
865 * use addExtensionField instead!
867 * @param string $table Name of the table to modify
868 * @param string $field Name of the new field
869 * @param string $patch Path to the patch file
870 * @param bool $fullpath Whether to treat $patch path as a relative or not
871 * @return bool False if this was skipped because schema changes are skipped
873 protected function addField( $table, $field, $patch, $fullpath = false ) {
874 if ( !$this->doTable( $table ) ) {
878 if ( !$this->db
->tableExists( $table, __METHOD__
) ) {
879 $this->output( "...$table table does not exist, skipping new field patch.\n" );
880 } elseif ( $this->db
->fieldExists( $table, $field, __METHOD__
) ) {
881 $this->output( "...have $field field in $table table.\n" );
883 return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
890 * Add a new index to an existing table
892 * @note Code in a LoadExtensionSchemaUpdates handler should
893 * use addExtensionIndex instead!
895 * @param string $table Name of the table to modify
896 * @param string $index Name of the new index
897 * @param string $patch Path to the patch file
898 * @param bool $fullpath Whether to treat $patch path as a relative or not
899 * @return bool False if this was skipped because schema changes are skipped
901 protected function addIndex( $table, $index, $patch, $fullpath = false ) {
902 if ( !$this->doTable( $table ) ) {
906 if ( !$this->db
->tableExists( $table, __METHOD__
) ) {
907 $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
908 } elseif ( $this->db
->indexExists( $table, $index, __METHOD__
) ) {
909 $this->output( "...index $index already set on $table table.\n" );
911 return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
918 * Drop a field from an existing table
920 * @note Code in a LoadExtensionSchemaUpdates handler should
921 * use dropExtensionField instead!
923 * @param string $table Name of the table to modify
924 * @param string $field Name of the old field
925 * @param string $patch Path to the patch file
926 * @param bool $fullpath Whether to treat $patch path as a relative or not
927 * @return bool False if this was skipped because schema changes are skipped
929 protected function dropField( $table, $field, $patch, $fullpath = false ) {
930 if ( !$this->doTable( $table ) ) {
934 if ( $this->db
->fieldExists( $table, $field, __METHOD__
) ) {
935 return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
938 $this->output( "...$table table does not contain $field field.\n" );
943 * Drop an index from an existing table
945 * @note Code in a LoadExtensionSchemaUpdates handler should
946 * use dropExtensionIndex instead!
948 * @param string $table Name of the table to modify
949 * @param string $index Name of the index
950 * @param string $patch Path to the patch file
951 * @param bool $fullpath Whether to treat $patch path as a relative or not
952 * @return bool False if this was skipped because schema changes are skipped
954 protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
955 if ( !$this->doTable( $table ) ) {
959 if ( $this->db
->indexExists( $table, $index, __METHOD__
) ) {
960 return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
963 $this->output( "...$index key doesn't exist.\n" );
968 * Rename an index from an existing table
970 * @note Code in a LoadExtensionSchemaUpdates handler should
971 * use renameExtensionIndex instead!
973 * @param string $table Name of the table to modify
974 * @param string $oldIndex Old name of the index
975 * @param string $newIndex New name of the index
976 * @param bool $skipBothIndexExistWarning Whether to warn if both the old and new indexes exist.
977 * @param string $patch Path to the patch file
978 * @param bool $fullpath Whether to treat $patch path as a relative or not
979 * @return bool False if this was skipped because schema changes are skipped
981 protected function renameIndex( $table, $oldIndex, $newIndex,
982 $skipBothIndexExistWarning, $patch, $fullpath = false
984 if ( !$this->doTable( $table ) ) {
988 // First requirement: the table must exist
989 if ( !$this->db
->tableExists( $table, __METHOD__
) ) {
990 $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
995 // Second requirement: the new index must be missing
996 if ( $this->db
->indexExists( $table, $newIndex, __METHOD__
) ) {
997 $this->output( "...index $newIndex already set on $table table.\n" );
998 if ( !$skipBothIndexExistWarning &&
999 $this->db
->indexExists( $table, $oldIndex, __METHOD__
)
1001 $this->output( "...WARNING: $oldIndex still exists, despite it has " .
1002 "been renamed into $newIndex (which also exists).\n" .
1003 " $oldIndex should be manually removed if not needed anymore.\n" );
1009 // Third requirement: the old index must exist
1010 if ( !$this->db
->indexExists( $table, $oldIndex, __METHOD__
) ) {
1011 $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
1016 // Requirements have been satisfied, the patch can be applied
1017 return $this->applyPatch(
1020 "Renaming index $oldIndex into $newIndex to table $table"
1025 * If the specified table exists, drop it, or execute the
1026 * patch if one is provided.
1028 * @note Code in a LoadExtensionSchemaUpdates handler should
1029 * use dropExtensionTable instead!
1031 * @note protected since 1.35
1033 * @param string $table Table to drop.
1034 * @param string|false $patch String of patch file that will drop the table. Default: false.
1035 * @param bool $fullpath Whether $patch is a full path. Default: false.
1036 * @return bool False if this was skipped because schema changes are skipped
1038 protected function dropTable( $table, $patch = false, $fullpath = false ) {
1039 if ( !$this->doTable( $table ) ) {
1043 if ( $this->db
->tableExists( $table, __METHOD__
) ) {
1044 $msg = "Dropping table $table";
1046 if ( $patch === false ) {
1047 $this->output( "$msg ..." );
1048 $this->db
->dropTable( $table, __METHOD__
);
1049 $this->output( "done.\n" );
1051 return $this->applyPatch( $patch, $fullpath, $msg );
1054 $this->output( "...$table doesn't exist.\n" );
1061 * Modify an existing field
1063 * @note Code in a LoadExtensionSchemaUpdates handler should
1064 * use modifyExtensionField instead!
1066 * @note protected since 1.35
1068 * @param string $table Name of the table to which the field belongs
1069 * @param string $field Name of the field to modify
1070 * @param string $patch Path to the patch file
1071 * @param bool $fullpath Whether to treat $patch path as a relative or not
1072 * @return bool False if this was skipped because schema changes are skipped
1074 protected function modifyField( $table, $field, $patch, $fullpath = false ) {
1075 return $this->modifyFieldWithCondition(
1077 static function () {
1085 * Modify an existing table, similar to modifyField. Intended for changes that
1086 * touch more than one column on a table.
1088 * @note Code in a LoadExtensionSchemaUpdates handler should
1089 * use modifyExtensionTable instead!
1091 * @note protected since 1.35
1093 * @param string $table Name of the table to modify
1094 * @param string $patch Name of the patch file to apply
1095 * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false
1096 * @return bool False if this was skipped because of schema changes being skipped
1098 protected function modifyTable( $table, $patch, $fullpath = false ) {
1099 if ( !$this->doTable( $table ) ) {
1103 $updateKey = "$table-$patch";
1104 if ( !$this->db
->tableExists( $table, __METHOD__
) ) {
1105 $this->output( "...$table table does not exist, skipping modify table patch.\n" );
1106 } elseif ( $this->updateRowExists( $updateKey ) ) {
1107 $this->output( "...table $table already modified by patch $patch.\n" );
1109 $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table with patch $patch" );
1111 $this->insertUpdateRow( $updateKey );
1119 * Modify a table if a field doesn't exist. This helps extensions to avoid
1120 * running updates on SQLite that are destructive because they don't copy
1124 * @param string $table Name of the table to which the field belongs
1125 * @param string $field Name of the field to check
1126 * @param string $patch Path to the patch file
1127 * @param bool $fullpath Whether to treat $patch path as a relative or not
1128 * @param string|null $fieldBeingModified The field being modified. If this
1129 * is specified, the updatelog key will match that used by modifyField(),
1130 * so if the patch was previously applied via modifyField(), it won't be
1131 * applied again. Also, if the field doesn't exist, the patch will not be
1132 * applied. If this is null, the updatelog key will match that used by
1134 * @return bool False if this was skipped because schema changes are skipped
1136 protected function modifyTableIfFieldNotExists( $table, $field, $patch, $fullpath = false,
1137 $fieldBeingModified = null
1139 if ( !$this->doTable( $table ) ) {
1143 if ( $fieldBeingModified === null ) {
1144 $updateKey = "$table-$patch";
1146 $updateKey = "$table-$fieldBeingModified-$patch";
1149 if ( !$this->db
->tableExists( $table, __METHOD__
) ) {
1150 $this->output( "...$table table does not exist, skipping patch $patch.\n" );
1151 } elseif ( $this->db
->fieldExists( $table, $field, __METHOD__
) ) {
1152 $this->output( "...$field field exists in $table table, skipping obsolete patch $patch.\n" );
1153 } elseif ( $fieldBeingModified !== null
1154 && !$this->db
->fieldExists( $table, $fieldBeingModified, __METHOD__
)
1156 $this->output( "...$fieldBeingModified field does not exist in $table table, " .
1157 "skipping patch $patch.\n" );
1158 } elseif ( $this->updateRowExists( $updateKey ) ) {
1159 $this->output( "...table $table already modified by patch $patch.\n" );
1161 $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table with patch $patch" );
1163 $this->insertUpdateRow( $updateKey );
1171 * Modify a field if the field exists and is nullable
1174 * @param string $table Name of the table to which the field belongs
1175 * @param string $field Name of the field to modify
1176 * @param string $patch Path to the patch file
1177 * @param bool $fullpath Whether to treat $patch path as a relative or not
1178 * @return bool False if this was skipped because schema changes are skipped
1180 protected function modifyFieldIfNullable( $table, $field, $patch, $fullpath = false ) {
1181 return $this->modifyFieldWithCondition(
1183 static function ( $fieldInfo ) {
1184 return $fieldInfo->isNullable();
1192 * Modify a field if a field exists and a callback returns true. The callback
1193 * is called with the FieldInfo of the field in question.
1196 * @param string $table Name of the table to modify
1197 * @param string $field Name of the field to modify
1198 * @param callable $condCallback A callback which will be called with the
1199 * \Wikimedia\Rdbms\Field object for the specified field. If the callback returns
1200 * true, the update will proceed.
1201 * @param string $patch Name of the patch file to apply
1202 * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false
1203 * @return bool False if this was skipped because of schema changes being skipped
1205 private function modifyFieldWithCondition(
1206 $table, $field, $condCallback, $patch, $fullpath = false
1208 if ( !$this->doTable( $table ) ) {
1212 $updateKey = "$table-$field-$patch";
1213 if ( !$this->db
->tableExists( $table, __METHOD__
) ) {
1214 $this->output( "...$table table does not exist, skipping modify field patch.\n" );
1217 $fieldInfo = $this->db
->fieldInfo( $table, $field );
1218 if ( !$fieldInfo ) {
1219 $this->output( "...$field field does not exist in $table table, " .
1220 "skipping modify field patch.\n" );
1223 if ( $this->updateRowExists( $updateKey ) ) {
1224 $this->output( "...$field in table $table already modified by patch $patch.\n" );
1227 if ( !$condCallback( $fieldInfo ) ) {
1228 $this->output( "...$field in table $table already has the required properties.\n" );
1232 $apply = $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
1234 $this->insertUpdateRow( $updateKey );
1240 * Run a maintenance script
1242 * This should only be used when the maintenance script must run before
1243 * later updates. If later updates don't depend on the script, add it to
1244 * DatabaseUpdater::$postDatabaseUpdateMaintenance instead.
1246 * The script's execute() method must return true to indicate successful
1247 * completion, and must return false (or throw an exception) to indicate
1248 * unsuccessful completion.
1250 * @note Code in a LoadExtensionSchemaUpdates handler should
1251 * use addExtensionUpdate instead!
1253 * @note protected since 1.35
1256 * @param string $class Maintenance subclass
1257 * @param string $unused Unused, kept for compatibility
1259 protected function runMaintenance( $class, $unused = '' ) {
1260 $this->output( "Running $class...\n" );
1261 $task = $this->maintenance
->runChild( $class );
1262 $ok = $task->execute();
1264 throw new RuntimeException( "Execution of $class did not complete successfully." );
1266 $this->output( "done.\n" );
1270 * Set any .htaccess files or equivalent for storage repos
1272 * Some zones (e.g. "temp") used to be public and may have been initialized as such
1274 public function setFileAccess() {
1275 $repo = MediaWikiServices
::getInstance()->getRepoGroup()->getLocalRepo();
1276 $zonePath = $repo->getZonePath( 'temp' );
1277 if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) {
1278 // If the directory was never made, then it will have the right ACLs when it is made
1279 $status = $repo->getBackend()->secure( [
1284 if ( $status->isOK() ) {
1285 $this->output( "Set the local repo temp zone container to be private.\n" );
1287 $this->output( "Failed to set the local repo temp zone container to be private.\n" );
1293 * Purge various database caches
1295 public function purgeCache() {
1296 global $wgLocalisationCacheConf;
1297 // We can't guarantee that the user will be able to use TRUNCATE,
1298 // but we know that DELETE is available to us
1299 $this->output( "Purging caches..." );
1302 $this->db
->newDeleteQueryBuilder()
1303 ->deleteFrom( 'objectcache' )
1304 ->where( ISQLPlatform
::ALL_ROWS
)
1305 ->caller( __METHOD__
)
1308 // LocalisationCache
1309 if ( $wgLocalisationCacheConf['manualRecache'] ) {
1310 $this->rebuildLocalisationCache();
1313 // ResourceLoader: Message cache
1314 $services = MediaWikiServices
::getInstance();
1315 MessageBlobStore
::clearGlobalCacheEntry(
1316 $services->getMainWANObjectCache()
1319 // ResourceLoader: File-dependency cache
1320 $this->db
->newDeleteQueryBuilder()
1321 ->deleteFrom( 'module_deps' )
1322 ->where( ISQLPlatform
::ALL_ROWS
)
1323 ->caller( __METHOD__
)
1325 $this->output( "done.\n" );
1329 * Check the site_stats table is not properly populated.
1331 protected function checkStats() {
1332 $this->output( "...site_stats is populated..." );
1333 $row = $this->db
->newSelectQueryBuilder()
1335 ->from( 'site_stats' )
1336 ->where( [ 'ss_row_id' => 1 ] )
1337 ->caller( __METHOD__
)->fetchRow();
1338 if ( $row === false ) {
1339 $this->output( "data is missing! rebuilding...\n" );
1340 } elseif ( isset( $row->site_stats
) && $row->ss_total_pages
== -1 ) {
1341 $this->output( "missing ss_total_pages, rebuilding...\n" );
1343 $this->output( "done.\n" );
1347 SiteStatsInit
::doAllAndCommit( $this->db
);
1350 # Common updater functions
1353 * Update CategoryLinks collation
1355 protected function doCollationUpdate() {
1356 global $wgCategoryCollation;
1357 if ( $this->updateRowExists( 'UpdateCollation::' . $wgCategoryCollation ) ) {
1358 $this->output( "...collations up-to-date.\n" );
1361 $this->output( "Updating category collations...\n" );
1362 $task = $this->maintenance
->runChild( UpdateCollation
::class );
1363 $ok = $task->execute();
1364 if ( $ok !== false ) {
1365 $this->output( "...done.\n" );
1366 $this->insertUpdateRow( 'UpdateCollation::' . $wgCategoryCollation );
1370 protected function doConvertDjvuMetadata() {
1371 if ( $this->updateRowExists( 'ConvertDjvuMetadata' ) ) {
1374 $this->output( "Converting djvu metadata..." );
1375 $task = $this->maintenance
->runChild( RefreshImageMetadata
::class );
1376 '@phan-var RefreshImageMetadata $task';
1377 $task->loadParamsAndArgs( RefreshImageMetadata
::class, [
1379 'mediatype' => 'OFFICE',
1380 'mime' => 'image/*',
1384 $ok = $task->execute();
1385 if ( $ok !== false ) {
1386 $this->output( "...done.\n" );
1387 $this->insertUpdateRow( 'ConvertDjvuMetadata' );
1392 * Rebuilds the localisation cache
1394 protected function rebuildLocalisationCache() {
1396 * @var RebuildLocalisationCache $cl
1398 $cl = $this->maintenance
->runChild(
1399 RebuildLocalisationCache
::class, 'rebuildLocalisationCache.php'
1401 '@phan-var RebuildLocalisationCache $cl';
1402 $this->output( "Rebuilding localisation cache...\n" );
1405 $this->output( "done.\n" );
1408 protected function migrateTemplatelinks() {
1409 if ( $this->updateRowExists( MigrateLinksTable
::class . 'templatelinks' ) ) {
1410 $this->output( "...templatelinks table has already been migrated.\n" );
1414 * @var MigrateLinksTable $task
1416 $task = $this->maintenance
->runChild(
1417 MigrateLinksTable
::class, 'migrateLinksTable.php'
1419 '@phan-var MigrateLinksTable $task';
1420 $task->loadParamsAndArgs( MigrateLinksTable
::class, [
1422 'table' => 'templatelinks'
1424 $this->output( "Running migrateLinksTable.php on templatelinks...\n" );
1426 $this->output( "done.\n" );
1429 protected function migratePagelinks() {
1430 if ( $this->updateRowExists( MigrateLinksTable
::class . 'pagelinks' ) ) {
1431 $this->output( "...pagelinks table has already been migrated.\n" );
1435 * @var MigrateLinksTable $task
1437 $task = $this->maintenance
->runChild(
1438 MigrateLinksTable
::class, 'migrateLinksTable.php'
1440 '@phan-var MigrateLinksTable $task';
1441 $task->loadParamsAndArgs( MigrateLinksTable
::class, [
1443 'table' => 'pagelinks'
1445 $this->output( "Running migrateLinksTable.php on pagelinks...\n" );
1447 $this->output( "done.\n" );
1451 * Only run a function if a table does not exist
1454 * @param string $table Table to check.
1455 * If passed $this, it's assumed to be a call from runUpdates() with
1456 * $passSelf = true: all other parameters are shifted and $this is
1457 * prepended to the rest of $params.
1458 * @param string|array|static $func Normally this is the string naming the method on $this to
1459 * call. It may also be an array style callable.
1460 * @param mixed ...$params Parameters for `$func`
1461 * @return mixed Whatever $func returns, or null when skipped.
1463 protected function ifTableNotExists( $table, $func, ...$params ) {
1464 // Handle $passSelf from runUpdates().
1466 if ( $table === $this ) {
1469 $func = array_shift( $params );
1472 if ( $this->db
->tableExists( $table, __METHOD__
) ) {
1476 if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1477 $func = [ $this, $func ];
1478 } elseif ( $passSelf ) {
1479 array_unshift( $params, $this );
1482 // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1483 return $func( ...$params );
1487 * Only run a function if the named field exists
1490 * @param string $table Table to check.
1491 * If passed $this, it's assumed to be a call from runUpdates() with
1492 * $passSelf = true: all other parameters are shifted and $this is
1493 * prepended to the rest of $params.
1494 * @param string $field Field to check
1495 * @param string|array|static $func Normally this is the string naming the method on $this to
1496 * call. It may also be an array style callable.
1497 * @param mixed ...$params Parameters for `$func`
1498 * @return mixed Whatever $func returns, or null when skipped.
1500 protected function ifFieldExists( $table, $field, $func, ...$params ) {
1501 // Handle $passSelf from runUpdates().
1503 if ( $table === $this ) {
1507 $func = array_shift( $params );
1510 if ( !$this->db
->tableExists( $table, __METHOD__
) ||
1511 !$this->db
->fieldExists( $table, $field, __METHOD__
)
1516 if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1517 $func = [ $this, $func ];
1518 } elseif ( $passSelf ) {
1519 array_unshift( $params, $this );
1522 // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1523 return $func( ...$params );
1528 /** @deprecated class alias since 1.42 */
1529 class_alias( DatabaseUpdater
::class, 'DatabaseUpdater' );