From 82068dc71612bb4faa355b4cca77b17751841600 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 07:46:39 +0000 Subject: [PATCH 1/4] Initial plan From 005dc9b9d84da054cf89b908d7b137a763f53efd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 07:51:07 +0000 Subject: [PATCH 2/4] Alert user of DB table permission errors via WP_CLI::warning() Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/search-replace.feature | 14 ++++++++++++++ src/Search_Replace_Command.php | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/features/search-replace.feature b/features/search-replace.feature index d6feee8a..3cd52e4a 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -1573,3 +1573,17 @@ Feature: Do global search/replace """ --old-content """ + + + @require-mysql + Scenario: Warn when updating a table fails due to a database error + Given a WP install + And I run `wp db query "CREATE TABLE wp_readonly_test ( id int(11) unsigned NOT NULL AUTO_INCREMENT, data TEXT, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"` + And I run `wp db query "INSERT INTO wp_readonly_test (data) VALUES ('old-value');"` + And I run `wp db query "CREATE TRIGGER prevent_update BEFORE UPDATE ON wp_readonly_test FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Table is read-only';"` + + When I try `wp search-replace old-value new-value wp_readonly_test --all-tables-with-prefix` + Then STDERR should contain: + """ + Error updating column 'data' in table 'wp_readonly_test' + """ diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index bbad1bdf..159454f4 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -685,6 +685,9 @@ private function sql_handle_col( $col, $primary_keys, $table, $old, $new ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident $count = (int) $wpdb->query( $wpdb->prepare( "UPDATE $table_sql SET $col_sql = REPLACE($col_sql, %s, %s);", $old, $new ) ); } + if ( $wpdb->last_error ) { + WP_CLI::warning( sprintf( "Error updating column '%s' in table '%s': %s", $col, $table, $wpdb->last_error ) ); + } } if ( $this->verbose && 'table' === $this->format ) { @@ -771,6 +774,9 @@ static function ( $key ) { } $wpdb->update( $table, [ $col => $value ], $update_where ); + if ( $wpdb->last_error ) { + WP_CLI::warning( sprintf( "Error updating column '%s' in table '%s': %s", $col, $table, $wpdb->last_error ) ); + } } } From f6138cae319665ebce7c48d77e9521297a7e4a17 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 16 Mar 2026 09:50:48 +0100 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- features/search-replace.feature | 6 +++++- src/Search_Replace_Command.php | 37 ++++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/features/search-replace.feature b/features/search-replace.feature index 3cd52e4a..2ae9dd0a 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -1582,8 +1582,12 @@ Feature: Do global search/replace And I run `wp db query "INSERT INTO wp_readonly_test (data) VALUES ('old-value');"` And I run `wp db query "CREATE TRIGGER prevent_update BEFORE UPDATE ON wp_readonly_test FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Table is read-only';"` - When I try `wp search-replace old-value new-value wp_readonly_test --all-tables-with-prefix` + When I try `wp search-replace old-value new-value wp_readonly_test` Then STDERR should contain: """ Error updating column 'data' in table 'wp_readonly_test' """ + And STDERR should contain: + """ + Table is read-only + """ diff --git a/src/Search_Replace_Command.php b/src/Search_Replace_Command.php index 159454f4..58ac2247 100644 --- a/src/Search_Replace_Command.php +++ b/src/Search_Replace_Command.php @@ -19,6 +19,14 @@ class Search_Replace_Command extends WP_CLI_Command { */ private $export_handle = false; + /** + * Tracks table/column combinations that have encountered update errors, + * so we can avoid repeated failing updates and noisy per-row warnings. + * + * @var array + */ + private $update_error_columns = array(); + /** * @var int */ @@ -766,17 +774,36 @@ static function ( $key ) { $replacer->clear_log_data(); } - ++$count; - if ( ! $this->dry_run ) { + // If we've already seen an update error for this table/column and are not in dry-run, + // skip further attempts to avoid repeated failures and noisy warnings. + if ( ! $this->dry_run && ! empty( $this->update_error_columns[ $table ][ $col ] ) ) { + continue; + } + + if ( $this->dry_run ) { + // In dry-run mode, count replacements once a change has been detected. + ++$count; + } else { $update_where = array(); foreach ( (array) $keys as $k => $v ) { $update_where[ $k ] = $v; } - $wpdb->update( $table, [ $col => $value ], $update_where ); - if ( $wpdb->last_error ) { - WP_CLI::warning( sprintf( "Error updating column '%s' in table '%s': %s", $col, $table, $wpdb->last_error ) ); + $result = $wpdb->update( $table, array( $col => $value ), $update_where ); + if ( false === $result ) { + if ( empty( $this->update_error_columns[ $table ][ $col ] ) ) { + $this->update_error_columns[ $table ][ $col ] = true; + if ( $wpdb->last_error ) { + WP_CLI::warning( sprintf( "Error updating column '%s' in table '%s': %s", $col, $table, $wpdb->last_error ) ); + } else { + WP_CLI::warning( sprintf( "Error updating column '%s' in table '%s'.", $col, $table ) ); + } + } + continue; } + + // Only count successful updates. + ++$count; } } From 8959809954be4ca7964efed78624fb618ea67e03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:10:00 +0000 Subject: [PATCH 4/4] Fix Behat test: use --all-tables-with-prefix instead of positional table name Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/search-replace.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/search-replace.feature b/features/search-replace.feature index 2ae9dd0a..6cdcd8bb 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -1582,7 +1582,7 @@ Feature: Do global search/replace And I run `wp db query "INSERT INTO wp_readonly_test (data) VALUES ('old-value');"` And I run `wp db query "CREATE TRIGGER prevent_update BEFORE UPDATE ON wp_readonly_test FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Table is read-only';"` - When I try `wp search-replace old-value new-value wp_readonly_test` + When I try `wp search-replace old-value new-value --all-tables-with-prefix` Then STDERR should contain: """ Error updating column 'data' in table 'wp_readonly_test'