MediaWiki 1.43.1
DatabaseUpdater.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Installer;
25
26use AutoLoader;
29use LogicException;
43use RuntimeException;
44use UnexpectedValueException;
49
50require_once __DIR__ . '/../../maintenance/Maintenance.php';
51
58abstract class DatabaseUpdater {
59 public const REPLICATION_WAIT_TIMEOUT = 300;
60
66 protected $updates = [];
67
73 protected $updatesSkipped = [];
74
79 protected $extensionUpdates = [];
80
86
92 protected $db;
93
97 protected $maintenance;
98
100 protected $shared = false;
101
104
110 DeleteDefaultMessages::class,
111 CleanupEmptyCategories::class,
112 ];
113
119 protected $fileHandle = null;
120
126 protected $skipSchema = false;
127
133 protected function __construct(
135 $shared,
137 ) {
138 $this->db = $db;
139 $this->db->setFlag( DBO_DDLMODE );
140 $this->shared = $shared;
141 if ( $maintenance ) {
142 $this->maintenance = $maintenance;
143 $this->fileHandle = $maintenance->fileHandle;
144 } else {
145 $this->maintenance = new FakeMaintenance;
146 }
147 $this->maintenance->setDB( $db );
148 }
149
153 private function loadExtensionSchemaUpdates() {
154 $hookContainer = $this->loadExtensions();
155 ( new HookRunner( $hookContainer ) )->onLoadExtensionSchemaUpdates( $this );
156 }
157
164 private function loadExtensions() {
165 if ( $this->autoExtensionHookContainer ) {
166 // Already injected by installer
168 }
169 if ( defined( 'MW_EXTENSIONS_LOADED' ) ) {
170 throw new LogicException( __METHOD__ .
171 ' apparently called from installer but no hook container was injected' );
172 }
173 if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
174 // Running under update.php: use the global locator
175 return MediaWikiServices::getInstance()->getHookContainer();
176 }
178
179 $registry = ExtensionRegistry::getInstance();
180 $queue = $registry->getQueue();
181 // Don't accidentally load extensions in the future
182 $registry->clearQueue();
183
184 // Read extension.json files
185 $extInfo = $registry->readFromQueue( $queue );
186
187 // Merge extension attribute hooks with hooks defined by a .php
188 // registration file included from LocalSettings.php
189 $legacySchemaHooks = $extInfo['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ?? [];
190 if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
191 $legacySchemaHooks = array_merge( $legacySchemaHooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] );
192 }
193
194 // Register classes defined by extensions that are loaded by including of a file that
195 // updates global variables, rather than having an extension.json manifest.
196 if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) {
197 AutoLoader::registerClasses( $vars['wgAutoloadClasses'] );
198 }
199
200 // Register class definitions from extension.json files
201 if ( !isset( $extInfo['autoloaderPaths'] )
202 || !isset( $extInfo['autoloaderClasses'] )
203 || !isset( $extInfo['autoloaderNS'] )
204 ) {
205 // NOTE: protect against changes to the structure of $extInfo.
206 // It's volatile, and this usage is easy to miss.
207 throw new LogicException( 'Missing autoloader keys from extracted extension info' );
208 }
209 AutoLoader::loadFiles( $extInfo['autoloaderPaths'] );
210 AutoLoader::registerClasses( $extInfo['autoloaderClasses'] );
211 AutoLoader::registerNamespaces( $extInfo['autoloaderNS'] );
212
213 return new HookContainer(
214 new StaticHookRegistry(
215 [ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ],
216 $extInfo['attributes']['Hooks'] ?? [],
217 $extInfo['attributes']['DeprecatedHooks'] ?? []
218 ),
219 MediaWikiServices::getInstance()->getObjectFactory()
220 );
221 }
222
229 public static function newForDB(
231 $shared = false,
233 ) {
234 $type = $db->getType();
235 if ( in_array( $type, Installer::getDBTypes() ) ) {
236 $class = '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Updater';
237
238 return new $class( $db, $shared, $maintenance );
239 }
240
241 throw new UnexpectedValueException( __METHOD__ . ' called for unsupported DB type' );
242 }
243
251 public function setAutoExtensionHookContainer( HookContainer $hookContainer ) {
252 $this->autoExtensionHookContainer = $hookContainer;
253 }
254
260 public function getDB() {
261 return $this->db;
262 }
263
270 public function output( $str ) {
271 if ( $this->maintenance->isQuiet() ) {
272 return;
273 }
274 if ( MW_ENTRY_POINT !== 'cli' ) {
275 $str = htmlspecialchars( $str );
276 }
277 echo $str;
278 flush();
279 }
280
293 public function addExtensionUpdate( array $update ) {
294 $this->extensionUpdates[] = $update;
295 }
296
306 public function addExtensionUpdateOnVirtualDomain( array $update ) {
307 $this->extensionUpdatesWithVirtualDomains[] = $update;
308 }
309
320 public function addExtensionTable( $tableName, $sqlPath ) {
321 $this->extensionUpdates[] = [ 'addTable', $tableName, $sqlPath, true ];
322 }
323
334 public function addExtensionIndex( $tableName, $indexName, $sqlPath ) {
335 $this->extensionUpdates[] = [ 'addIndex', $tableName, $indexName, $sqlPath, true ];
336 }
337
348 public function addExtensionField( $tableName, $columnName, $sqlPath ) {
349 $this->extensionUpdates[] = [ 'addField', $tableName, $columnName, $sqlPath, true ];
350 }
351
362 public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
363 $this->extensionUpdates[] = [ 'dropField', $tableName, $columnName, $sqlPath, true ];
364 }
365
376 public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
377 $this->extensionUpdates[] = [ 'dropIndex', $tableName, $indexName, $sqlPath, true ];
378 }
379
389 public function dropExtensionTable( $tableName, $sqlPath = false ) {
390 $this->extensionUpdates[] = [ 'dropTable', $tableName, $sqlPath, true ];
391 }
392
406 public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
407 $sqlPath, $skipBothIndexExistWarning = false
408 ) {
409 $this->extensionUpdates[] = [
410 'renameIndex',
411 $tableName,
412 $oldIndexName,
413 $newIndexName,
414 $skipBothIndexExistWarning,
415 $sqlPath,
416 true
417 ];
418 }
419
430 public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
431 $this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
432 }
433
443 public function modifyExtensionTable( $tableName, $sqlPath ) {
444 $this->extensionUpdates[] = [ 'modifyTable', $tableName, $sqlPath, true ];
445 }
446
453 public function tableExists( $tableName ) {
454 return ( $this->db->tableExists( $tableName, __METHOD__ ) );
455 }
456
464 public function fieldExists( $tableName, $fieldName ) {
465 return ( $this->db->fieldExists( $tableName, $fieldName, __METHOD__ ) );
466 }
467
477 public function addPostDatabaseUpdateMaintenance( $class ) {
478 $this->postDatabaseUpdateMaintenance[] = $class;
479 }
480
486 protected function getExtensionUpdates() {
488 }
489
498
504 private function writeSchemaUpdateFile() {
506 $this->updatesSkipped = [];
507
508 foreach ( $updates as [ $func, $args, $origParams ] ) {
509 // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
510 $func( ...$args );
511 flush();
512 $this->updatesSkipped[] = $origParams;
513 }
514 }
515
526 public function getSchemaVars() {
527 return []; // DB-type specific
528 }
529
535 public function doUpdates( array $what = [ 'core', 'extensions', 'stats' ] ) {
536 $this->db->setSchemaVars( $this->getSchemaVars() );
537
538 $what = array_fill_keys( $what, true );
539 $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
540
541 if ( isset( $what['initial'] ) ) {
542 $this->output( 'Inserting initial update keys...' );
544 $this->output( "done.\n" );
545 }
546 if ( isset( $what['core'] ) ) {
547 $this->doCollationUpdate();
548 $this->runUpdates( $this->getCoreUpdateList(), false );
549 }
550 if ( isset( $what['extensions'] ) ) {
551 $this->loadExtensionSchemaUpdates();
552 $this->runUpdates( $this->getExtensionUpdates(), true );
553 $this->runUpdates( $this->extensionUpdatesWithVirtualDomains, true, true );
554 }
555
556 if ( isset( $what['stats'] ) ) {
557 $this->checkStats();
558 }
559
560 if ( $this->fileHandle ) {
561 $this->skipSchema = false;
562 $this->writeSchemaUpdateFile();
563 }
564 }
565
573 private function runUpdates( array $updates, $passSelf, $hasVirtualDomain = false ) {
574 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
575 $updatesDone = [];
576 $updatesSkipped = [];
577 foreach ( $updates as $params ) {
578 $origParams = $params;
579 $oldDb = null;
580 $virtualDomain = null;
581 if ( $hasVirtualDomain === true ) {
582 $virtualDomain = array_shift( $params );
583 $oldDb = $this->db;
584 $virtualDb = $lbFactory->getPrimaryDatabase( $virtualDomain );
585 '@phan-var IMaintainableDatabase $virtualDb';
586 $this->maintenance->setDB( $virtualDb );
587 $this->db = $virtualDb;
588 }
589 $func = array_shift( $params );
590 if ( !is_array( $func ) && method_exists( $this, $func ) ) {
591 $func = [ $this, $func ];
592 } elseif ( $passSelf ) {
593 array_unshift( $params, $this );
594 }
595 $ret = $func( ...$params );
596 if ( $hasVirtualDomain === true && $oldDb ) {
597 $this->db = $oldDb;
598 $this->maintenance->setDB( $oldDb );
599 }
600
601 flush();
602 if ( $ret !== false ) {
603 $updatesDone[] = $origParams;
604 $lbFactory->waitForReplication( [ 'timeout' => self::REPLICATION_WAIT_TIMEOUT ] );
605 } else {
606 if ( $hasVirtualDomain === true ) {
607 $params = $origParams;
608 $func = array_shift( $params );
609 }
610 $updatesSkipped[] = [ $func, $params, $origParams ];
611 }
612 }
613 $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
614 $this->updates = array_merge( $this->updates, $updatesDone );
615 }
616
623 public function updateRowExists( $key ) {
624 // Return false if the updatelog table does not exist. This can occur if performing schema changes for tables
625 // that are on a virtual database domain.
626 if ( !$this->db->tableExists( 'updatelog', __METHOD__ ) ) {
627 return false;
628 }
629
630 $row = $this->db->newSelectQueryBuilder()
631 ->select( '1 AS X' ) // T67813
632 ->from( 'updatelog' )
633 ->where( [ 'ul_key' => $key ] )
634 ->caller( __METHOD__ )->fetchRow();
635
636 return (bool)$row;
637 }
638
649 public function insertUpdateRow( $key, $val = null ) {
650 // We cannot insert anything to the updatelog table if it does not exist. This can occur for schema changes
651 // on tables that are on a virtual database domain.
652 if ( !$this->db->tableExists( 'updatelog', __METHOD__ ) ) {
653 return;
654 }
655
656 $this->db->clearFlag( DBO_DDLMODE );
657 $values = [ 'ul_key' => $key ];
658 if ( $val ) {
659 $values['ul_value'] = $val;
660 }
661 $this->db->newInsertQueryBuilder()
662 ->insertInto( 'updatelog' )
663 ->ignore()
664 ->row( $values )
665 ->caller( __METHOD__ )->execute();
666 $this->db->setFlag( DBO_DDLMODE );
667 }
668
672 public function insertInitialUpdateKeys() {
673 $this->db->clearFlag( DBO_DDLMODE );
674 $iqb = $this->db->newInsertQueryBuilder()
675 ->insertInto( 'updatelog' )
676 ->ignore()
677 ->caller( __METHOD__ );
678 foreach ( $this->getInitialUpdateKeys() as $key ) {
679 $iqb->row( [ 'ul_key' => $key ] );
680 }
681 $iqb->execute();
682 $this->db->setFlag( DBO_DDLMODE );
683 }
684
693 protected function doTable( $name ) {
695
696 // Don't bother to check $wgSharedTables if there isn't a shared database
697 // or the user actually also wants to do updates on the shared database.
698 if ( $wgSharedDB === null || $this->shared ) {
699 return true;
700 }
701
702 if ( in_array( $name, $wgSharedTables ) ) {
703 $this->output( "...skipping update to shared table $name.\n" );
704 return false;
705 }
706
707 return true;
708 }
709
717 abstract protected function getCoreUpdateList();
718
729 abstract protected function getInitialUpdateKeys();
730
738 protected function copyFile( $filename ) {
739 $this->db->sourceFile(
740 $filename,
741 null,
742 null,
743 __METHOD__,
744 function ( $line ) {
745 return $this->appendLine( $line );
746 }
747 );
748 }
749
761 protected function appendLine( $line ) {
762 $line = rtrim( $line ) . ";\n";
763 if ( fwrite( $this->fileHandle, $line ) === false ) {
764 throw new RuntimeException( "trouble writing file" );
765 }
766
767 return false;
768 }
769
781 protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
782 $msg ??= "Applying $path patch";
783 if ( $this->skipSchema ) {
784 $this->output( "...skipping schema change ($msg).\n" );
785
786 return false;
787 }
788
789 $this->output( "{$msg}..." );
790
791 if ( !$isFullPath ) {
792 $path = $this->patchPath( $this->db, $path );
793 }
794 if ( $this->fileHandle !== null ) {
795 $this->copyFile( $path );
796 } else {
797 $this->db->sourceFile( $path );
798 }
799 $this->output( "done.\n" );
800
801 return true;
802 }
803
812 public function patchPath( IDatabase $db, $patch ) {
813 $baseDir = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::BaseDirectory );
814
815 $dbType = $db->getType();
816 if ( file_exists( "$baseDir/maintenance/$dbType/archives/$patch" ) ) {
817 return "$baseDir/maintenance/$dbType/archives/$patch";
818 }
819
820 return "$baseDir/maintenance/archives/$patch";
821 }
822
834 protected function addTable( $name, $patch, $fullpath = false ) {
835 if ( !$this->doTable( $name ) ) {
836 return true;
837 }
838
839 if ( $this->db->tableExists( $name, __METHOD__ ) ) {
840 $this->output( "...$name table already exists.\n" );
841 return true;
842 }
843
844 return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
845 }
846
859 protected function addField( $table, $field, $patch, $fullpath = false ) {
860 if ( !$this->doTable( $table ) ) {
861 return true;
862 }
863
864 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
865 $this->output( "...$table table does not exist, skipping new field patch.\n" );
866 } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
867 $this->output( "...have $field field in $table table.\n" );
868 } else {
869 return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
870 }
871
872 return true;
873 }
874
887 protected function addIndex( $table, $index, $patch, $fullpath = false ) {
888 if ( !$this->doTable( $table ) ) {
889 return true;
890 }
891
892 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
893 $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
894 } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
895 $this->output( "...index $index already set on $table table.\n" );
896 } else {
897 return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
898 }
899
900 return true;
901 }
902
915 protected function dropField( $table, $field, $patch, $fullpath = false ) {
916 if ( !$this->doTable( $table ) ) {
917 return true;
918 }
919
920 if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
921 return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
922 }
923
924 $this->output( "...$table table does not contain $field field.\n" );
925 return true;
926 }
927
940 protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
941 if ( !$this->doTable( $table ) ) {
942 return true;
943 }
944
945 if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
946 return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
947 }
948
949 $this->output( "...$index key doesn't exist.\n" );
950 return true;
951 }
952
967 protected function renameIndex( $table, $oldIndex, $newIndex,
968 $skipBothIndexExistWarning, $patch, $fullpath = false
969 ) {
970 if ( !$this->doTable( $table ) ) {
971 return true;
972 }
973
974 // First requirement: the table must exist
975 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
976 $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
977
978 return true;
979 }
980
981 // Second requirement: the new index must be missing
982 if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
983 $this->output( "...index $newIndex already set on $table table.\n" );
984 if ( !$skipBothIndexExistWarning &&
985 $this->db->indexExists( $table, $oldIndex, __METHOD__ )
986 ) {
987 $this->output( "...WARNING: $oldIndex still exists, despite it has " .
988 "been renamed into $newIndex (which also exists).\n" .
989 " $oldIndex should be manually removed if not needed anymore.\n" );
990 }
991
992 return true;
993 }
994
995 // Third requirement: the old index must exist
996 if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
997 $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
998
999 return true;
1000 }
1001
1002 // Requirements have been satisfied, the patch can be applied
1003 return $this->applyPatch(
1004 $patch,
1005 $fullpath,
1006 "Renaming index $oldIndex into $newIndex to table $table"
1007 );
1008 }
1009
1024 protected function dropTable( $table, $patch = false, $fullpath = false ) {
1025 if ( !$this->doTable( $table ) ) {
1026 return true;
1027 }
1028
1029 if ( $this->db->tableExists( $table, __METHOD__ ) ) {
1030 $msg = "Dropping table $table";
1031
1032 if ( $patch === false ) {
1033 $this->output( "$msg ..." );
1034 $this->db->dropTable( $table, __METHOD__ );
1035 $this->output( "done.\n" );
1036 } else {
1037 return $this->applyPatch( $patch, $fullpath, $msg );
1038 }
1039 } else {
1040 $this->output( "...$table doesn't exist.\n" );
1041 }
1042
1043 return true;
1044 }
1045
1060 protected function modifyField( $table, $field, $patch, $fullpath = false ) {
1061 if ( !$this->doTable( $table ) ) {
1062 return true;
1063 }
1064
1065 $updateKey = "$table-$field-$patch";
1066 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
1067 $this->output( "...$table table does not exist, skipping modify field patch.\n" );
1068 } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
1069 $this->output( "...$field field does not exist in $table table, " .
1070 "skipping modify field patch.\n" );
1071 } elseif ( $this->updateRowExists( $updateKey ) ) {
1072 $this->output( "...$field in table $table already modified by patch $patch.\n" );
1073 } else {
1074 $apply = $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
1075 if ( $apply ) {
1076 $this->insertUpdateRow( $updateKey );
1077 }
1078 return $apply;
1079 }
1080 return true;
1081 }
1082
1097 protected function modifyTable( $table, $patch, $fullpath = false ) {
1098 if ( !$this->doTable( $table ) ) {
1099 return true;
1100 }
1101
1102 $updateKey = "$table-$patch";
1103 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
1104 $this->output( "...$table table does not exist, skipping modify table patch.\n" );
1105 } elseif ( $this->updateRowExists( $updateKey ) ) {
1106 $this->output( "...table $table already modified by patch $patch.\n" );
1107 } else {
1108 $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table" );
1109 if ( $apply ) {
1110 $this->insertUpdateRow( $updateKey );
1111 }
1112 return $apply;
1113 }
1114 return true;
1115 }
1116
1137 protected function runMaintenance( $class, $unused = '' ) {
1138 $this->output( "Running $class...\n" );
1139 $task = $this->maintenance->runChild( $class );
1140 $ok = $task->execute();
1141 if ( !$ok ) {
1142 throw new RuntimeException( "Execution of $class did not complete successfully." );
1143 }
1144 $this->output( "done.\n" );
1145 }
1146
1152 public function setFileAccess() {
1153 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
1154 $zonePath = $repo->getZonePath( 'temp' );
1155 if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) {
1156 // If the directory was never made, then it will have the right ACLs when it is made
1157 $status = $repo->getBackend()->secure( [
1158 'dir' => $zonePath,
1159 'noAccess' => true,
1160 'noListing' => true
1161 ] );
1162 if ( $status->isOK() ) {
1163 $this->output( "Set the local repo temp zone container to be private.\n" );
1164 } else {
1165 $this->output( "Failed to set the local repo temp zone container to be private.\n" );
1166 }
1167 }
1168 }
1169
1173 public function purgeCache() {
1175 // We can't guarantee that the user will be able to use TRUNCATE,
1176 // but we know that DELETE is available to us
1177 $this->output( "Purging caches..." );
1178
1179 // ObjectCache
1180 $this->db->newDeleteQueryBuilder()
1181 ->deleteFrom( 'objectcache' )
1182 ->where( ISQLPlatform::ALL_ROWS )
1183 ->caller( __METHOD__ )
1184 ->execute();
1185
1186 // LocalisationCache
1187 if ( $wgLocalisationCacheConf['manualRecache'] ) {
1188 $this->rebuildLocalisationCache();
1189 }
1190
1191 // ResourceLoader: Message cache
1192 $services = MediaWikiServices::getInstance();
1194 $services->getMainWANObjectCache()
1195 );
1196
1197 // ResourceLoader: File-dependency cache
1198 $this->db->newDeleteQueryBuilder()
1199 ->deleteFrom( 'module_deps' )
1200 ->where( ISQLPlatform::ALL_ROWS )
1201 ->caller( __METHOD__ )
1202 ->execute();
1203 $this->output( "done.\n" );
1204 }
1205
1209 protected function checkStats() {
1210 $this->output( "...site_stats is populated..." );
1211 $row = $this->db->newSelectQueryBuilder()
1212 ->select( '*' )
1213 ->from( 'site_stats' )
1214 ->where( [ 'ss_row_id' => 1 ] )
1215 ->caller( __METHOD__ )->fetchRow();
1216 if ( $row === false ) {
1217 $this->output( "data is missing! rebuilding...\n" );
1218 } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) {
1219 $this->output( "missing ss_total_pages, rebuilding...\n" );
1220 } else {
1221 $this->output( "done.\n" );
1222
1223 return;
1224 }
1225 SiteStatsInit::doAllAndCommit( $this->db );
1226 }
1227
1228 # Common updater functions
1229
1233 protected function doCollationUpdate() {
1234 global $wgCategoryCollation;
1235 if ( $this->updateRowExists( 'UpdateCollation::' . $wgCategoryCollation ) ) {
1236 $this->output( "...collations up-to-date.\n" );
1237 return;
1238 }
1239 $this->output( "Updating category collations...\n" );
1240 $task = $this->maintenance->runChild( UpdateCollation::class );
1241 $ok = $task->execute();
1242 if ( $ok !== false ) {
1243 $this->output( "...done.\n" );
1244 $this->insertUpdateRow( 'UpdateCollation::' . $wgCategoryCollation );
1245 }
1246 }
1247
1248 protected function doConvertDjvuMetadata() {
1249 if ( $this->updateRowExists( 'ConvertDjvuMetadata' ) ) {
1250 return;
1251 }
1252 $this->output( "Converting djvu metadata..." );
1253 $task = $this->maintenance->runChild( RefreshImageMetadata::class );
1254 '@phan-var RefreshImageMetadata $task';
1255 $task->loadParamsAndArgs( RefreshImageMetadata::class, [
1256 'force' => true,
1257 'mediatype' => 'OFFICE',
1258 'mime' => 'image/*',
1259 'batch-size' => 1,
1260 'sleep' => 1
1261 ] );
1262 $ok = $task->execute();
1263 if ( $ok !== false ) {
1264 $this->output( "...done.\n" );
1265 $this->insertUpdateRow( 'ConvertDjvuMetadata' );
1266 }
1267 }
1268
1272 protected function rebuildLocalisationCache() {
1276 $cl = $this->maintenance->runChild(
1277 RebuildLocalisationCache::class, 'rebuildLocalisationCache.php'
1278 );
1279 '@phan-var RebuildLocalisationCache $cl';
1280 $this->output( "Rebuilding localisation cache...\n" );
1281 $cl->setForce();
1282 $cl->execute();
1283 $this->output( "done.\n" );
1284 }
1285
1286 protected function migrateTemplatelinks() {
1287 if ( $this->updateRowExists( MigrateLinksTable::class . 'templatelinks' ) ) {
1288 $this->output( "...templatelinks table has already been migrated.\n" );
1289 return;
1290 }
1294 $task = $this->maintenance->runChild(
1295 MigrateLinksTable::class, 'migrateLinksTable.php'
1296 );
1297 '@phan-var MigrateLinksTable $task';
1298 $task->loadParamsAndArgs( MigrateLinksTable::class, [
1299 'force' => true,
1300 'table' => 'templatelinks'
1301 ] );
1302 $this->output( "Running migrateLinksTable.php on templatelinks...\n" );
1303 $task->execute();
1304 $this->output( "done.\n" );
1305 }
1306
1307 protected function migratePagelinks() {
1308 if ( $this->updateRowExists( MigrateLinksTable::class . 'pagelinks' ) ) {
1309 $this->output( "...pagelinks table has already been migrated.\n" );
1310 return;
1311 }
1315 $task = $this->maintenance->runChild(
1316 MigrateLinksTable::class, 'migrateLinksTable.php'
1317 );
1318 '@phan-var MigrateLinksTable $task';
1319 $task->loadParamsAndArgs( MigrateLinksTable::class, [
1320 'force' => true,
1321 'table' => 'pagelinks'
1322 ] );
1323 $this->output( "Running migrateLinksTable.php on pagelinks...\n" );
1324 $task->execute();
1325 $this->output( "done.\n" );
1326 }
1327
1341 protected function ifTableNotExists( $table, $func, ...$params ) {
1342 // Handle $passSelf from runUpdates().
1343 $passSelf = false;
1344 if ( $table === $this ) {
1345 $passSelf = true;
1346 $table = $func;
1347 $func = array_shift( $params );
1348 }
1349
1350 if ( $this->db->tableExists( $table, __METHOD__ ) ) {
1351 return null;
1352 }
1353
1354 if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1355 $func = [ $this, $func ];
1356 } elseif ( $passSelf ) {
1357 array_unshift( $params, $this );
1358 }
1359
1360 // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1361 return $func( ...$params );
1362 }
1363
1378 protected function ifFieldExists( $table, $field, $func, ...$params ) {
1379 // Handle $passSelf from runUpdates().
1380 $passSelf = false;
1381 if ( $table === $this ) {
1382 $passSelf = true;
1383 $table = $field;
1384 $field = $func;
1385 $func = array_shift( $params );
1386 }
1387
1388 if ( !$this->db->tableExists( $table, __METHOD__ ) ||
1389 !$this->db->fieldExists( $table, $field, __METHOD__ )
1390 ) {
1391 return null;
1392 }
1393
1394 if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1395 $func = [ $this, $func ];
1396 } elseif ( $passSelf ) {
1397 array_unshift( $params, $this );
1398 }
1399
1400 // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1401 return $func( ...$params );
1402 }
1403
1404}
1405
1407class_alias( DatabaseUpdater::class, 'DatabaseUpdater' );
array $params
The job parameters.
const MW_ENTRY_POINT
Definition api.php:35
Locations of core classes Extension classes are specified with $wgAutoloadClasses.
static registerClasses(array $files)
Register a file to load the given class from.
static loadFiles(array $files)
Batch version of loadFile()
static registerNamespaces(array $dirs)
Register a directory to load the classes of a given namespace from, per PSR4.
Maintenance script to clean up empty categories in the category table.
Maintenance script that deletes all pages in the MediaWiki namespace which were last edited by "Media...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This is a simple immutable HookRegistry which can be used to set up a local HookContainer in tests an...
Class for handling database updates.
insertInitialUpdateKeys()
Add initial keys to the updatelog table.
array $extensionUpdates
List of extension-provided database updates.
dropExtensionTable( $tableName, $sqlPath=false)
Drop an extension table.
getSchemaVars()
Get appropriate schema variables in the current database connection.
applyPatch( $path, $isFullPath=false, $msg=null)
Applies a SQL patch.
checkStats()
Check the site_stats table is not properly populated.
static newForDB(IMaintainableDatabase $db, $shared=false, ?Maintenance $maintenance=null)
modifyExtensionField( $tableName, $fieldName, $sqlPath)
Modify an existing field in an extension table.
array $updates
Array of updates to perform on the database.
addExtensionField( $tableName, $columnName, $sqlPath)
Add a field to an existing extension table.
addExtensionIndex( $tableName, $indexName, $sqlPath)
Add an index to an existing extension table.
dropField( $table, $field, $patch, $fullpath=false)
Drop a field from an existing table.
copyFile( $filename)
Append an SQL fragment to the open file handle.
addField( $table, $field, $patch, $fullpath=false)
Add a new field to an existing table.
ifTableNotExists( $table, $func,... $params)
Only run a function if a table does not exist.
dropIndex( $table, $index, $patch, $fullpath=false)
Drop an index from an existing table.
patchPath(IDatabase $db, $patch)
Get the full path of a patch file.
renameIndex( $table, $oldIndex, $newIndex, $skipBothIndexExistWarning, $patch, $fullpath=false)
Rename an index from an existing table.
getDB()
Get a database connection to run updates.
appendLine( $line)
Append a line to the open file handle.
bool $skipSchema
Flag specifying whether to skip schema (e.g., SQL-only) updates.
modifyField( $table, $field, $patch, $fullpath=false)
Modify an existing field.
doCollationUpdate()
Update CategoryLinks collation.
string[] $postDatabaseUpdateMaintenance
Scripts to run after database update Should be a subclass of LoggedUpdateMaintenance.
ifFieldExists( $table, $field, $func,... $params)
Only run a function if the named field exists.
dropExtensionField( $tableName, $columnName, $sqlPath)
Drop a field from an extension table.
modifyTable( $table, $patch, $fullpath=false)
Modify an existing table, similar to modifyField.
renameExtensionIndex( $tableName, $oldIndexName, $newIndexName, $sqlPath, $skipBothIndexExistWarning=false)
Rename an index on an extension table Intended for use in LoadExtensionSchemaUpdates hook handlers.
getCoreUpdateList()
Get an array of updates to perform on the database.
doTable( $name)
Returns whether updates should be executed on the database table $name.
setAutoExtensionHookContainer(HookContainer $hookContainer)
Set the HookContainer to use for loading extension schema updates.
fieldExists( $tableName, $fieldName)
array $extensionUpdatesWithVirtualDomains
List of extension-provided database updates on virtual domain dbs.
updateRowExists( $key)
Helper function: check if the given key is present in the updatelog table.
addExtensionUpdateOnVirtualDomain(array $update)
Add a new update coming from an extension on virtual domain databases.
addPostDatabaseUpdateMaintenance( $class)
Add a maintenance script to be run after the database updates are complete.
insertUpdateRow( $key, $val=null)
Helper function: Add a key to the updatelog table.
IMaintainableDatabase $db
Handle to the database subclass.
addIndex( $table, $index, $patch, $fullpath=false)
Add a new index to an existing table.
array $updatesSkipped
Array of updates that were skipped.
addExtensionUpdate(array $update)
Add a new update coming from an extension.
setFileAccess()
Set any .htaccess files or equivalent for storage repos.
resource null $fileHandle
File handle for SQL output.
runMaintenance( $class, $unused='')
Run a maintenance script.
__construct(IMaintainableDatabase &$db, $shared, ?Maintenance $maintenance=null)
addTable( $name, $patch, $fullpath=false)
Add a new table to the database.
rebuildLocalisationCache()
Rebuilds the localisation cache.
doUpdates(array $what=[ 'core', 'extensions', 'stats'])
Do all the updates.
modifyExtensionTable( $tableName, $sqlPath)
Modify an existing extension table.
addExtensionTable( $tableName, $sqlPath)
Convenience wrapper for addExtensionUpdate() when adding a new table (which is the most common usage ...
getExtensionUpdates()
Get the list of extension-defined updates.
getInitialUpdateKeys()
Get an array of update keys to insert into the updatelog table after a new installation.
dropTable( $table, $patch=false, $fullpath=false)
If the specified table exists, drop it, or execute the patch if one is provided.
dropExtensionIndex( $tableName, $indexName, $sqlPath)
Drop an index from an extension table Intended for use in LoadExtensionSchemaUpdates hook handlers.
purgeCache()
Purge various database caches.
static getDBTypes()
Get a list of known DB types.
static getExistingLocalSettings()
Determine if LocalSettings.php exists.
A class containing constants representing the names of configuration variables.
const BaseDirectory
Name constant for the BaseDirectory setting, for use with Config::get()
Fake maintenance wrapper, mostly used for the web installer/updater.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
setDB(IMaintainableDatabase $db)
Sets database object to be returned by getDB().
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Load JSON files, and uses a Processor to extract information.
This class generates message blobs for use by ResourceLoader.
static clearGlobalCacheEntry(WANObjectCache $cache)
Invalidate cache keys for all known modules.
Class designed for counting of stats.
static doAllAndCommit( $database, array $options=[])
Do all updates and commit them.
Maintenance script that populates normalization column in links tables.
Maintenance script to rebuild the localisation cache.
Maintenance script to refresh image metadata fields.
Maintenance script that will find all rows in the categorylinks table whose collation is out-of-date.
$wgSharedTables
Config variable stub for the SharedTables setting, for use by phpdoc and IDEs.
$wgCategoryCollation
Config variable stub for the CategoryCollation setting, for use by phpdoc and IDEs.
$wgSharedDB
Config variable stub for the SharedDB setting, for use by phpdoc and IDEs.
$wgLocalisationCacheConf
Config variable stub for the LocalisationCacheConf setting, for use by phpdoc and IDEs.
setFlag( $flag, $remember=self::REMEMBER_NOTHING)
Set a flag for this connection.
Interface to a relational database.
Definition IDatabase.php:48
Advanced database interface for IDatabase handles that include maintenance methods.
getType()
Get the RDBMS type of the server (e.g.
Interface for query language.
const DBO_DDLMODE
Definition defines.php:16