(bug 38110) provide a way to separate out schema changes
authorMarkAHershberger <mah@everybody.org>
Thu, 22 Nov 2012 03:53:24 +0000 (22:53 -0500)
committerMarkAHershberger <mah@everybody.org>
Thu, 22 Nov 2012 03:53:24 +0000 (22:53 -0500)
Allows update.php to be run when $wgAllowSchemaUpdates = false.

This is useful for non-WMF environments where strict DB permissions
allow database updates (which update.php performs), but no schema
changes (such as adding or dropping tables or indices which update.php
also performs).

It does this by adding the --schema and --noschema flags. Without
either of these flags, update.php will perform exactly as before.

With --noschema, all changes to the table structure or table additions
are skipped. Only data changes are made.

With --schema is used, no schema changes are made to the database, but
the schema changes are saved to a separate SQL file that can be run.

Change-Id: I96b4cfd4c02e9cbf46cc6a0499b87fb3b89020a0

RELEASE-NOTES-1.21
UPGRADE
includes/db/Database.php
includes/db/DatabaseMysql.php
includes/installer/DatabaseUpdater.php
includes/installer/MysqlUpdater.php
maintenance/Maintenance.php
maintenance/populateFilearchiveSha1.php
maintenance/populateRevisionLength.php
maintenance/populateRevisionSha1.php
maintenance/update.php

index a0035d5..e2bf3ad 100644 (file)
@@ -16,6 +16,13 @@ production.
   instead.
 
 === New features in 1.21 ===
+* (bug 38110) Schema changes (adding or dropping tables, indicies and
+  fields) can be now be done separately from from other changes that
+  update.php makes.  This is useful in environments that use database
+  permissions to restrict schema changes but allow the DB user that
+  MediaWiki normally runs as to perform other changes that update.php
+  makes.  Schema changes can be run seperately.  See the file UPGRADE
+  for more information.
 * (bug 34876) jquery.makeCollapsible has been improved in performance.
 * Added ContentHandler facility to allow extensions to support other content than wikitext.
   See docs/contenthandler.txt for details.
diff --git a/UPGRADE b/UPGRADE
index 46f5e65..3e3cb13 100644 (file)
--- a/UPGRADE
+++ b/UPGRADE
@@ -51,6 +51,11 @@ deleted file archives, and any custom skins.
 
 === Perform the database upgrade ===
 
