Commit 19693ec5146 for woocommerce

commit 19693ec51462ee7131cce7d59462e9aabe727a49
Author: Dekadinious <37292177+Dekadinious@users.noreply.github.com>
Date:   Mon Jun 1 16:05:14 2026 +0200

    Fix notoptions bloat from variable product cache priming (#65440)

    * Skip transient option cache priming for variable products under a persistent object cache

    WC_Product_Variable_Data_Store_CPT::read_product_data() primed the
    _transient_wc_var_prices_* and _transient_wc_product_children_* option
    names via wp_prime_option_caches() on every variable product read. With a
    persistent object cache these transients live in the 'transient' cache
    group, not in wp_options, so the prime never finds them and records each
    name in core's notoptions cache permanently. notoptions then grows by four
    entries per variable product across the catalog and is never cleared.

    Guard the prime with ! wp_using_ext_object_cache(), matching the condition
    WordPress uses to choose between the options table and the object cache.
    Sites without a persistent object cache keep the existing query batching.

    This mirrors the 10.8.0 change that removed transients from
    WC_Order::needs_processing() for the same reason.

    * Fix object-cache global leaking from the new test into later tests

    wp_using_ext_object_cache( $previous ) is a no-op when $previous is null,
    so the simulated `true` state was not reset and leaked into other tests in
    the same suite, causing unrelated transient and object-cache assertions to
    fail. Restore the flag inside a finally block and cast to bool so it is
    always reset to a concrete value.

diff --git a/plugins/woocommerce/changelog/fix-variable-product-notoptions-prime b/plugins/woocommerce/changelog/fix-variable-product-notoptions-prime
new file mode 100644
index 00000000000..21b8b353e10
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-variable-product-notoptions-prime
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Avoid priming transient option caches for variable products when a persistent object cache is active, which prevents unbounded growth of the notoptions cache.
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
index 6a40709a906..3e9b88d9122 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
@@ -128,16 +128,25 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 	 * @since 3.0.0
 	 */
 	protected function read_product_data( &$product ) {
-		// Prime caches to reduce future queries.
-		$product_id = $product->get_id();
-		wp_prime_option_caches(
-			array(
-				'_transient_wc_var_prices_' . $product_id,
-				'_transient_timeout_wc_var_prices_' . $product_id,
-				'_transient_wc_product_children_' . $product_id,
-				'_transient_timeout_wc_product_children_' . $product_id,
-			)
-		);
+		// Only prime the transient option caches when there is no persistent object
+		// cache. When one is active, get_transient() reads these values from the
+		// 'transient' cache group and never from wp_options, so priming the option
+		// names cannot help. Instead each missing name is recorded in core's
+		// 'notoptions' cache, and because the options never exist the entries are
+		// never cleared, so notoptions grows by four per variable product across the
+		// catalog. WC_Order::needs_processing() stopped using transients for the same
+		// reason in 10.8.0.
+		if ( ! wp_using_ext_object_cache() ) {
+			$product_id = $product->get_id();
+			wp_prime_option_caches(
+				array(
+					'_transient_wc_var_prices_' . $product_id,
+					'_transient_timeout_wc_var_prices_' . $product_id,
+					'_transient_wc_product_children_' . $product_id,
+					'_transient_timeout_wc_product_children_' . $product_id,
+				)
+			);
+		}

 		parent::read_product_data( $product );

diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php
index 33ce8ff95ee..12eff53c3f4 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php
@@ -783,4 +783,49 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		}
 		$product->delete();
 	}
+
+	/**
+	 * @testdox read_product_data does not record variable product transient names in the notoptions cache when a persistent object cache is in use.
+	 */
+	public function test_read_product_data_does_not_prime_transients_with_object_cache() {
+		$product    = WC_Helper_Product::create_variation_product();
+		$product_id = $product->get_id();
+
+		$option_names = array(
+			'_transient_wc_var_prices_' . $product_id,
+			'_transient_timeout_wc_var_prices_' . $product_id,
+			'_transient_wc_product_children_' . $product_id,
+			'_transient_timeout_wc_product_children_' . $product_id,
+		);
+
+		// Simulate a persistent object cache and start from a clean notoptions cache.
+		$previous = wp_using_ext_object_cache( true );
+
+		try {
+			wp_cache_delete( 'notoptions', 'options' );
+
+			// Force a fresh read so read_product_data() runs.
+			$data_store = new WC_Product_Variable_Data_Store_CPT();
+			$fresh      = new WC_Product_Variable();
+			$fresh->set_id( $product_id );
+			$data_store->read( $fresh );
+
+			$notoptions = wp_cache_get( 'notoptions', 'options' );
+		} finally {
+			// Always restore. Cast to bool because wp_using_ext_object_cache( null ) is a
+			// no-op, which would otherwise leak the simulated true state into later tests.
+			wp_using_ext_object_cache( (bool) $previous );
+		}
+
+		$notoptions = is_array( $notoptions ) ? $notoptions : array();
+		foreach ( $option_names as $option_name ) {
+			$this->assertArrayNotHasKey(
+				$option_name,
+				$notoptions,
+				'Variable product transient option names must not be added to notoptions when a persistent object cache is active.'
+			);
+		}
+
+		$product->delete();
+	}
 }