MediaWiki  REL1_22
DatabaseUpdater.php
Go to the documentation of this file.
00001 <?php
00024 require_once __DIR__ . '/../../maintenance/Maintenance.php';
00025 
00033 abstract class DatabaseUpdater {
00034 
00040     protected $updates = array();
00041 
00047     protected $updatesSkipped = array();
00048 
00053     protected $extensionUpdates = array();
00054 
00060     protected $db;
00061 
00062     protected $shared = false;
00063 
00068     protected $postDatabaseUpdateMaintenance = array(
00069         'DeleteDefaultMessages',
00070         'PopulateRevisionLength',
00071         'PopulateRevisionSha1',
00072         'PopulateImageSha1',
00073         'FixExtLinksProtocolRelative',
00074         'PopulateFilearchiveSha1',
00075     );
00076 
00082     protected $fileHandle = null;
00083 
00089     protected $skipSchema = false;
00090 
00098     protected function __construct( DatabaseBase &$db, $shared, Maintenance $maintenance = null ) {
00099         $this->db = $db;
00100         $this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files
00101         $this->shared = $shared;
00102         if ( $maintenance ) {
00103             $this->maintenance = $maintenance;
00104             $this->fileHandle = $maintenance->fileHandle;
00105         } else {
00106             $this->maintenance = new FakeMaintenance;
00107         }
00108         $this->maintenance->setDB( $db );
00109         $this->initOldGlobals();
00110         $this->loadExtensions();
00111         wfRunHooks( 'LoadExtensionSchemaUpdates', array( $this ) );
00112     }
00113 
00118     private function initOldGlobals() {
00119         global $wgExtNewTables, $wgExtNewFields, $wgExtPGNewFields,
00120             $wgExtPGAlteredFields, $wgExtNewIndexes, $wgExtModifiedFields;
00121 
00122         # For extensions only, should be populated via hooks
00123         # $wgDBtype should be checked to specifiy the proper file
00124         $wgExtNewTables = array(); // table, dir
00125         $wgExtNewFields = array(); // table, column, dir
00126         $wgExtPGNewFields = array(); // table, column, column attributes; for PostgreSQL
00127         $wgExtPGAlteredFields = array(); // table, column, new type, conversion method; for PostgreSQL
00128         $wgExtNewIndexes = array(); // table, index, dir
00129         $wgExtModifiedFields = array(); // table, index, dir
00130     }
00131 
00136     private function loadExtensions() {
00137         if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
00138             return; // already loaded
00139         }
00140         $vars = Installer::getExistingLocalSettings();
00141         if ( !$vars ) {
00142             return; // no LocalSettings found
00143         }
00144         if ( !isset( $vars['wgHooks'] ) || !isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
00145             return;
00146         }
00147         global $wgHooks, $wgAutoloadClasses;
00148         $wgHooks['LoadExtensionSchemaUpdates'] = $vars['wgHooks']['LoadExtensionSchemaUpdates'];
00149         $wgAutoloadClasses = $wgAutoloadClasses + $vars['wgAutoloadClasses'];
00150     }
00151 
00159     public static function newForDB( &$db, $shared = false, $maintenance = null ) {
00160         $type = $db->getType();
00161         if ( in_array( $type, Installer::getDBTypes() ) ) {
00162             $class = ucfirst( $type ) . 'Updater';
00163 
00164             return new $class( $db, $shared, $maintenance );
00165         } else {
00166             throw new MWException( __METHOD__ . ' called for unsupported $wgDBtype' );
00167         }
00168     }
00169 
00175     public function getDB() {
00176         return $this->db;
00177     }
00178 
00184     public function output( $str ) {
00185         if ( $this->maintenance->isQuiet() ) {
00186             return;
00187         }
00188         global $wgCommandLineMode;
00189         if ( !$wgCommandLineMode ) {
00190             $str = htmlspecialchars( $str );
00191         }
00192         echo $str;
00193         flush();
00194     }
00195 
00209     public function addExtensionUpdate( array $update ) {
00210         $this->extensionUpdates[] = $update;
00211     }
00212 
00222     public function addExtensionTable( $tableName, $sqlPath ) {
00223         $this->extensionUpdates[] = array( 'addTable', $tableName, $sqlPath, true );
00224     }
00225 
00233     public function addExtensionIndex( $tableName, $indexName, $sqlPath ) {
00234         $this->extensionUpdates[] = array( 'addIndex', $tableName, $indexName, $sqlPath, true );
00235     }
00236 
00245     public function addExtensionField( $tableName, $columnName, $sqlPath ) {
00246         $this->extensionUpdates[] = array( 'addField', $tableName, $columnName, $sqlPath, true );
00247     }
00248 
00257     public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
00258         $this->extensionUpdates[] = array( 'dropField', $tableName, $columnName, $sqlPath, true );
00259     }
00260 
00270     public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
00271         $this->extensionUpdates[] = array( 'dropIndex', $tableName, $indexName, $sqlPath, true );
00272     }
00273 
00281     public function dropExtensionTable( $tableName, $sqlPath ) {
00282         $this->extensionUpdates[] = array( 'dropTable', $tableName, $sqlPath, true );
00283     }
00284 
00297     public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
00298         $sqlPath, $skipBothIndexExistWarning = false
00299     ) {
00300         $this->extensionUpdates[] = array(
00301             'renameIndex',
00302             $tableName,
00303             $oldIndexName,
00304             $newIndexName,
00305             $skipBothIndexExistWarning,
00306             $sqlPath,
00307             true
00308         );
00309     }
00310 
00318     public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
00319         $this->extensionUpdates[] = array( 'modifyField', $tableName, $fieldName, $sqlPath, true );
00320     }
00321 
00329     public function tableExists( $tableName ) {
00330         return ( $this->db->tableExists( $tableName, __METHOD__ ) );
00331     }
00332 
00342     public function addPostDatabaseUpdateMaintenance( $class ) {
00343         $this->postDatabaseUpdateMaintenance[] = $class;
00344     }
00345 
00351     protected function getExtensionUpdates() {
00352         return $this->extensionUpdates;
00353     }
00354 
00360     public function getPostDatabaseUpdateMaintenance() {
00361         return $this->postDatabaseUpdateMaintenance;
00362     }
00363 
00369     private function writeSchemaUpdateFile( $schemaUpdate = array() ) {
00370         $updates = $this->updatesSkipped;
00371         $this->updatesSkipped = array();
00372 
00373         foreach ( $updates as $funcList ) {
00374             $func = $funcList[0];
00375             $arg = $funcList[1];
00376             $origParams = $funcList[2];
00377             call_user_func_array( $func, $arg );
00378             flush();
00379             $this->updatesSkipped[] = $origParams;
00380         }
00381     }
00382 
00388     public function doUpdates( $what = array( 'core', 'extensions', 'stats' ) ) {
00389         global $wgVersion;
00390 
00391         $this->db->begin( __METHOD__ );
00392         $what = array_flip( $what );
00393         $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
00394         if ( isset( $what['core'] ) ) {
00395             $this->runUpdates( $this->getCoreUpdateList(), false );
00396         }
00397         if ( isset( $what['extensions'] ) ) {
00398             $this->runUpdates( $this->getOldGlobalUpdates(), false );
00399             $this->runUpdates( $this->getExtensionUpdates(), true );
00400         }
00401 
00402         if ( isset( $what['stats'] ) ) {
00403             $this->checkStats();
00404         }
00405 
00406         $this->setAppliedUpdates( $wgVersion, $this->updates );
00407 
00408         if ( $this->fileHandle ) {
00409             $this->skipSchema = false;
00410             $this->writeSchemaUpdateFile();
00411             $this->setAppliedUpdates( "$wgVersion-schema", $this->updatesSkipped );
00412         }
00413 
00414         $this->db->commit( __METHOD__ );
00415     }
00416 
00424     private function runUpdates( array $updates, $passSelf ) {
00425         $updatesDone = array();
00426         $updatesSkipped = array();
00427         foreach ( $updates as $params ) {
00428             $origParams = $params;
00429             $func = array_shift( $params );
00430             if ( !is_array( $func ) && method_exists( $this, $func ) ) {
00431                 $func = array( $this, $func );
00432             } elseif ( $passSelf ) {
00433                 array_unshift( $params, $this );
00434             }
00435             $ret = call_user_func_array( $func, $params );
00436             flush();
00437             if ( $ret !== false ) {
00438                 $updatesDone[] = $origParams;
00439             } else {
00440                 $updatesSkipped[] = array( $func, $params, $origParams );
00441             }
00442         }
00443         $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
00444         $this->updates = array_merge( $this->updates, $updatesDone );
00445     }
00446 
00451     protected function setAppliedUpdates( $version, $updates = array() ) {
00452         $this->db->clearFlag( DBO_DDLMODE );
00453         if ( !$this->canUseNewUpdatelog() ) {
00454             return;
00455         }
00456         $key = "updatelist-$version-" . time();
00457         $this->db->insert( 'updatelog',
00458             array( 'ul_key' => $key, 'ul_value' => serialize( $updates ) ),
00459             __METHOD__ );
00460         $this->db->setFlag( DBO_DDLMODE );
00461     }
00462 
00471     public function updateRowExists( $key ) {
00472         $row = $this->db->selectRow(
00473             'updatelog',
00474             '1',
00475             array( 'ul_key' => $key ),
00476             __METHOD__
00477         );
00478 
00479         return (bool)$row;
00480     }
00481 
00489     public function insertUpdateRow( $key, $val = null ) {
00490         $this->db->clearFlag( DBO_DDLMODE );
00491         $values = array( 'ul_key' => $key );
00492         if ( $val && $this->canUseNewUpdatelog() ) {
00493             $values['ul_value'] = $val;
00494         }
00495         $this->db->insert( 'updatelog', $values, __METHOD__, 'IGNORE' );
00496         $this->db->setFlag( DBO_DDLMODE );
00497     }
00498 
00507     protected function canUseNewUpdatelog() {
00508         return $this->db->tableExists( 'updatelog', __METHOD__ ) &&
00509             $this->db->fieldExists( 'updatelog', 'ul_value', __METHOD__ );
00510     }
00511 
00520     protected function doTable( $name ) {
00521         global $wgSharedDB, $wgSharedTables;
00522 
00523         // Don't bother to check $wgSharedTables if there isn't a shared database
00524         // or the user actually also wants to do updates on the shared database.
00525         if ( $wgSharedDB === null || $this->shared ) {
00526             return true;
00527         }
00528 
00529         return !in_array( $name, $wgSharedTables );
00530     }
00531 
00540     protected function getOldGlobalUpdates() {
00541         global $wgExtNewFields, $wgExtNewTables, $wgExtModifiedFields,
00542             $wgExtNewIndexes;
00543 
00544         $updates = array();
00545 
00546         foreach ( $wgExtNewTables as $tableRecord ) {
00547             $updates[] = array(
00548                 'addTable', $tableRecord[0], $tableRecord[1], true
00549             );
00550         }
00551 
00552         foreach ( $wgExtNewFields as $fieldRecord ) {
00553             $updates[] = array(
00554                 'addField', $fieldRecord[0], $fieldRecord[1],
00555                 $fieldRecord[2], true
00556             );
00557         }
00558 
00559         foreach ( $wgExtNewIndexes as $fieldRecord ) {
00560             $updates[] = array(
00561                 'addIndex', $fieldRecord[0], $fieldRecord[1],
00562                 $fieldRecord[2], true
00563             );
00564         }
00565 
00566         foreach ( $wgExtModifiedFields as $fieldRecord ) {
00567             $updates[] = array(
00568                 'modifyField', $fieldRecord[0], $fieldRecord[1],
00569                 $fieldRecord[2], true
00570             );
00571         }
00572 
00573         return $updates;
00574     }
00575 
00584     abstract protected function getCoreUpdateList();
00585 
00591     public function copyFile( $filename ) {
00592         $this->db->sourceFile( $filename, false, false, false,
00593             array( $this, 'appendLine' )
00594         );
00595     }
00596 
00607     public function appendLine( $line ) {
00608         $line = rtrim( $line ) . ";\n";
00609         if ( fwrite( $this->fileHandle, $line ) === false ) {
00610             throw new MWException( "trouble writing file" );
00611         }
00612 
00613         return false;
00614     }
00615 
00624     protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
00625         if ( $msg === null ) {
00626             $msg = "Applying $path patch";
00627         }
00628         if ( $this->skipSchema ) {
00629             $this->output( "...skipping schema change ($msg).\n" );
00630 
00631             return false;
00632         }
00633 
00634         $this->output( "$msg ..." );
00635 
00636         if ( !$isFullPath ) {
00637             $path = $this->db->patchPath( $path );
00638         }
00639         if ( $this->fileHandle !== null ) {
00640             $this->copyFile( $path );
00641         } else {
00642             $this->db->sourceFile( $path );
00643         }
00644         $this->output( "done.\n" );
00645 
00646         return true;
00647     }
00648 
00657     protected function addTable( $name, $patch, $fullpath = false ) {
00658         if ( !$this->doTable( $name ) ) {
00659             return true;
00660         }
00661 
00662         if ( $this->db->tableExists( $name, __METHOD__ ) ) {
00663             $this->output( "...$name table already exists.\n" );
00664         } else {
00665             return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
00666         }
00667 
00668         return true;
00669     }
00670 
00680     protected function addField( $table, $field, $patch, $fullpath = false ) {
00681         if ( !$this->doTable( $table ) ) {
00682             return true;
00683         }
00684 
00685         if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
00686             $this->output( "...$table table does not exist, skipping new field patch.\n" );
00687         } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
00688             $this->output( "...have $field field in $table table.\n" );
00689         } else {
00690             return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
00691         }
00692 
00693         return true;
00694     }
00695 
00705     protected function addIndex( $table, $index, $patch, $fullpath = false ) {
00706         if ( !$this->doTable( $table ) ) {
00707             return true;
00708         }
00709 
00710         if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
00711             $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
00712         } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
00713             $this->output( "...index $index already set on $table table.\n" );
00714         } else {
00715             return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
00716         }
00717 
00718         return true;
00719     }
00720 
00730     protected function dropField( $table, $field, $patch, $fullpath = false ) {
00731         if ( !$this->doTable( $table ) ) {
00732             return true;
00733         }
00734 
00735         if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
00736             return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
00737         } else {
00738             $this->output( "...$table table does not contain $field field.\n" );
00739         }
00740 
00741         return true;
00742     }
00743 
00753     protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
00754         if ( !$this->doTable( $table ) ) {
00755             return true;
00756         }
00757 
00758         if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
00759             return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
00760         } else {
00761             $this->output( "...$index key doesn't exist.\n" );
00762         }
00763 
00764         return true;
00765     }
00766 
00779     protected function renameIndex( $table, $oldIndex, $newIndex,
00780         $skipBothIndexExistWarning, $patch, $fullpath = false
00781     ) {
00782         if ( !$this->doTable( $table ) ) {
00783             return true;
00784         }
00785 
00786         // First requirement: the table must exist
00787         if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
00788             $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
00789 
00790             return true;
00791         }
00792 
00793         // Second requirement: the new index must be missing
00794         if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
00795             $this->output( "...index $newIndex already set on $table table.\n" );
00796             if ( !$skipBothIndexExistWarning &&
00797                 $this->db->indexExists( $table, $oldIndex, __METHOD__ )
00798             ) {
00799                 $this->output( "...WARNING: $oldIndex still exists, despite it has " .
00800                     "been renamed into $newIndex (which also exists).\n" .
00801                     "            $oldIndex should be manually removed if not needed anymore.\n" );
00802             }
00803 
00804             return true;
00805         }
00806 
00807         // Third requirement: the old index must exist
00808         if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
00809             $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
00810 
00811             return true;
00812         }
00813 
00814         // Requirements have been satisfied, patch can be applied
00815         return $this->applyPatch(
00816             $patch,
00817             $fullpath,
00818             "Renaming index $oldIndex into $newIndex to table $table"
00819         );
00820     }
00821 
00833     public function dropTable( $table, $patch = false, $fullpath = false ) {
00834         if ( !$this->doTable( $table ) ) {
00835             return true;
00836         }
00837 
00838         if ( $this->db->tableExists( $table, __METHOD__ ) ) {
00839             $msg = "Dropping table $table";
00840 
00841             if ( $patch === false ) {
00842                 $this->output( "$msg ..." );
00843                 $this->db->dropTable( $table, __METHOD__ );
00844                 $this->output( "done.\n" );
00845             } else {
00846                 return $this->applyPatch( $patch, $fullpath, $msg );
00847             }
00848         } else {
00849             $this->output( "...$table doesn't exist.\n" );
00850         }
00851 
00852         return true;
00853     }
00854 
00864     public function modifyField( $table, $field, $patch, $fullpath = false ) {
00865         if ( !$this->doTable( $table ) ) {
00866             return true;
00867         }
00868 
00869         $updateKey = "$table-$field-$patch";
00870         if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
00871             $this->output( "...$table table does not exist, skipping modify field patch.\n" );
00872         } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
00873             $this->output( "...$field field does not exist in $table table, " .
00874                 "skipping modify field patch.\n" );
00875         } elseif ( $this->updateRowExists( $updateKey ) ) {
00876             $this->output( "...$field in table $table already modified by patch $patch.\n" );
00877         } else {
00878             $this->insertUpdateRow( $updateKey );
00879 
00880             return $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
00881         }
00882 
00883         return true;
00884     }
00885 
00889     public function purgeCache() {
00890         global $wgLocalisationCacheConf;
00891         # We can't guarantee that the user will be able to use TRUNCATE,
00892         # but we know that DELETE is available to us
00893         $this->output( "Purging caches..." );
00894         $this->db->delete( 'objectcache', '*', __METHOD__ );
00895         if ( $wgLocalisationCacheConf['manualRecache'] ) {
00896             $this->rebuildLocalisationCache();
00897         }
00898         MessageBlobStore::clear();
00899         $this->output( "done.\n" );
00900     }
00901 
00905     protected function checkStats() {
00906         $this->output( "...site_stats is populated..." );
00907         $row = $this->db->selectRow( 'site_stats', '*', array( 'ss_row_id' => 1 ), __METHOD__ );
00908         if ( $row === false ) {
00909             $this->output( "data is missing! rebuilding...\n" );
00910         } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) {
00911             $this->output( "missing ss_total_pages, rebuilding...\n" );
00912         } else {
00913             $this->output( "done.\n" );
00914 
00915             return;
00916         }
00917         SiteStatsInit::doAllAndCommit( $this->db );
00918     }
00919 
00920     # Common updater functions
00921 
00925     protected function doActiveUsersInit() {
00926         $activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', false, __METHOD__ );
00927         if ( $activeUsers == -1 ) {
00928             $activeUsers = $this->db->selectField( 'recentchanges',
00929                 'COUNT( DISTINCT rc_user_text )',
00930                 array( 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers'" ), __METHOD__
00931             );
00932             $this->db->update( 'site_stats',
00933                 array( 'ss_active_users' => intval( $activeUsers ) ),
00934                 array( 'ss_row_id' => 1 ), __METHOD__, array( 'LIMIT' => 1 )
00935             );
00936         }
00937         $this->output( "...ss_active_users user count set...\n" );
00938     }
00939 
00943     protected function doLogUsertextPopulation() {
00944         if ( !$this->updateRowExists( 'populate log_usertext' ) ) {
00945             $this->output(
00946                 "Populating log_user_text field, printing progress markers. For large\n" .
00947                 "databases, you may want to hit Ctrl-C and do this manually with\n" .
00948                 "maintenance/populateLogUsertext.php.\n"
00949             );
00950 
00951             $task = $this->maintenance->runChild( 'PopulateLogUsertext' );
00952             $task->execute();
00953             $this->output( "done.\n" );
00954         }
00955     }
00956 
00960     protected function doLogSearchPopulation() {
00961         if ( !$this->updateRowExists( 'populate log_search' ) ) {
00962             $this->output(
00963                 "Populating log_search table, printing progress markers. For large\n" .
00964                 "databases, you may want to hit Ctrl-C and do this manually with\n" .
00965                 "maintenance/populateLogSearch.php.\n" );
00966 
00967             $task = $this->maintenance->runChild( 'PopulateLogSearch' );
00968             $task->execute();
00969             $this->output( "done.\n" );
00970         }
00971     }
00972 
00976     protected function doUpdateTranscacheField() {
00977         if ( $this->updateRowExists( 'convert transcache field' ) ) {
00978             $this->output( "...transcache tc_time already converted.\n" );
00979 
00980             return true;
00981         }
00982 
00983         return $this->applyPatch( 'patch-tc-timestamp.sql', false,
00984             "Converting tc_time from UNIX epoch to MediaWiki timestamp" );
00985     }
00986 
00990     protected function doCollationUpdate() {
00991         global $wgCategoryCollation;
00992         if ( $this->db->fieldExists( 'categorylinks', 'cl_collation', __METHOD__ ) ) {
00993             if ( $this->db->selectField(
00994                 'categorylinks',
00995                 'COUNT(*)',
00996                 'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
00997                 __METHOD__
00998                 ) == 0
00999             ) {
01000                 $this->output( "...collations up-to-date.\n" );
01001 
01002                 return;
01003             }
01004 
01005             $this->output( "Updating category collations..." );
01006             $task = $this->maintenance->runChild( 'UpdateCollation' );
01007             $task->execute();
01008             $this->output( "...done.\n" );
01009         }
01010     }
01011 
01015     protected function doMigrateUserOptions() {
01016         if ( $this->db->tableExists( 'user_properties' ) ) {
01017             $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
01018             $cl->execute();
01019             $this->output( "done.\n" );
01020         }
01021     }
01022 
01026     protected function rebuildLocalisationCache() {
01030         $cl = $this->maintenance->runChild( 'RebuildLocalisationCache', 'rebuildLocalisationCache.php' );
01031         $this->output( "Rebuilding localisation cache...\n" );
01032         $cl->setForce();
01033         $cl->execute();
01034         $this->output( "done.\n" );
01035     }
01036 }