+As of 1.21, it is possible to separate schema changes (i.e. adding,
+dropping, or changing tables, fields, or indices) from all other
+database changes (e.g. populating fields).  If you need this
+capability, see "From the command line" below.
+
 ==== From the web ====
 
 If you browse to the web-based installation script (usually at
@@ -64,6 +69,12 @@ update.php script to check and update the schema. This will insert missing
 tables, update existing tables, and move data around as needed. In most cases,
 this is successful and nothing further needs to be done.
 
+If you need to separate out the schema changes so they can be run
+by someone with more privileges, then you can use the --schema option
+to produce a text file with the necessary commands.  You can use
+--schema, --noschema, $wgAllowSchemaUpdates as well as proper database
+permissions to enforce this separation.
+
 === Check configuration settings ===
 
 The names of configuration variables, and their default values and purposes,
index 659c6e0..dfb3469 100644 (file)
@@ -274,6 +274,13 @@ abstract class DatabaseBase implements DatabaseType {
         */
        private $mTrxAutomatic = false;
 
+       /**
+        * @since 1.21
+        * @var file handle for upgrade
+        */
+       protected $fileHandle = null;
+
+
 # ------------------------------------------------------------------------------
 # Accessors
 # ------------------------------------------------------------------------------
@@ -386,6 +393,15 @@ abstract class DatabaseBase implements DatabaseType {
                return wfSetVar( $this->mTablePrefix, $prefix );
        }
 
+       /**
+        * Set the filehandle to copy write statements to.
+        *
+        * @param $fh filehandle
+        */
+       public function setFileHandle( $fh ) {
+               $this->fileHandle = $fh;
+       }
+
        /**
         * Get properties passed down from the server info array of the load
         * balancer.
@@ -1578,6 +1594,10 @@ abstract class DatabaseBase implements DatabaseType {
         * @return bool|null
         */
        public function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) {
+               if( !$this->tableExists( $table ) ) {
+                       return null;
+               }
+
                $info = $this->indexInfo( $table, $index, $fname );
                if ( is_null( $info ) ) {
                        return null;
@@ -1690,6 +1710,10 @@ abstract class DatabaseBase implements DatabaseType {
                        $options = array( $options );
                }
 
+               $fh = null;
+               if ( isset( $options['fileHandle'] ) ) {
+                       $fh = $options['fileHandle'];
+               }
                $options = $this->makeInsertOptions( $options );
 
                if ( isset( $a[0] ) && is_array( $a[0] ) ) {
@@ -1717,6 +1741,12 @@ abstract class DatabaseBase implements DatabaseType {
                        $sql .= '(' . $this->makeList( $a ) . ')';
                }
 
+               if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
+                       return false;
+               } elseif ( $fh !== null ) {
+                       return true;
+               }
+
                return (bool)$this->query( $sql, $fname );
        }
 
@@ -3245,11 +3275,12 @@ abstract class DatabaseBase implements DatabaseType {
         * @param bool|callable $resultCallback Optional function called for each MySQL result
         * @param bool|string $fname Calling function name or false if name should be
         *      generated dynamically using $filename
+        * @param bool|callable $inputCallback Callback: Optional function called for each complete line sent
         * @throws MWException
         * @return bool|string
         */
        public function sourceFile(
-               $filename, $lineCallback = false, $resultCallback = false, $fname = false
+               $filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
        ) {
                wfSuppressWarnings();
                $fp = fopen( $filename, 'r' );
@@ -3264,7 +3295,7 @@ abstract class DatabaseBase implements DatabaseType {
                }
 
                try {
-                       $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname );
+                       $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
                }
                catch ( MWException $e ) {
                        fclose( $fp );
@@ -3313,10 +3344,10 @@ abstract class DatabaseBase implements DatabaseType {
         * on object's error ignore settings).
         *
         * @param $fp Resource: File handle
-        * @param $lineCallback Callback: Optional function called before reading each line
+        * @param $lineCallback Callback: Optional function called before reading each query
         * @param $resultCallback Callback: Optional function called for each MySQL result
         * @param $fname String: Calling function name
-        * @param $inputCallback Callback: Optional function called for each complete line (ended with ;) sent
+        * @param $inputCallback Callback: Optional function called for each complete query sent
         * @return bool|string
         */
        public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
@@ -3349,20 +3380,19 @@ abstract class DatabaseBase implements DatabaseType {
 
                        if ( $done || feof( $fp ) ) {
                                $cmd = $this->replaceVars( $cmd );
-                               if ( $inputCallback ) {
-                                       call_user_func( $inputCallback, $cmd );
-                               }
-                               $res = $this->query( $cmd, $fname );
 
-                               if ( $resultCallback ) {
-                                       call_user_func( $resultCallback, $res, $this );
-                               }
+                               if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
+                                       $res = $this->query( $cmd, $fname );
 
-                               if ( false === $res ) {
-                                       $err = $this->lastError();
-                                       return "Query \"{$cmd}\" failed with error code \"$err\".\n";
-                               }
+                                       if ( $resultCallback ) {
+                                               call_user_func( $resultCallback, $res, $this );
+                                       }
 
+                                       if ( false === $res ) {
+                                               $err = $this->lastError();
+                                               return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+                                       }
+                               }
                                $cmd = '';
                        }
                }
index b509302..36a1126 100644 (file)
@@ -414,6 +414,7 @@ class DatabaseMysql extends DatabaseBase {
                # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
                $table = $this->tableName( $table );
                $index = $this->indexName( $index );
+
                $sql = 'SHOW INDEX FROM ' . $table;
                $res = $this->query( $sql, $fname );
 
@@ -428,7 +429,6 @@ class DatabaseMysql extends DatabaseBase {
                                $result[] = $row;
                        }
                }
-
                return empty( $result ) ? false : $result;
        }
 
index 740ead5..312b796 100644 (file)
@@ -39,6 +39,13 @@ abstract class DatabaseUpdater {
         */
        protected $updates = array();
 
+       /**
+        * Array of updates that were skipped
+        *
+        * @var array
+        */
+       protected $updatesSkipped = array();
+
        /**
         * List of extension-provided database updates
         * @var array
@@ -67,6 +74,20 @@ abstract class DatabaseUpdater {
                'PopulateFilearchiveSha1',
        );
 
+       /**
+        * File handle for SQL output.
+        *
+        * @var Filehandle
+        */
+       protected $fileHandle = null;
+
+       /**
+        * Flag specifying whether or not to skip schema (e.g. SQL-only) updates.
+        *
+        * @var bool
+        */
+       protected $skipSchema = false;
+
        /**
         * Constructor
         *
@@ -80,6 +101,7 @@ abstract class DatabaseUpdater {
                $this->shared = $shared;
                if ( $maintenance ) {
                        $this->maintenance = $maintenance;
+                       $this->fileHandle = $maintenance->fileHandle;
                } else {
                        $this->maintenance = new FakeMaintenance;
                }
@@ -287,16 +309,35 @@ abstract class DatabaseUpdater {
                return $this->postDatabaseUpdateMaintenance;
        }
 
+       /**
+        * @since 1.21
+        *
+        * Writes the schema updates desired to a file for the DB Admin to run.
+        */
+       private function writeSchemaUpdateFile( $schemaUpdate = array() ) {
+               $updates = $this->updatesSkipped;
+               $this->updatesSkipped = array();
+
+               foreach( $updates as $funcList ) {
+                       $func = $funcList[0];
+                       $arg = $funcList[1];
+                       $ret = call_user_func_array( $func, $arg );
+                       flush();
+                       $this->updatesSkipped[] = $arg;
+               }
+       }
+
        /**
         * Do all the updates
         *
         * @param $what Array: what updates to perform
         */
        public function doUpdates( $what = array( 'core', 'extensions', 'stats' ) ) {
-               global $wgVersion;
+               global $wgVersion, $wgLocalisationCacheConf;
 
                $this->db->begin( __METHOD__ );
                $what = array_flip( $what );
+               $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
                if ( isset( $what['core'] ) ) {
                        $this->runUpdates( $this->getCoreUpdateList(), false );
                }
@@ -305,11 +346,26 @@ abstract class DatabaseUpdater {
                        $this->runUpdates( $this->getExtensionUpdates(), true );
                }
 
-               $this->setAppliedUpdates( $wgVersion, $this->updates );
-
                if ( isset( $what['stats'] ) ) {
                        $this->checkStats();
                }
+
+               if ( isset( $what['purge'] ) ) {
+                       $this->purgeCache();
+
+                       if ( $wgLocalisationCacheConf['manualRecache'] ) {
+                               $this->rebuildLocalisationCache();
+                       }
+               }
+
+               $this->setAppliedUpdates( $wgVersion, $this->updates );
+
+               if( $this->fileHandle ) {
+                       $this->skipSchema = false;
+                       $this->writeSchemaUpdateFile( );
+                       $this->setAppliedUpdates( "$wgVersion-schema", $this->updatesSkipped );
+               }
+
                $this->db->commit( __METHOD__ );
        }
 
@@ -321,6 +377,8 @@ abstract class DatabaseUpdater {
         *                  functions
         */
        private function runUpdates( array $updates, $passSelf ) {
+               $updatesDone = array();
+               $updatesSkipped = array();
                foreach ( $updates as $params ) {
                        $func = array_shift( $params );
                        if( !is_array( $func ) && method_exists( $this, $func ) ) {
@@ -328,10 +386,16 @@ abstract class DatabaseUpdater {
                        } elseif ( $passSelf ) {
                                array_unshift( $params, $this );
                        }
-                       call_user_func_array( $func, $params );
+                       $ret = call_user_func_array( $func, $params );
                        flush();
+                       if( $ret !== false ) {
+                               $updatesDone[] = $params;
+                       } else {
+                               $updatesSkipped[] = array( $func, $params );
+                       }
                }
-               $this->updates = array_merge( $this->updates, $updates );
+               $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
+               $this->updates = array_merge( $this->updates, $updatesDone );
        }
 
        /**
@@ -458,24 +522,63 @@ abstract class DatabaseUpdater {
         */
        protected abstract function getCoreUpdateList();
 
+       /**
+        * Append an SQL fragment to the open file handle.
+        *
+        * @param $filename String: File name to open
+        */
+       public function copyFile( $filename ) {
+               $this->db->sourceFile( $filename, false, false, false,
+                       array( $this, 'appendLine' )
+               );
+       }
+
+       /**
+        * Append a line to the open filehandle.  The line is assumed to
+        * be a complete SQL statement.
+        *
+        * This is used as a callback for for sourceLine().
+        *
+        * @param $line String text to append to the file
+        * @return Boolean false to skip actually executing the file
+        * @throws MWException
+        */
+       public function appendLine( $line ) {
+               $line = rtrim( $line ) . ";\n";
+               if( fwrite( $this->fileHandle, $line ) === false ) {
+                       throw new MWException( "trouble writing file" );
+               }
+               return false;
+       }
+
        /**
         * Applies a SQL patch
         * @param $path String Path to the patch file
         * @param $isFullPath Boolean Whether to treat $path as a relative or not
         * @param $msg String Description of the patch
+        * @return boolean false if patch is skipped.
         */
        protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
                if ( $msg === null ) {
                        $msg = "Applying $path patch";
                }
+               if ( $this->skipSchema ) {
+                       $this->output( "...skipping schema change ($msg).\n" );
+                       return false;
+               }
+
+               $this->output( "$msg ..." );
 
                if ( !$isFullPath ) {
                        $path = $this->db->patchPath( $path );
                }
-
-               $this->output( "$msg ..." );
-               $this->db->sourceFile( $path );
-               $this->output( "done.\n" );
+               if( $this->fileHandle !== null ) {
+                       $this->copyFile( $path );
+               } else {
+                       $this->db->sourceFile( $path );
+               }
+               $this->output( "done.\n" );
+               return true;
        }
 
        /**
@@ -483,13 +586,15 @@ abstract class DatabaseUpdater {
         * @param $name String Name of the new table
         * @param $patch String Path to the patch file
         * @param $fullpath Boolean Whether to treat $patch path as a relative or not
+        * @return Boolean false if this was skipped because schema changes are skipped
         */
        protected function addTable( $name, $patch, $fullpath = false ) {
                if ( $this->db->tableExists( $name, __METHOD__ ) ) {
                        $this->output( "...$name table already exists.\n" );
                } else {
-                       $this->applyPatch( $patch, $fullpath, "Creating $name table" );
+                       return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
                }
+               return true;
        }
 
        /**
@@ -498,6 +603,7 @@ abstract class DatabaseUpdater {
         * @param $field String Name of the new field
         * @param $patch String Path to the patch file
         * @param $fullpath Boolean Whether to treat $patch path as a relative or not
+        * @return Boolean false if this was skipped because schema changes are skipped
         */
        protected function addField( $table, $field, $patch, $fullpath = false ) {
                if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
@@ -505,8 +611,9 @@ abstract class DatabaseUpdater {
                } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
                        $this->output( "...have $field field in $table table.\n" );
                } else {
-                       $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
+                       return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
                }
+               return true;
        }
 
        /**
@@ -515,13 +622,18 @@ abstract class DatabaseUpdater {
         * @param $index String Name of the new index
         * @param $patch String Path to the patch file
         * @param $fullpath Boolean Whether to treat $patch path as a relative or not
+        * @return Boolean false if this was skipped because schema changes are skipped
         */
        protected function addIndex( $table, $index, $patch, $fullpath = false ) {
-               if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
+               if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
+                       $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
+                       return false;
+               } else if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
                        $this->output( "...index $index already set on $table table.\n" );
                } else {
-                       $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
+                       return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
                }
+               return true;
        }
 
        /**
@@ -531,13 +643,15 @@ abstract class DatabaseUpdater {
         * @param $field String Name of the old field
         * @param $patch String Path to the patch file
         * @param $fullpath Boolean Whether to treat $patch path as a relative or not
+        * @return Boolean false if this was skipped because schema changes are skipped
         */
        protected function dropField( $table, $field, $patch, $fullpath = false ) {
                if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
-                       $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
+                       return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
                } else {
                        $this->output( "...$table table does not contain $field field.\n" );
                }
+               return true;
        }
 
        /**
@@ -547,13 +661,15 @@ abstract class DatabaseUpdater {
         * @param $index String: Name of the old index
         * @param $patch String: Path to the patch file
         * @param $fullpath Boolean: Whether to treat $patch path as a relative or not
+        * @return Boolean false if this was skipped because schema changes are skipped
         */
        protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
                if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
-                       $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
+                       return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
                } else {
                        $this->output( "...$index key doesn't exist.\n" );
                }
