Skip to content
Merged
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"commands": [
"core",
"core check-update",
"core check-update-db",
"core download",
"core install",
"core is-installed",
Expand Down
99 changes: 99 additions & 0 deletions features/core-check-update-db.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
Feature: Check if WordPress database update is needed

# This test downgrades to an older WordPress version, but the SQLite plugin requires 6.0+
@require-mysql
Scenario: Check if database update is needed on a single site
Given a WP install
And a disable_sidebar_check.php file:
"""
<?php
WP_CLI::add_wp_hook( 'init', static function () {
remove_action( 'after_switch_theme', '_wp_sidebars_changed' );
} );
"""
And I try `wp theme install twentytwenty --activate`
And I run `wp core download --version=5.4 --force`
And I run `wp option update db_version 45805 --require=disable_sidebar_check.php`

When I try `wp core check-update-db`
Then the return code should be 1
And STDOUT should contain:
"""
WordPress database update required from db version 45805 to 47018.
"""

When I run `wp core update-db`
Then STDOUT should contain:
"""
Success: WordPress database upgraded successfully from db version 45805 to 47018.
"""

When I run `wp core check-update-db`
Then STDOUT should contain:
"""
Success: WordPress database is up to date.
"""

Scenario: Check if database update is needed when database is already up to date
Given a WP install

When I run `wp core check-update-db`
Then STDOUT should contain:
"""
Success: WordPress database is up to date.
"""

Scenario: Check if database update is needed across network
Given a WP multisite install
And a disable_sidebar_check.php file:
"""
<?php
WP_CLI::add_wp_hook( 'init', static function () {
remove_action( 'after_switch_theme', '_wp_sidebars_changed' );
} );
"""
And I try `wp theme install twentytwenty --activate`
And I run `wp core download --version=6.6 --force`
And I run `wp option update db_version 57155 --require=disable_sidebar_check.php`
And I run `wp site option update wpmu_upgrade_site 57155`
And I run `wp site create --slug=foo`
And I run `wp site create --slug=bar`
And I run `wp site create --slug=burrito --porcelain`
And save STDOUT as {BURRITO_ID}
And I run `wp site create --slug=taco --porcelain`
And save STDOUT as {TACO_ID}
And I run `wp site create --slug=pizza --porcelain`
And save STDOUT as {PIZZA_ID}
And I run `wp site archive {BURRITO_ID}`
And I run `wp site spam {TACO_ID}`
And I run `wp site delete {PIZZA_ID} --yes`
And I run `wp core update`

When I try `wp core check-update-db --network`
Then the return code should be 1
And STDOUT should contain:
"""
WordPress database update needed on 3/3 sites:
"""

When I run `wp core update-db --network`
Then STDOUT should contain:
"""
Success: WordPress database upgraded on 3/3 sites.
"""

When I run `wp core check-update-db --network`
Then STDOUT should contain:
"""
Success: WordPress databases are up to date on 3/3 sites.
"""

Scenario: Check database update on network installation errors on single site
Given a WP install

When I try `wp core check-update-db --network`
Then STDERR should contain:
"""
Error: This is not a multisite installation.
"""
And the return code should be 1
102 changes: 102 additions & 0 deletions src/Core_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,108 @@ static function () {
}
}

