MediaWiki  master
DatabaseUpdater.php
Go to the documentation of this file.
1 <?php
32 
33 require_once __DIR__ . '/../../maintenance/Maintenance.php';
34 
42 abstract class DatabaseUpdater {
43  public const REPLICATION_WAIT_TIMEOUT = 300;
44 
50  protected $updates = [];
51 
57  protected $updatesSkipped = [];
58 
63  protected $extensionUpdates = [];
64 
70  protected $db;
71 
75  protected $maintenance;
76 
77  protected $shared = false;
78 
81 
87  DeleteDefaultMessages::class,
88  PopulateRevisionLength::class,
89  PopulateRevisionSha1::class,
90  PopulateImageSha1::class,
91  FixExtLinksProtocolRelative::class,
92  PopulateFilearchiveSha1::class,
93  PopulateBacklinkNamespace::class,
94  FixDefaultJsonContentPages::class,
95  CleanupEmptyCategories::class,
96  AddRFCandPMIDInterwiki::class,
97  PopulatePPSortKey::class,
98  PopulateIpChanges::class,
99  RefreshExternallinksIndex::class,
100  ];
101 
107  protected $fileHandle = null;
108 
114  protected $skipSchema = false;
115 
122  protected function __construct(
124  $shared,
126  ) {
127  $this->db = $db;
128  $this->db->setFlag( DBO_DDLMODE );
129  $this->shared = $shared;
130  if ( $maintenance ) {
131  $this->maintenance = $maintenance;
132  $this->fileHandle = $maintenance->fileHandle;
133  } else {
134  $this->maintenance = new FakeMaintenance;
135  }
136  $this->maintenance->setDB( $db );
137  }
138 
142  private function loadExtensionSchemaUpdates() {
143  $hookContainer = $this->loadExtensions();
144  ( new HookRunner( $hookContainer ) )->onLoadExtensionSchemaUpdates( $this );
145  }
146 
153  private function loadExtensions() {
154  if ( $this->autoExtensionHookContainer ) {
155  // Already injected by installer
157  }
158  if ( defined( 'MW_EXTENSIONS_LOADED' ) ) {
159  throw new Exception( __METHOD__ .
160  ' apparently called from installer but no hook container was injected' );
161  }
162  if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
163  // Running under update.php: just use global locator
164  return MediaWikiServices::getInstance()->getHookContainer();
165  }
167 
168  $registry = ExtensionRegistry::getInstance();
169  $queue = $registry->getQueue();
170  // Don't accidentally load extensions in the future
171  $registry->clearQueue();
172 
173  // Read extension.json files
174  $extInfo = $registry->readFromQueue( $queue );
175 
176  // Merge extension attribute hooks with hooks defined by a .php
177  // registration file included from LocalSettings.php
178  $legacySchemaHooks = $extInfo['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ?? [];
179  if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
180  $legacySchemaHooks = array_merge( $legacySchemaHooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] );
181  }
182 
183  // Register classes defined by extensions that are loaded by including of a file that
184  // updates global variables, rather than having an extension.json manifest.
185  if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) {
186  AutoLoader::registerClasses( $vars['wgAutoloadClasses'] );
187  }
188 
189  // Register class definitions from extension.json files
190  if ( !isset( $extInfo['autoloaderPaths'] )
191  || !isset( $extInfo['autoloaderClasses'] )
192  || !isset( $extInfo['autoloaderNS'] )
193  ) {
194  // NOTE: protect against changes to the structure of $extInfo. It's volatile, and this usage easy to miss.
195  throw new LogicException( 'Missing autoloader keys from extracted extension info' );
196  }
197  AutoLoader::loadFiles( $extInfo['autoloaderPaths'] );
198  AutoLoader::registerClasses( $extInfo['autoloaderClasses'] );
199  AutoLoader::registerNamespaces( $extInfo['autoloaderNS'] );
200 
201  return new HookContainer(
202  new StaticHookRegistry(
203  [ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ],
204  $extInfo['attributes']['Hooks'] ?? [],
205  $extInfo['attributes']['DeprecatedHooks'] ?? []
206  ),
207  MediaWikiServices::getInstance()->getObjectFactory()
208  );
209  }
210 
219  public static function newForDB(
221  $shared = false,
223  ) {
224  $type = $db->getType();
225  if ( in_array( $type, Installer::getDBTypes() ) ) {
226  $class = ucfirst( $type ) . 'Updater';
227 
228  return new $class( $db, $shared, $maintenance );
229  } else {
230  throw new MWException( __METHOD__ . ' called for unsupported $wgDBtype' );
231  }
232  }
233 
241  public function setAutoExtensionHookContainer( HookContainer $hookContainer ) {
242  $this->autoExtensionHookContainer = $hookContainer;
243  }
244 
250  public function getDB() {
251  return $this->db;
252  }
253 
260  public function output( $str ) {
261  if ( $this->maintenance->isQuiet() ) {
262  return;
263  }
264  global $wgCommandLineMode;
265  if ( !$wgCommandLineMode ) {
266  $str = htmlspecialchars( $str );
267  }
268  echo $str;
269  flush();
270  }
271 
284  public function addExtensionUpdate( array $update ) {
285  $this->extensionUpdates[] = $update;
286  }
287 
298  public function addExtensionTable( $tableName, $sqlPath ) {
299  $this->extensionUpdates[] = [ 'addTable', $tableName, $sqlPath, true ];
300  }
301 
312  public function addExtensionIndex( $tableName, $indexName, $sqlPath ) {
313  $this->extensionUpdates[] = [ 'addIndex', $tableName, $indexName, $sqlPath, true ];
314  }
315 
326  public function addExtensionField( $tableName, $columnName, $sqlPath ) {
327  $this->extensionUpdates[] = [ 'addField', $tableName, $columnName, $sqlPath, true ];
328  }
329 
340  public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
341  $this->extensionUpdates[] = [ 'dropField', $tableName, $columnName, $sqlPath, true ];
342  }
343 
354  public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
355  $this->extensionUpdates[] = [ 'dropIndex', $tableName, $indexName, $sqlPath, true ];
356  }
357 
367  public function dropExtensionTable( $tableName, $sqlPath = false ) {
368  $this->extensionUpdates[] = [ 'dropTable', $tableName, $sqlPath, true ];
369  }
370 
384  public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
385  $sqlPath, $skipBothIndexExistWarning = false
386  ) {
387  $this->extensionUpdates[] = [
388  'renameIndex',
389  $tableName,
390  $oldIndexName,
391  $newIndexName,
392  $skipBothIndexExistWarning,
393  $sqlPath,
394  true
395  ];
396  }
397 
408  public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
409  $this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
410  }
411 
421  public function modifyExtensionTable( $tableName, $sqlPath ) {
422  $this->extensionUpdates[] = [ 'modifyTable', $tableName, $sqlPath, true ];
423  }
424 
431  public function tableExists( $tableName ) {
432  return ( $this->db->tableExists( $tableName, __METHOD__ ) );
433  }
434 
444  public function addPostDatabaseUpdateMaintenance( $class ) {
445  $this->postDatabaseUpdateMaintenance[] = $class;
446  }
447 
453  protected function getExtensionUpdates() {
455  }
456 
464  }
465 
472  private function writeSchemaUpdateFile( array $schemaUpdate = [] ) {
474  $this->updatesSkipped = [];
475 
476  foreach ( $updates as [ $func, $args, $origParams ] ) {
477  // @phan-suppress-next-line PhanUndeclaredInvokeInCallable
478  $func( ...$args );
479  flush();
480  $this->updatesSkipped[] = $origParams;
481  }
482  }
483 
495  public function getSchemaVars() {
496  return []; // DB-type specific
497  }
498 
504  public function doUpdates( array $what = [ 'core', 'extensions', 'stats' ] ) {
505  $this->db->setSchemaVars( $this->getSchemaVars() );
506 
507  $what = array_fill_keys( $what, true );
508  $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
509  if ( isset( $what['core'] ) ) {
510  $this->doCollationUpdate();
511  $this->runUpdates( $this->getCoreUpdateList(), false );
512  }
513  if ( isset( $what['extensions'] ) ) {
514  $this->loadExtensionSchemaUpdates();
515  $this->runUpdates( $this->getExtensionUpdates(), true );
516  }
517 
518  if ( isset( $what['stats'] ) ) {
519  $this->checkStats();
520  }
521 
522  if ( $this->fileHandle ) {
523  $this->skipSchema = false;
524  $this->writeSchemaUpdateFile();
525  }
526  }
527 
534  private function runUpdates( array $updates, $passSelf ) {
535  $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
536 
537  $updatesDone = [];
538  $updatesSkipped = [];
539  foreach ( $updates as $params ) {
540  $origParams = $params;
541  $func = array_shift( $params );
542  if ( !is_array( $func ) && method_exists( $this, $func ) ) {
543  $func = [ $this, $func ];
544  } elseif ( $passSelf ) {
545  array_unshift( $params, $this );
546  }
547  $ret = $func( ...$params );
548  flush();
549  if ( $ret !== false ) {
550  $updatesDone[] = $origParams;
551  $lbFactory->waitForReplication( [ 'timeout' => self::REPLICATION_WAIT_TIMEOUT ] );
552  } else {
553  $updatesSkipped[] = [ $func, $params, $origParams ];
554  }
555  }
556  $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
557  $this->updates = array_merge( $this->updates, $updatesDone );
558  }
559 
567  public function updateRowExists( $key ) {
568  $row = $this->db->selectRow(
569  'updatelog',
570  # T67813
571  '1 AS X',
572  [ 'ul_key' => $key ],
573  __METHOD__
574  );
575 
576  return (bool)$row;
577  }
578 
592  public function insertUpdateRow( $key, $val = null ) {
593  $this->db->clearFlag( DBO_DDLMODE );
594  $values = [ 'ul_key' => $key ];
595  if ( $val && $this->canUseNewUpdatelog() ) {
596  $values['ul_value'] = $val;
597  }
598  $this->db->insert( 'updatelog', $values, __METHOD__, [ 'IGNORE' ] );
599  $this->db->setFlag( DBO_DDLMODE );
600  }
601 
610  protected function canUseNewUpdatelog() {
611  return $this->db->tableExists( 'updatelog', __METHOD__ ) &&
612  $this->db->fieldExists( 'updatelog', 'ul_value', __METHOD__ );
613  }
614 
623  protected function doTable( $name ) {
625 
626  // Don't bother to check $wgSharedTables if there isn't a shared database
627  // or the user actually also wants to do updates on the shared database.
628  if ( $wgSharedDB === null || $this->shared ) {
629  return true;
630  }
631 
632  if ( in_array( $name, $wgSharedTables ) ) {
633  $this->output( "...skipping update to shared table $name.\n" );
634  return false;
635  } else {
636  return true;
637  }
638  }
639 
647  abstract protected function getCoreUpdateList();
648 
656  protected function copyFile( $filename ) {
657  $this->db->sourceFile(
658  $filename,
659  null,
660  null,
661  __METHOD__,
662  function ( $line ) {
663  return $this->appendLine( $line );
664  }
665  );
666  }
667 
680  protected function appendLine( $line ) {
681  $line = rtrim( $line ) . ";\n";
682  if ( fwrite( $this->fileHandle, $line ) === false ) {
683  throw new MWException( "trouble writing file" );
684  }
685 
686  return false;
687  }
688 
700  protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
701  if ( $msg === null ) {
702  $msg = "Applying $path patch";
703  }
704  if ( $this->skipSchema ) {
705  $this->output( "...skipping schema change ($msg).\n" );
706 
707  return false;
708  }
709 
710  $this->output( "{$msg}..." );
711 
712  if ( !$isFullPath ) {
713  $path = $this->patchPath( $this->db, $path );
714  }
715  if ( $this->fileHandle !== null ) {
716  $this->copyFile( $path );
717  } else {
718  $this->db->sourceFile( $path );
719  }
720  $this->output( "done.\n" );
721 
722  return true;
723  }
724 
733  public function patchPath( IDatabase $db, $patch ) {
734  $baseDir = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::BaseDirectory );
735 
736  $dbType = $db->getType();
737  if ( file_exists( "$baseDir/maintenance/$dbType/archives/$patch" ) ) {
738  return "$baseDir/maintenance/$dbType/archives/$patch";
739  } else {
740  return "$baseDir/maintenance/archives/$patch";
741  }
742  }
743 
755  protected function addTable( $name, $patch, $fullpath = false ) {
756  if ( !$this->doTable( $name ) ) {
757  return true;
758  }
759 
760  if ( $this->db->tableExists( $name, __METHOD__ ) ) {
761  $this->output( "...$name table already exists.\n" );
762  } else {
763  return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
764  }
765 
766  return true;
767  }
768 
781  protected function addField( $table, $field, $patch, $fullpath = false ) {
782  if ( !$this->doTable( $table ) ) {
783  return true;
784  }
785 
786  if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
787  $this->output( "...$table table does not exist, skipping new field patch.\n" );
788  } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
789  $this->output( "...have $field field in $table table.\n" );
790  } else {
791  return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
792  }
793 
794  return true;
795  }
796 
809  protected function addIndex( $table, $index, $patch, $fullpath = false ) {
810  if ( !$this->doTable( $table ) ) {
811  return true;
812  }
813 
814  if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
815  $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
816  } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
817  $this->output( "...index $index already set on $table table.\n" );
818  } else {
819  return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
820  }
821 
822  return true;
823  }
824 
837  protected function dropField( $table, $field, $patch, $fullpath = false ) {
838  if ( !$this->doTable( $table ) ) {
839  return true;
840  }
841 
842  if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
843  return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
844  } else {
845  $this->output( "...$table table does not contain $field field.\n" );
846  }
847 
848  return true;
849  }
850 
863  protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
864  if ( !$this->doTable( $table ) ) {
865  return true;
866  }
867 
868  if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
869  return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
870  } else {
871  $this->output( "...$index key doesn't exist.\n" );
872  }
873 
874  return true;
875  }
876 
893  protected function renameIndex( $table, $oldIndex, $newIndex,
894  $skipBothIndexExistWarning, $patch, $fullpath = false
895  ) {
896  if ( !$this->doTable( $table ) ) {
897  return true;
898  }
899 
900  // First requirement: the table must exist
901  if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
902  $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
903 
904  return true;
905  }
906 
907  // Second requirement: the new index must be missing
908  if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
909  $this->output( "...index $newIndex already set on $table table.\n" );
910  if ( !$skipBothIndexExistWarning &&
911  $this->db->indexExists( $table, $oldIndex, __METHOD__ )
912  ) {
913  $this->output( "...WARNING: $oldIndex still exists, despite it has " .
914  "been renamed into $newIndex (which also exists).\n" .
915  " $oldIndex should be manually removed if not needed anymore.\n" );
916  }
917 
918  return true;
919  }
920 
921  // Third requirement: the old index must exist
922  if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
923  $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
924 
925  return true;
926  }
927 
928  // Requirements have been satisfied, patch can be applied
929  return $this->applyPatch(
930  $patch,
931  $fullpath,
932  "Renaming index $oldIndex into $newIndex to table $table"
933  );
934  }
935 
950  protected function dropTable( $table, $patch = false, $fullpath = false ) {
951  if ( !$this->doTable( $table ) ) {
952  return true;
953  }
954 
955  if ( $this->db->tableExists( $table, __METHOD__ ) ) {
956  $msg = "Dropping table $table";
957 
958  if ( $patch === false ) {
959  $this->output( "$msg ..." );
960  $this->db->dropTable( $table, __METHOD__ );
961  $this->output( "done.\n" );
962  } else {
963  return $this->applyPatch( $patch, $fullpath, $msg );
964  }
965  } else {
966  $this->output( "...$table doesn't exist.\n" );
967  }
968 
969  return true;
970  }
971 
986  protected function modifyField( $table, $field, $patch, $fullpath = false ) {
987  if ( !$this->doTable( $table ) ) {
988  return true;
989  }
990 
991  $updateKey = "$table-$field-$patch";
992  if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
993  $this->output( "...$table table does not exist, skipping modify field patch.\n" );
994  } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
995  $this->output( "...$field field does not exist in $table table, " .
996  "skipping modify field patch.\n" );
997  } elseif ( $this->updateRowExists( $updateKey ) ) {
998  $this->output( "...$field in table $table already modified by patch $patch.\n" );
999  } else {
1000  $apply = $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
1001  if ( $apply ) {
1002  $this->insertUpdateRow( $updateKey );
1003  }
1004  return $apply;
1005  }
1006  return true;
1007  }
1008 
1023  protected function modifyTable( $table, $patch, $fullpath = false ) {
1024  if ( !$this->doTable( $table ) ) {
1025  return true;
1026  }
1027 
1028  $updateKey = "$table-$patch";
1029  if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
1030  $this->output( "...$table table does not exist, skipping modify table patch.\n" );
1031  } elseif ( $this->updateRowExists( $updateKey ) ) {
1032  $this->output( "...table $table already modified by patch $patch.\n" );
1033  } else {
1034  $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table" );
1035  if ( $apply ) {
1036  $this->insertUpdateRow( $updateKey );
1037  }
1038  return $apply;
1039  }
1040  return true;
1041  }
1042 
1063  protected function runMaintenance( $class, $script ) {
1064  $this->output( "Running $script...\n" );
1065  $task = $this->maintenance->runChild( $class );
1066  $ok = $task->execute();
1067  if ( !$ok ) {
1068  throw new RuntimeException( "Execution of $script did not complete successfully." );
1069  }
1070  $this->output( "done.\n" );
1071  }
1072 
1078  public function setFileAccess() {
1079  $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
1080  $zonePath = $repo->getZonePath( 'temp' );
1081  if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) {
1082  // If the directory was never made, then it will have the right ACLs when it is made
1083  $status = $repo->getBackend()->secure( [
1084  'dir' => $zonePath,
1085  'noAccess' => true,
1086  'noListing' => true
1087  ] );
1088  if ( $status->isOK() ) {
1089  $this->output( "Set the local repo temp zone container to be private.\n" );
1090  } else {
1091  $this->output( "Failed to set the local repo temp zone container to be private.\n" );
1092  }
1093  }
1094  }
1095 
1099  public function purgeCache() {
1100  global $wgLocalisationCacheConf;
1101  // We can't guarantee that the user will be able to use TRUNCATE,
1102  // but we know that DELETE is available to us
1103  $this->output( "Purging caches..." );
1104 
1105  // ObjectCache
1106  $this->db->delete( 'objectcache', '*', __METHOD__ );
1107 
1108  // LocalisationCache
1109  if ( $wgLocalisationCacheConf['manualRecache'] ) {
1110  $this->rebuildLocalisationCache();
1111  }
1112 
1113  // ResourceLoader: Message cache
1114  $services = MediaWikiServices::getInstance();
1115  $blobStore = new MessageBlobStore(
1116  $services->getResourceLoader(),
1117  null,
1118  $services->getMainWANObjectCache()
1119  );
1120  $blobStore->clear();
1121 
1122  // ResourceLoader: File-dependency cache
1123  $this->db->delete( 'module_deps', '*', __METHOD__ );
1124  $this->output( "done.\n" );
1125  }
1126 
1130  protected function checkStats() {
1131  $this->output( "...site_stats is populated..." );
1132  $row = $this->db->selectRow( 'site_stats', '*', [ 'ss_row_id' => 1 ], __METHOD__ );
1133  if ( $row === false ) {
1134  $this->output( "data is missing! rebuilding...\n" );
1135  } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) {
1136  $this->output( "missing ss_total_pages, rebuilding...\n" );
1137  } else {
1138  $this->output( "done.\n" );
1139 
1140  return;
1141  }
1142  SiteStatsInit::doAllAndCommit( $this->db );
1143  }
1144 
1145  # Common updater functions
1146 
1150  protected function doCollationUpdate() {
1151  global $wgCategoryCollation;
1152  if ( $this->db->selectField(
1153  'categorylinks',
1154  'COUNT(*)',
1155  'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
1156  __METHOD__
1157  ) == 0
1158  ) {
1159  $this->output( "...collations up-to-date.\n" );
1160 
1161  return;
1162  }
1163 
1164  $this->output( "Updating category collations..." );
1165  $task = $this->maintenance->runChild( UpdateCollation::class );
1166  $task->execute();
1167  $this->output( "...done.\n" );
1168  }
1169 
1170  protected function doConvertDjvuMetadata() {
1171  if ( $this->updateRowExists( 'ConvertDjvuMetadata' ) ) {
1172  return;
1173  }
1174  $this->output( "Converting djvu metadata..." );
1175  $task = $this->maintenance->runChild( RefreshImageMetadata::class );
1176  '@phan-var RefreshImageMetadata $task';
1177  $task->loadParamsAndArgs( RefreshImageMetadata::class, [
1178  'force' => true,
1179  'mediatype' => 'OFFICE',
1180  'mime' => 'image/*',
1181  'batch-size' => 1,
1182  'sleep' => 1
1183  ] );
1184  $ok = $task->execute();
1185  if ( $ok !== false ) {
1186  $this->output( "...done.\n" );
1187  $this->insertUpdateRow( 'ConvertDjvuMetadata' );
1188  }
1189  }
1190 
1194  protected function rebuildLocalisationCache() {
1198  $cl = $this->maintenance->runChild(
1199  RebuildLocalisationCache::class, 'rebuildLocalisationCache.php'
1200  );
1201  '@phan-var RebuildLocalisationCache $cl';
1202  $this->output( "Rebuilding localisation cache...\n" );
1203  $cl->setForce();
1204  $cl->execute();
1205  $this->output( "done.\n" );
1206  }
1207 
1208  protected function migrateTemplatelinks() {
1209  if ( $this->updateRowExists( MigrateLinksTable::class . 'templatelinks' ) ) {
1210  $this->output( "...templatelinks table has already been migrated.\n" );
1211  return;
1212  }
1216  $task = $this->maintenance->runChild(
1217  MigrateLinksTable::class, 'migrateLinksTable.php'
1218  );
1219  '@phan-var MigrateLinksTable $task';
1220  $task->loadParamsAndArgs( MigrateLinksTable::class, [
1221  'force' => true,
1222  'table' => 'templatelinks'
1223  ] );
1224  $this->output( "Running migrateLinksTable.php on templatelinks...\n" );
1225  $task->execute();
1226  $this->output( "done.\n" );
1227  }
1228 
1233  protected function migrateComments() {
1234  if ( !$this->updateRowExists( 'MigrateComments' ) ) {
1235  $this->output(
1236  "Migrating comments to the 'comments' table, printing progress markers. For large\n" .
1237  "databases, you may want to hit Ctrl-C and do this manually with\n" .
1238  "maintenance/migrateComments.php.\n"
1239  );
1240  $task = $this->maintenance->runChild( MigrateComments::class, 'migrateComments.php' );
1241  $ok = $task->execute();
1242  $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
1243  }
1244  }
1245 
1250  protected function migrateImageCommentTemp() {
1251  if ( $this->tableExists( 'image_comment_temp' ) ) {
1252  $this->output( "Merging image_comment_temp into the image table\n" );
1253  $task = $this->maintenance->runChild(
1254  MigrateImageCommentTemp::class, 'migrateImageCommentTemp.php'
1255  );
1256  // @phan-suppress-next-line PhanUndeclaredMethod
1257  $task->setForce();
1258  $ok = $task->execute();
1259  $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
1260  if ( $ok ) {
1261  $this->dropTable( 'image_comment_temp' );
1262  }
1263  }
1264  }
1265 
1270  protected function migrateActors() {
1271  if ( !$this->updateRowExists( 'MigrateActors' ) ) {
1272  $this->output(
1273  "Migrating actors to the 'actor' table, printing progress markers. For large\n" .
1274  "databases, you may want to hit Ctrl-C and do this manually with\n" .
1275  "maintenance/migrateActors.php.\n"
1276  );
1277  $task = $this->maintenance->runChild( MigrateActors::class, 'migrateActors.php' );
1278  $ok = $task->execute();
1279  $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
1280  }
1281  }
1282 
1287  protected function migrateArchiveText() {
1288  if ( $this->db->fieldExists( 'archive', 'ar_text', __METHOD__ ) ) {
1289  $this->output( "Migrating archive ar_text to modern storage.\n" );
1290  $task = $this->maintenance->runChild( MigrateArchiveText::class, 'migrateArchiveText.php' );
1291  // @phan-suppress-next-line PhanUndeclaredMethod
1292  $task->setForce();
1293  if ( $task->execute() ) {
1294  $this->applyPatch( 'patch-drop-ar_text.sql', false,
1295  'Dropping ar_text and ar_flags columns' );
1296  }
1297  }
1298  }
1299 
1304  protected function populateArchiveRevId() {
1305  $info = $this->db->fieldInfo( 'archive', 'ar_rev_id' );
1306  if ( !$info ) {
1307  throw new MWException( 'Missing ar_rev_id field of archive table. Should not happen.' );
1308  }
1309  if ( $info->isNullable() ) {
1310  $this->output( "Populating ar_rev_id.\n" );
1311  $task = $this->maintenance->runChild( PopulateArchiveRevId::class, 'populateArchiveRevId.php' );
1312  if ( $task->execute() ) {
1313  $this->applyPatch( 'patch-ar_rev_id-not-null.sql', false,
1314  'Making ar_rev_id not nullable' );
1315  }
1316  }
1317  }
1318 
1323  protected function populateExternallinksIndex60() {
1324  if ( !$this->updateRowExists( 'populate externallinks.el_index_60' ) ) {
1325  $this->output(
1326  "Populating el_index_60 field, printing progress markers. For large\n" .
1327  "databases, you may want to hit Ctrl-C and do this manually with\n" .
1328  "maintenance/populateExternallinksIndex60.php.\n"
1329  );
1330  $task = $this->maintenance->runChild( PopulateExternallinksIndex60::class,
1331  'populateExternallinksIndex60.php' );
1332  $task->execute();
1333  $this->output( "done.\n" );
1334  }
1335  }
1336 
1341  protected function populateContentTables() {
1342  if ( !$this->updateRowExists( 'PopulateContentTables' ) ) {
1343  $this->output(
1344  "Migrating revision data to the MCR 'slot' and 'content' tables, printing progress markers.\n" .
1345  "For large databases, you may want to hit Ctrl-C and do this manually with\n" .
1346  "maintenance/populateContentTables.php.\n"
1347  );
1348  $task = $this->maintenance->runChild(
1349  PopulateContentTables::class, 'populateContentTables.php'
1350  );
1351  $ok = $task->execute();
1352  $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
1353  if ( $ok ) {
1354  $this->insertUpdateRow( 'PopulateContentTables' );
1355  }
1356  }
1357  }
1358 
1372  protected function ifTableNotExists( $table, $func, ...$params ) {
1373  // Handle $passSelf from runUpdates().
1374  $passSelf = false;
1375  if ( $table === $this ) {
1376  $passSelf = true;
1377  $table = $func;
1378  $func = array_shift( $params );
1379  }
1380 
1381  if ( $this->db->tableExists( $table, __METHOD__ ) ) {
1382  return null;
1383  }
1384 
1385  if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1386  $func = [ $this, $func ];
1387  } elseif ( $passSelf ) {
1388  array_unshift( $params, $this );
1389  }
1390 
1391  // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1392  return $func( ...$params );
1393  }
1394 
1409  protected function ifFieldExists( $table, $field, $func, ...$params ) {
1410  // Handle $passSelf from runUpdates().
1411  $passSelf = false;
1412  if ( $table === $this ) {
1413  $passSelf = true;
1414  $table = $field;
1415  $field = $func;
1416  $func = array_shift( $params );
1417  }
1418 
1419  if ( !$this->db->tableExists( $table, __METHOD__ ) ||
1420  !$this->db->fieldExists( $table, $field, __METHOD__ )
1421  ) {
1422  return null;
1423  }
1424 
1425  if ( !is_array( $func ) && method_exists( $this, $func ) ) {
1426  $func = [ $this, $func ];
1427  } elseif ( $passSelf ) {
1428  array_unshift( $params, $this );
1429  }
1430 
1431  // @phan-suppress-next-line PhanUndeclaredInvokeInCallable Phan is confused
1432  return $func( ...$params );
1433  }
1434 
1435 }
global $wgCommandLineMode
Class for handling database updates.
purgeCache()
Purge various database caches.
addExtensionTable( $tableName, $sqlPath)
Convenience wrapper for addExtensionUpdate() when adding a new table (which is the most common usage ...
getDB()
Get a database connection to run updates.
setFileAccess()
Set any .htaccess files or equivalent for storage repos.
migrateImageCommentTemp()
Merge image_comment_temp into the image table.
addIndex( $table, $index, $patch, $fullpath=false)
Add a new index to an existing table.
array $updates
Array of updates to perform on the database.
modifyExtensionTable( $tableName, $sqlPath)
Modify an existing extension table.
bool $skipSchema
Flag specifying whether or not to skip schema (e.g.
populateArchiveRevId()
Populate ar_rev_id, then make it not nullable.
runMaintenance( $class, $script)
Run a maintenance script.
migrateArchiveText()
Migrate ar_text to modern storage.
addField( $table, $field, $patch, $fullpath=false)
Add a new field to an existing table.
addExtensionIndex( $tableName, $indexName, $sqlPath)
Add an index to an existing extension table.
modifyTable( $table, $patch, $fullpath=false)
Modify an existing table, similar to modifyField.
doUpdates(array $what=[ 'core', 'extensions', 'stats'])
Do all the updates.
Maintenance $maintenance
ifTableNotExists( $table, $func,... $params)
Only run a function if a table does not exist.
updateRowExists( $key)
Helper function: check if the given key is present in the updatelog table.
const REPLICATION_WAIT_TIMEOUT
getCoreUpdateList()
Get an array of updates to perform on the database.
populateExternallinksIndex60()
Populates the externallinks.el_index_60 field.
output( $str)
Output some text.
getExtensionUpdates()
Get the list of extension-defined updates.
insertUpdateRow( $key, $val=null)
Helper function: Add a key to the updatelog table.
setAutoExtensionHookContainer(HookContainer $hookContainer)
Set the HookContainer to use for loading extension schema updates.
canUseNewUpdatelog()
Updatelog was changed in 1.17 to have a ul_value column so we can record more information about what ...
patchPath(IDatabase $db, $patch)
Get the full path of a patch file.
copyFile( $filename)
Append an SQL fragment to the open file handle.
tableExists( $tableName)
string[] $postDatabaseUpdateMaintenance
Scripts to run after database update Should be a subclass of LoggedUpdateMaintenance.
HookContainer null $autoExtensionHookContainer
dropExtensionTable( $tableName, $sqlPath=false)
Drop an extension table.
resource null $fileHandle
File handle for SQL output.
renameIndex( $table, $oldIndex, $newIndex, $skipBothIndexExistWarning, $patch, $fullpath=false)
Rename an index from an existing table.
rebuildLocalisationCache()
Rebuilds the localisation cache.
addExtensionField( $tableName, $columnName, $sqlPath)
Add a field to an existing extension table.
IMaintainableDatabase $db
Handle to the database subclass.
ifFieldExists( $table, $field, $func,... $params)
Only run a function if the named field exists.
addTable( $name, $patch, $fullpath=false)
Add a new table to the database.
modifyField( $table, $field, $patch, $fullpath=false)
Modify an existing field.
dropExtensionField( $tableName, $columnName, $sqlPath)
Drop a field from an extension table.
static newForDB(IMaintainableDatabase $db, $shared=false, Maintenance $maintenance=null)
addPostDatabaseUpdateMaintenance( $class)
Add a maintenance script to be run after the database updates are complete.
dropField( $table, $field, $patch, $fullpath=false)
Drop a field from an existing table.
doCollationUpdate()
Update CategoryLinks collation.
renameExtensionIndex( $tableName, $oldIndexName, $newIndexName, $sqlPath, $skipBothIndexExistWarning=false)
Rename an index on an extension table Intended for use in LoadExtensionSchemaUpdates hook handlers.
migrateActors()
Migrate actors to the new 'actor' table.
addExtensionUpdate(array $update)
Add a new update coming from an extension.
array $updatesSkipped
Array of updates that were skipped.
appendLine( $line)
Append a line to the open filehandle.
dropExtensionIndex( $tableName, $indexName, $sqlPath)
Drop an index from an extension table Intended for use in LoadExtensionSchemaUpdates hook handlers.
array $extensionUpdates
List of extension-provided database updates.
checkStats()
Check the site_stats table is not properly populated.
modifyExtensionField( $tableName, $fieldName, $sqlPath)
Modify an existing field in an extension table.
applyPatch( $path, $isFullPath=false, $msg=null)
Applies a SQL patch.
dropIndex( $table, $index, $patch, $fullpath=false)
Drop an index from an existing table.
populateContentTables()
Populates the MCR content tables.
__construct(IMaintainableDatabase &$db, $shared, Maintenance $maintenance=null)
dropTable( $table, $patch=false, $fullpath=false)
If the specified table exists, drop it, or execute the patch if one is provided.
migrateComments()
Migrate comments to the new 'comment' table.
getSchemaVars()
Get appropriate schema variables in the current database connection.
doTable( $name)
Returns whether updates should be executed on the database table $name.
Fake maintenance wrapper, mostly used for the web installer/updater.
static getExistingLocalSettings()
Determine if LocalSettings.php exists.
Definition: Installer.php:655
static getDBTypes()
Get a list of known DB types.
Definition: Installer.php:536
MediaWiki exception.
Definition: MWException.php:29
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: Maintenance.php:66
setDB(IMaintainableDatabase $db)
Sets database object to be returned by getDB().
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:560
This is a simple immutable HookRegistry which can be used to set up a local HookContainer in tests an...
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
This class generates message blobs for use by ResourceLoader.
static doAllAndCommit( $database, array $options=[])
Do all updates and commit them.
$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.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:39
getType()
Get the RDBMS type of the server (e.g.
setFlag( $flag, $remember=self::REMEMBER_NOTHING)
Set a flag for this connection.
Advanced database interface for IDatabase handles that include maintenance methods.
$line
Definition: mcc.php:119
if( $line===false) $args
Definition: mcc.php:124
const DBO_DDLMODE
Definition: defines.php:16
return true
Definition: router.php:90