+               return true;
        }
 
        /**
@@ -565,6 +681,7 @@ abstract class DatabaseUpdater {
         * @param $table string
         * @param $patch string|false
         * @param $fullpath bool
+        * @return Boolean false if this was skipped because schema changes are skipped
         */
        public function dropTable( $table, $patch = false, $fullpath = false ) {
                if ( $this->db->tableExists( $table, __METHOD__ ) ) {
@@ -576,12 +693,12 @@ abstract class DatabaseUpdater {
                                $this->output( "done.\n" );
                        }
                        else {
-                               $this->applyPatch( $patch, $fullpath, $msg );
+                               return $this->applyPatch( $patch, $fullpath, $msg );
                        }
-
                } else {
                        $this->output( "...$table doesn't exist.\n" );
                }
+               return true;
        }
 
        /**
@@ -591,6 +708,7 @@ abstract class DatabaseUpdater {
         * @param $field String: name of the field to modify
         * @param $patch String: path to the patch file
         * @param $fullpath Boolean: whether to treat $patch path as a relative or not
+        * @return Boolean false if this was skipped because schema changes are skipped
         */
        public function modifyField( $table, $field, $patch, $fullpath = false ) {
                $updateKey = "$table-$field-$patch";
@@ -601,9 +719,10 @@ abstract class DatabaseUpdater {
                } elseif( $this->updateRowExists( $updateKey ) ) {
                        $this->output( "...$field in table $table already modified by patch $patch.\n" );
                } else {
-                       $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
+                       return $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
                        $this->insertUpdateRow( $updateKey );
                }
+               return true;
        }
 
        /**
@@ -699,7 +818,8 @@ abstract class DatabaseUpdater {
                        return;
                }
 
-               $this->applyPatch( 'patch-tc-timestamp.sql', false, "Converting tc_time from UNIX epoch to MediaWiki timestamp" );
+               return $this->applyPatch( 'patch-tc-timestamp.sql', false,
+                       "Converting tc_time from UNIX epoch to MediaWiki timestamp" );
        }
 
        /**
@@ -707,29 +827,33 @@ abstract class DatabaseUpdater {
         */
        protected function doCollationUpdate() {
                global $wgCategoryCollation;
-               if ( $this->db->selectField(
-                       'categorylinks',
-                       'COUNT(*)',
-                       'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
-                       __METHOD__
-               ) == 0 ) {
-                       $this->output( "...collations up-to-date.\n" );
-                       return;
-               }
+               if ( $this->db->fieldExists( 'categorylinks', 'cl_collation', __METHOD__ ) ) {
+                       if ( $this->db->selectField(
+                               'categorylinks',
+                               'COUNT(*)',
+                               'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
+                               __METHOD__
+                               ) == 0 ) {
+                                       $this->output( "...collations up-to-date.\n" );
+                                       return;
+                       }
 
-               $this->output( "Updating category collations..." );
-               $task = $this->maintenance->runChild( 'UpdateCollation' );
-               $task->execute();
-               $this->output( "...done.\n" );
+                       $this->output( "Updating category collations..." );
+                       $task = $this->maintenance->runChild( 'UpdateCollation' );
+                       $task->execute();
+                       $this->output( "...done.\n" );
+               }
        }
 
        /**
         * Migrates user options from the user table blob to user_properties
         */
        protected function doMigrateUserOptions() {
-               $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
-               $cl->execute();
-               $this->output( "done.\n" );
+               if( $this->db->tableExists( 'user_properties' ) ) {
+                       $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
+                       $cl->execute();
+                       $this->output( "done.\n" );
+               }
        }
 
        /**
index 113c65a..c00dc13 100644 (file)
@@ -155,8 +155,8 @@ class MysqlUpdater extends DatabaseUpdater {
                        // 1.15
                        array( 'doUniquePlTlIl' ),
                        array( 'addTable', 'change_tag',                        'patch-change_tag.sql' ),
-                       array( 'addTable', 'tag_summary',                       'patch-change_tag.sql' ),
-                       array( 'addTable', 'valid_tag',                         'patch-change_tag.sql' ),
+                       /* array( 'addTable', 'tag_summary',                       'patch-change_tag.sql' ), */
+                       /* array( 'addTable', 'valid_tag',                         'patch-change_tag.sql' ), */
 
                        // 1.16
                        array( 'addTable', 'user_properties',                   'patch-user_properties.sql' ),
