diff --git a/features/search-replace.feature b/features/search-replace.feature index d6feee8a..6cdcd8bb 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -1573,3 +1573,21 @@ 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 --all-tables-with-prefix` + 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 bbad1bdf..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 */ @@ -685,6 +693,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 ) { @@ -763,14 +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 ); + $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; } }