/**
* Checks for the need for WordPress database updates.
*
* Compares the current database version with the version required by WordPress core
* to determine if database updates are needed.
*
* ## OPTIONS
*
* [--network]
* : Check databases for all sites on a network.
*
* ## EXAMPLES
*
* # Check if database update is needed
* $ wp core check-update-db
* Success: WordPress database is up to date.
*
* # Check database update status for all sites on a network
* $ wp core check-update-db --network
* Success: WordPress databases are up to date on 5/5 sites.
*
* @subcommand check-update-db
*
* @param string[] $args Positional arguments. Unused.
* @param array{network?: bool} $assoc_args Associative arguments.
*/
public function check_update_db( $args, $assoc_args ) {
global $wpdb, $wp_db_version, $wp_current_db_version;

$network = Utils\get_flag_value( $assoc_args, 'network' );
if ( $network && ! is_multisite() ) {
WP_CLI::error( 'This is not a multisite installation.' );
}

if ( $network ) {
$iterator_args = [
'table' => $wpdb->blogs,
'where' => [
'spam' => 0,
'deleted' => 0,
'archived' => 0,
],
];
$it = new TableIterator( $iterator_args );
$total = 0;
$needs_update = 0;
$sites_needing_update = [];

/**
* @var object{site_id: int, domain: string, path: string} $blog
*/
foreach ( $it as $blog ) {
++$total;
$url = $blog->domain . $blog->path;
$cmd = "--url={$url} core check-update-db";

/**
* @var object{stdout: string, stderr: string, return_code: int} $process
*/
$process = WP_CLI::runcommand(
$cmd,
[
'return' => 'all',
'exit_error' => false,
]
);
// If return code is 1, it means update is needed
if ( 1 === (int) $process->return_code ) {
++$needs_update;
$sites_needing_update[] = $url;
}
}

if ( $needs_update > 0 ) {
WP_CLI::log( "WordPress database update needed on {$needs_update}/{$total} sites:" );
foreach ( $sites_needing_update as $site_url ) {
WP_CLI::log( " - {$site_url}" );
}
WP_CLI::halt( 1 );
} else {
WP_CLI::success( "WordPress databases are up to date on {$total}/{$total} sites." );
}
Comment on lines +1352 to +1399

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation for network-wide checks spawns a new wp process for each site by calling WP_CLI::runcommand(). This can be very inefficient on multisite installations with a large number of sites. A more performant approach would be to iterate through the sites using switch_to_blog() and check the db_version option directly. This avoids the overhead of process creation for each site.

Additionally, the PHPDoc for the $blog object incorrectly states site_id while it should be blog_id, as the iterator is over $wpdb->blogs. The suggested change corrects this and uses the proper blog_id.

if ( $network ) {
			require_once ABSPATH . 'wp-admin/includes/upgrade.php';

			$iterator_args        = [
				'table' => $wpdb->blogs,
				'where' => [
					'spam'     => 0,
					'deleted'  => 0,
					'archived' => 0,
				],
			];
			$it                   = new TableIterator( $iterator_args );
			$total                = 0;
			$needs_update         = 0;
			$sites_needing_update = [];

			/**
			 * @var object{blog_id: int, domain: string, path: string} $blog
			 */
			foreach ( $it as $blog ) {
				++$total;
				switch_to_blog( $blog->blog_id );
				$site_db_version = (int) get_option( 'db_version' );
				restore_current_blog();

				if ( $wp_db_version !== $site_db_version ) {
					++$needs_update;
					$sites_needing_update[] = $blog->domain . $blog->path;
				}
			}

			if ( $needs_update > 0 ) {
				WP_CLI::log( "WordPress database update needed on {$needs_update}/{$total} sites:" );
				foreach ( $sites_needing_update as $site_url ) {
					WP_CLI::log( "  - {$site_url}" );
				}
				WP_CLI::halt( 1 );
			} else {
				WP_CLI::success( "WordPress databases are up to date on {$total}/{$total} sites." );
			}
		}

} else {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';

/**
* @var string $wp_current_db_version
*/
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Replacing WP Core behavior is the goal here.
$wp_current_db_version = __get_option( 'db_version' );
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Replacing WP Core behavior is the goal here.
$wp_current_db_version = (int) $wp_current_db_version;
Comment on lines +1403 to +1409

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type hint for $wp_current_db_version is @var string, but the variable is immediately cast to an integer. It would be clearer to declare it as @var int. Additionally, the two lines for getting and casting the db_version can be combined into one for conciseness, which also simplifies the phpcs:ignore directives.

			/**
			 * @var int $wp_current_db_version
			 */
			// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Replacing WP Core behavior is the goal here.
			$wp_current_db_version = (int) __get_option( 'db_version' );


if ( $wp_db_version !== $wp_current_db_version ) {
WP_CLI::log( "WordPress database update required from db version {$wp_current_db_version} to {$wp_db_version}." );
WP_CLI::halt( 1 );
} else {
WP_CLI::success( 'WordPress database is up to date.' );
}
}
}

/**
* Runs the WordPress database update procedure.
*
Expand Down