@@ -641,6 +641,9 @@ class MysqlUpdater extends DatabaseUpdater {
         */
        protected function doWatchlistNull() {
                $info = $this->db->fieldInfo( 'watchlist', 'wl_notificationtimestamp' );
+               if ( !$info ) {
+                       return;
+               }
                if ( $info->isNullable() ) {
                        $this->output( "...wl_notificationtimestamp is already nullable.\n" );
                        return;
@@ -782,9 +785,10 @@ class MysqlUpdater extends DatabaseUpdater {
 
        protected function doMaybeProfilingMemoryUpdate() {
                if ( !$this->db->tableExists( 'profiling', __METHOD__ ) ) {
-                       // Simply ignore
+                       return true;
                } elseif ( $this->db->fieldExists( 'profiling', 'pf_memory', __METHOD__ ) ) {
                        $this->output( "...profiling table has pf_memory field.\n" );
+                       return true;
                } else {
                        $this->applyPatch( 'patch-profiling-memory.sql', false, "Adding pf_memory field to table profiling" );
                }
@@ -795,16 +799,21 @@ class MysqlUpdater extends DatabaseUpdater {
                if ( !$info ) {
                        $this->applyPatch( 'patch-filearchive-user-index.sql', false, "Updating filearchive indices" );
                }
+               return true;
        }
 
        protected function doUniquePlTlIl() {
                $info = $this->db->indexInfo( 'pagelinks', 'pl_namespace' );
                if ( is_array( $info ) && !$info[0]->Non_unique ) {
                        $this->output( "...pl_namespace, tl_namespace, il_to indices are already UNIQUE.\n" );
-                       return;
+                       return true;
+               }
+               if ( $this->skipSchema ) {
+                       $this->output( "...skipping schema change (making pl_namespace, tl_namespace and il_to indices UNIQUE).\n" );
+                       return false;
                }
 
-               $this->applyPatch( 'patch-pl-tl-il-unique.sql', false, "Making pl_namespace, tl_namespace and il_to indices UNIQUE" );
+               return $this->applyPatch( 'patch-pl-tl-il-unique.sql', false, "Making pl_namespace, tl_namespace and il_to indices UNIQUE" );
        }
 
        protected function renameEuWikiId() {
@@ -848,6 +857,9 @@ class MysqlUpdater extends DatabaseUpdater {
 
        protected function doUserNewTalkTimestampNotNull() {
                $info = $this->db->fieldInfo( 'user_newtalk', 'user_last_timestamp' );
+               if ( $info === false ) {
+                       return;
+               }
                if ( $info->isNullable() ) {
                        $this->output( "...user_last_timestamp is already nullable.\n" );
                        return;
index ea649a6..b0371c0 100644 (file)
@@ -108,6 +108,12 @@ abstract class Maintenance {
         */
        private $mDb = null;
 
+       /**
+        * Used when creating separate schema files.
+        * @var resource
+        */
+       public $fileHandle;
+
        /**
         * List of all the core maintenance scripts. This is added
         * to scripts added by extensions in $wgMaintenanceScripts
index 6c57fde..27e692d 100644 (file)
@@ -48,6 +48,12 @@ class PopulateFilearchiveSha1 extends LoggedUpdateMaintenance {
                $dbw = wfGetDB( DB_MASTER );
                $table = 'filearchive';
                $conds = array( 'fa_sha1' => '', 'fa_storage_key IS NOT NULL' );
+
+               if ( !$dbw->fieldExists( $table, 'fa_sha1', __METHOD__ ) ) {
+                       $this->output( "fa_sha1 column does not exist\n\n", true );
+                       return false;
+               }
+
                $this->output( "Populating fa_sha1 field from fa_storage_key\n" );
                $endId = $dbw->selectField( $table, 'MAX(fa_id)', false, __METHOD__ );
 
index 07c395f..4827642 100644 (file)
@@ -48,7 +48,11 @@ class PopulateRevisionLength extends LoggedUpdateMaintenance {
                $db = $this->getDB( DB_MASTER );
                if ( !$db->tableExists( 'revision' ) ) {
                        $this->error( "revision table does not exist", true );
+               } else if ( !$db->fieldExists( 'revision', 'rev_sha1', __METHOD__ ) ) {
+                       $this->output( "rev_sha1 column does not exist\n\n", true );
+                       return false;
                }
+
                $this->output( "Populating rev_len column\n" );
 
                $start = $db->selectField( 'revision', 'MIN(rev_id)', false, __METHOD__ );
index 382b7be..113eef4 100644 (file)
@@ -48,6 +48,9 @@ class PopulateRevisionSha1 extends LoggedUpdateMaintenance {
                        $this->error( "revision table does not exist", true );
                } elseif ( !$db->tableExists( 'archive' ) ) {
                        $this->error( "archive table does not exist", true );
+               } else if ( !$db->fieldExists( 'revision', 'rev_sha1', __METHOD__ ) ) {
+                       $this->output( "rev_sha1 column does not exist\n\n", true );
+                       return false;
                }
 
                $this->output( "Populating rev_sha1 column\n" );
index 20e6068..ba1d8cd 100644 (file)
@@ -40,7 +40,6 @@ require_once( __DIR__ . '/Maintenance.php' );
  * @ingroup Maintenance
  */
 class UpdateMediaWiki extends Maintenance {
-
        function __construct() {
                parent::__construct();
                $this->mDescription = "MediaWiki database updater";
@@ -48,6 +47,8 @@ class UpdateMediaWiki extends Maintenance {
                $this->addOption( 'quick', 'Skip 5 second countdown before starting' );
                $this->addOption( 'doshared', 'Also update shared tables' );
                $this->addOption( 'nopurge', 'Do not purge the objectcache table after updates' );
+               $this->addOption( 'noschema', 'Only do the updates that are not done during schema updates' );
+               $this->addOption( 'schema', 'Output SQL to do the schema updates instead of doing them.  Works even when $wgAllowSchemaUpdates is false', false, true );
                $this->addOption( 'force', 'Override when $wgAllowSchemaUpdates disables this script' );
        }
 
@@ -83,10 +84,24 @@ class UpdateMediaWiki extends Maintenance {
        function execute() {
                global $wgVersion, $wgTitle, $wgLang, $wgAllowSchemaUpdates;
 
-               if( !$wgAllowSchemaUpdates && !$this->hasOption( 'force' ) ) {
+               if( !$wgAllowSchemaUpdates && !( $this->hasOption( 'force' ) || $this->hasOption( 'schema' ) || $this->hasOption( 'noschema' ) ) ) {
                        $this->error( "Do not run update.php on this wiki. If you're seeing this you should\n"
-                               . "probably ask for some help in performing your schema updates.\n\n"
-                               . "If you know what you are doing, you can continue with --force", true );
+                               . "probably ask for some help in performing your schema updates or use\n"
+                               . "the --noschema and --schema options to get an SQL file for someone\n"
+                               . "else to inspect and run.\n\n"
+                               . "If you know what you are doing, you can continue with --force\n", true );
+               }
+
+               $this->fileHandle = null;
+               if( substr( $this->getOption( 'schema' ), 0, 2 ) === "--" ) {
+                       $this->error( "The --schema option requires a file as an argument.\n", true );
+               } else if( $this->hasOption( 'schema' ) ) {
+                       $file = $this->getOption( 'schema' );
+                       $this->fileHandle = fopen( $file, "w" );
+                       if( $this->fileHandle === false ) {
+                               $err = error_get_last();
+                               $this->error( "Problem opening the schema file for writing: $file\n\t{$err['message']}", true );
+                       }
                }
 
                $wgLang = Language::factory( 'en' );
@@ -120,7 +135,17 @@ class UpdateMediaWiki extends Maintenance {
 
                $shared = $this->hasOption( 'doshared' );
 
-               $updates = array( 'core', 'extensions', 'stats' );
+               $updates = array( 'core', 'extensions' );
+               if( !$this->hasOption('schema') ) {
+                       if( $this->hasOption('noschema') ) {
+                               $updates[] = 'noschema';
+                       }
+                       $updates[] = 'stats';
+
+                       if( !$this->hasOption('nopurge') ) {
+                               $updates[] = 'purge';
+                       }
+               }
 
                $updater = DatabaseUpdater::newForDb( $db, $shared, $this );
                $updater->doUpdates( $updates );
@@ -134,6 +159,8 @@ class UpdateMediaWiki extends Maintenance {
                        if ( !$isLoggedUpdate && $updater->updateRowExists( $maint ) ) {
                                continue;
                        }
+
+                       $child = $this->runChild( $maint );
                        $child->execute();
                        if ( !$isLoggedUpdate ) {
                                $updater->insertUpdateRow( $maint );