Commit 8db37848b72 for woocommerce
commit 8db37848b7214053b32dbc8d070caf8cd4472589
Author: Sulaiman Dauda <abiodauda@gmail.com>
Date: Fri Oct 16 08:12:12 2026 +0100
Make Clear transients delete per-product and report transients (#69862)
* Fix Clear transients tool leaving per-product and report transients
* Add changelog entry for Clear transients tool fix
diff --git a/plugins/woocommerce/changelog/fix-clear-transients-tool b/plugins/woocommerce/changelog/fix-clear-transients-tool
new file mode 100644
index 00000000000..f5564192e0b
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-clear-transients-tool
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Make Status > Tools > Clear transients delete every product's cached data and cached reports, not just a few fixed entries.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php
index 8dd6cbc9993..86ecfc9d983 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php
@@ -11,6 +11,8 @@
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\ProductFilters\CacheController;
+use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
defined( 'ABSPATH' ) || exit;
@@ -557,6 +559,11 @@ class WC_REST_System_Status_Tools_V2_Controller extends WC_REST_Controller {
wc_get_container()->get( CacheController::class )->delete_filter_data_transients();
+ // These names end in a product ID or a hash, so they can only be found by prefix.
+ wc_get_container()->get( DatabaseUtil::class )->delete_transients_by_prefix(
+ array_merge( ProductUtil::PRODUCT_SPECIFIC_TRANSIENT_PREFIXES, array( 'wc_product_loop_', 'wc_report_' ) )
+ );
+
$message = __( 'Product transients cleared', 'woocommerce' );
break;
diff --git a/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php b/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php
index a94ae3b4bcd..6fbade9c168 100644
--- a/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php
+++ b/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php
@@ -437,4 +437,28 @@ $on_duplicate_clause
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_item_table is hardcoded.
return ! empty( $wpdb->get_results( "SHOW INDEX FROM $order_item_table WHERE Key_name = 'order_item_fts'" ) );
}
+
+ /**
+ * Delete the transients whose name starts with any of the given prefixes.
+ *
+ * Rows are deleted from the options table directly, so transients held in a persistent object cache are not affected.
+ *
+ * @since 11.3.0
+ *
+ * @param string[] $prefixes Transient name prefixes, without the "_transient_" part. Empty prefixes are ignored.
+ * @return void
+ */
+ public function delete_transients_by_prefix( array $prefixes ): void {
+ global $wpdb;
+
+ foreach ( array_filter( $prefixes ) as $prefix ) {
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
+ $wpdb->esc_like( '_transient_' . $prefix ) . '%',
+ $wpdb->esc_like( '_transient_timeout_' . $prefix ) . '%'
+ )
+ );
+ }
+ }
}
diff --git a/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php b/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
index ed1877b7163..8b44c63b24f 100644
--- a/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
+++ b/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
@@ -23,6 +23,24 @@ class ProductUtil {
*/
public const OUTOFSTOCK_COUNT_TRANSIENT = 'wc_outofstock_count';
+ /**
+ * Name prefixes of the transients kept for each product, followed by the product ID.
+ *
+ * Exposed as a constant so the "Clear transients" tool, which deletes these transients for
+ * every product, stays in sync with the per-product invalidation below.
+ *
+ * @since 11.3.0
+ *
+ * @var string[]
+ */
+ public const PRODUCT_SPECIFIC_TRANSIENT_PREFIXES = array(
+ 'wc_product_children_',
+ 'wc_var_prices_',
+ 'wc_related_',
+ 'wc_child_has_weight_',
+ 'wc_child_has_dimensions_',
+ );
+
/**
* Get a product ID from a product, post, or numeric value.
*
@@ -131,16 +149,8 @@ class ProductUtil {
$product_ids = array_unique( array_filter( array_map( 'absint', $product_ids ) ) );
- $product_specific_transient_names = array(
- 'wc_product_children_',
- 'wc_var_prices_',
- 'wc_related_',
- 'wc_child_has_weight_',
- 'wc_child_has_dimensions_',
- );
-
foreach ( $product_ids as $product_id ) {
- foreach ( $product_specific_transient_names as $transient ) {
+ foreach ( self::PRODUCT_SPECIFIC_TRANSIENT_PREFIXES as $transient ) {
delete_transient( $transient . $product_id );
}
}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller-test.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller-test.php
index dfeea681116..bd2e1fa5f80 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller-test.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller-test.php
@@ -1,5 +1,7 @@
<?php
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query as OrdersStatsQuery;
+
/**
* Class WC_REST_System_Status_Tools_V2_Controller.
*/
@@ -146,6 +148,62 @@ class WC_REST_System_Status_Tools_V2_Controller_Test extends WC_REST_Unit_Test_C
$this->assertNotEquals( $prefix_before, $prefix_after, 'Cache prefix should have changed after invalidation' );
}
+ /**
+ * @testdox Clear transients tool deletes the per-product, product shortcode and report transients, and keeps other transients.
+ */
+ public function test_execute_tool_clear_transients_deletes_transients_of_every_product() {
+ wp_set_current_user( $this->user );
+
+ $product = WC_Helper_Product::create_variation_product();
+ $product_id = $product->get_id();
+ $product->get_variation_prices();
+ $product->child_has_weight();
+ $product->child_has_dimensions();
+ wc_get_related_products( $product_id );
+ do_shortcode( '[products]' );
+ ( new OrdersStatsQuery( array() ) )->get_data();
+ set_transient( 'wc_facebook_test', 'kept', DAY_IN_SECONDS );
+ set_transient( 'other_plugin_cache', 'kept', DAY_IN_SECONDS );
+
+ $core_transients = array(
+ 'wc_var_prices_' . $product_id,
+ 'wc_product_children_' . $product_id,
+ 'wc_child_has_weight_' . $product_id,
+ 'wc_child_has_dimensions_' . $product_id,
+ 'wc_related_' . $product_id,
+ 'wc_product_loop_',
+ 'wc_report_orders_stats_',
+ );
+ foreach ( $core_transients as $name ) {
+ $this->assertNotEmpty( $this->get_option_names_starting_with( '_transient_' . $name ), "Arrange should create the $name transient" );
+ $this->assertNotEmpty( $this->get_option_names_starting_with( '_transient_timeout_' . $name ), "Arrange should create the $name transient timeout" );
+ }
+
+ $response = $this->server->dispatch( new WP_REST_Request( 'PUT', '/wc/v2/system_status/tools/clear_transients' ) );
+
+ $this->assertTrue( $response->get_data()['success'], 'The tool should report success' );
+ foreach ( $core_transients as $name ) {
+ $this->assertSame( array(), $this->get_option_names_starting_with( '_transient_' . $name ), "The $name transient should be deleted" );
+ $this->assertSame( array(), $this->get_option_names_starting_with( '_transient_timeout_' . $name ), "The $name transient timeout should be deleted" );
+ }
+ foreach ( array( 'wc_facebook_test', 'other_plugin_cache' ) as $name ) {
+ $this->assertSame( array( "_transient_$name" ), $this->get_option_names_starting_with( "_transient_$name" ), "Another plugin's $name transient should be kept" );
+ $this->assertSame( array( "_transient_timeout_$name" ), $this->get_option_names_starting_with( "_transient_timeout_$name" ), "Another plugin's $name transient timeout should be kept" );
+ }
+ }
+
+ /**
+ * Get the names of the rows in the options table whose name starts with a given string.
+ *
+ * @param string $prefix Start of the option name.
+ * @return string[] Matching option names.
+ */
+ private function get_option_names_starting_with( string $prefix ): array {
+ global $wpdb;
+
+ return $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( $prefix ) . '%' ) );
+ }
+
/**
* @testdox Product lookup table regeneration status is shown alongside the matching tool.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/Utilities/DatabaseUtilTest.php b/plugins/woocommerce/tests/php/src/Internal/Utilities/DatabaseUtilTest.php
index 9780f361b8b..f380d49859a 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Utilities/DatabaseUtilTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Utilities/DatabaseUtilTest.php
@@ -159,4 +159,29 @@ class DatabaseUtilTest extends \WC_Unit_Test_Case {
$this->assertEquals( $expected, $this->sut->sanitise_boolean_fts_search_term( $term ) );
}
}
+
+ /**
+ * @testdox delete_transients_by_prefix() deletes the value and timeout rows of transients whose name starts with a prefix, and ignores empty prefixes.
+ */
+ public function test_delete_transients_by_prefix(): void {
+ global $wpdb;
+
+ set_transient( 'wc_test_one_1', 'deleted', DAY_IN_SECONDS );
+ set_transient( 'wc_test_two_abc', 'deleted', DAY_IN_SECONDS );
+ set_transient( 'wc_testXone_1', 'kept, as the underscores in a prefix are not wildcards', DAY_IN_SECONDS );
+ set_transient( 'wc_test_three_1', 'kept', DAY_IN_SECONDS );
+
+ $this->sut->delete_transients_by_prefix( array( 'wc_test_one_', '', 'wc_test_two_' ) );
+
+ $this->assertEqualsCanonicalizing(
+ array(
+ '_transient_wc_testXone_1',
+ '_transient_timeout_wc_testXone_1',
+ '_transient_wc_test_three_1',
+ '_transient_timeout_wc_test_three_1',
+ ),
+ $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", '%' . $wpdb->esc_like( 'wc_test' ) . '%' ) ),
+ 'Only the transients starting with a given prefix should be deleted'
+ );
+ }
}