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
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.
=== 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
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,
*/
private $mTrxAutomatic = false;
+ /**
+ * @since 1.21
+ * @var file handle for upgrade
+ */
+ protected $fileHandle = null;
+
+
# ------------------------------------------------------------------------------
# Accessors
# ------------------------------------------------------------------------------
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.
* @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;
$options = array( $options );
}
+ $fh = null;
+ if ( isset( $options['fileHandle'] ) ) {
+ $fh = $options['fileHandle'];
+ }
$options = $this->makeInsertOptions( $options );
if ( isset( $a[0] ) && is_array( $a[0] ) ) {
$sql .= '(' . $this->makeList( $a ) . ')';
}
+ if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
+ return false;
+ } elseif ( $fh !== null ) {
+ return true;
+ }
+
return (bool)$this->query( $sql, $fname );
}
* @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' );
}
try {
- $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname );
+ $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
}
catch ( MWException $e ) {
fclose( $fp );
* 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,
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 = '';
}
}
# 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 );
$result[] = $row;
}
}
-
return empty( $result ) ? false : $result;
}
*/
protected $updates = array();
+ /**
+ * Array of updates that were skipped
+ *
+ * @var array
+ */
+ protected $updatesSkipped = array();
+
/**
* List of extension-provided database updates
* @var array
'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
*
$this->shared = $shared;
if ( $maintenance ) {
$this->maintenance = $maintenance;
+ $this->fileHandle = $maintenance->fileHandle;
} else {
$this->maintenance = new FakeMaintenance;
}
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 );
}
$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__ );
}
* 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 ) ) {
} 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 );
}
/**
*/
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;
}
/**
* @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;
}
/**
* @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__ ) ) {
} 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;
}
/**
* @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;
}
/**
* @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;
}
/**
* @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;
}
/**
* @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__ ) ) {
$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;
}
/**
* @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";
} 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;
}
/**
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" );
}
/**
*/
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" );
+ }
}
/**
// 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' ),
*/
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;
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" );
}
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() {
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;
*/
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
$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__ );
$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__ );
$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" );
* @ingroup Maintenance
*/
class UpdateMediaWiki extends Maintenance {
-
function __construct() {
parent::__construct();
$this->mDescription = "MediaWiki database updater";
$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' );
}
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' );
$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 );
if ( !$isLoggedUpdate && $updater->updateRowExists( $maint ) ) {
continue;
}
+
+ $child = $this->runChild( $maint );
$child->execute();
if ( !$isLoggedUpdate ) {
$updater->insertUpdateRow( $maint );