Commit fa4e8940953 for woocommerce

commit fa4e89409538b4b70c8829d50ab7d6539ba0cafb
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Wed Jul 22 08:35:53 2026 +0200

    [Performance] Audit variable product datastore (#66084)

    - addressed N-query anti-pattern in \WC_Product_Variable_Data_Store_CPT::read_variation_attributes
    - optimized SQL queries and inefficient workflows
    - added test coverage focusing on the modified methods

diff --git a/plugins/woocommerce/changelog/performance-audit-variable-product-datastore b/plugins/woocommerce/changelog/performance-audit-variable-product-datastore
new file mode 100644
index 00000000000..8e8d60e928f
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-audit-variable-product-datastore
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Audit the variable product datastore and implement optimizations to improve SQL query performance.
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 0bd489a0140..ab354b20f03 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
@@ -55,7 +55,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 				);

 				// Maintain data integrity: WordPress 4.9 changed sanitization functions, and we update the values here so variations function correctly.
-				// As per 2026, we are refactoring the updates into product-level: BC-focused (not all-in on-spot migration), optimized for performance.
+				// Since 10.6.0, we are refactoring the updates into product-level: BC-focused (not all-in on-spot migration), optimized for performance.
 				// Use-case: `_product_attributes` has data populated on WordPress pre-4.8 and containing symbols affected by the breaking changes.
 				if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) {
 					global $wpdb;
@@ -87,6 +87,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 					if ( ! taxonomy_exists( $meta_value['name'] ) ) {
 						continue;
 					}
+					// Performance note: at this stage, the product factory has already primed the caches, so wc_get_object_terms does not present a concern.
 					$id      = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
 					$options = wc_get_object_terms( $product_id, $meta_value['name'], 'term_id' );
 				} else {
@@ -235,44 +236,46 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 	public function read_variation_attributes( &$product ) {
 		global $wpdb;

-		$variation_attributes = array();
-		$attributes           = $product->get_attributes();
-		$child_ids            = $product->get_children();
-		$cache_key            = WC_Cache_Helper::get_cache_prefix( 'product_' . $product->get_id() ) . 'product_variation_attributes_' . $product->get_id();
-		$cache_group          = 'products';
-		$cached_data          = wp_cache_get( $cache_key, $cache_group );
+		$product_id  = $product->get_id();
+		$cache_key   = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . 'product_variation_attributes_' . $product_id;
+		$cached_data = wp_cache_get( $cache_key, 'products' );

 		if ( false !== $cached_data ) {
 			return $cached_data;
 		}

+		$variation_attributes = array();
+		$attributes           = $product->get_attributes();
+
 		if ( ! empty( $attributes ) ) {
-			foreach ( $attributes as $attribute ) {
-				if ( empty( $attribute['is_variation'] ) ) {
-					continue;
+			$child_ids  = $product->get_children();
+			$attributes = array_values( array_filter( $attributes, static fn ( $attribute ) => ! empty( $attribute['is_variation'] ) ) );
+
+			$attributes_values = array();
+			if ( ! empty( $child_ids ) && ! empty( $attributes ) ) {
+				// Performance note: for optimal performance, retrieve attribute values using a single SQL query.
+				$attributes_placeholders = implode( ', ', array_fill( 0, count( $attributes ), '%s' ) );
+				$children_placeholders   = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
+				$prefetch                = $wpdb->get_results(
+					$wpdb->prepare(
+						// Performance: DISTINCT + CAST(meta_value AS BINARY(200)) is feasible but forces a temp table — keep array_unique.
+						// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+						"SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id IN ( {$children_placeholders} ) AND meta_key IN ( {$attributes_placeholders} )",
+						...$child_ids,
+						...array_map( static fn( $attribute ) => wc_variation_attribute_name( $attribute['name'] ), $attributes )
+					)
+				);
+				foreach ( $prefetch as $row ) {
+					$attributes_values[ $row->meta_key ][] = $row->meta_value;
 				}
+			}

-				// Get possible values for this attribute, for only visible variations.
-				if ( ! empty( $child_ids ) ) {
-					$format     = array_fill( 0, count( $child_ids ), '%d' );
-					$query_in   = '(' . implode( ',', $format ) . ')';
-					$query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids;
-					$values     = array_unique(
-						$wpdb->get_col(
-							$wpdb->prepare(
-								// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-								"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine.
-								$query_args
-							)
-						)
-					);
-				} else {
-					$values = array();
-				}
+			foreach ( $attributes as $attribute ) {
+				$values = $attributes_values[ wc_variation_attribute_name( $attribute['name'] ) ] ?? array();

 				// Empty value indicates that all options for given attribute are available.
-				if ( in_array( null, $values, true ) || in_array( '', $values, true ) || empty( $values ) ) {
-					$values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] );
+				if ( empty( $values ) || in_array( null, $values, true ) || in_array( '', $values, true ) ) {
+					$values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product_id, $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] );
 					// Get custom attributes (non taxonomy) as defined.
 				} elseif ( ! $attribute['is_taxonomy'] ) {
 					$text_attributes          = wc_get_text_attributes( $attribute['value'] );
@@ -280,7 +283,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 					$values                   = array();

 					// Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
-					if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
+					if ( version_compare( get_post_meta( $product_id, '_product_version', true ), '2.4.0', '<' ) ) {
 						$assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes );
 						foreach ( $text_attributes as $text_attribute ) {
 							if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) {
@@ -299,7 +302,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 			}
 		}

-		wp_cache_set( $cache_key, $variation_attributes, $cache_group );
+		wp_cache_set( $cache_key, $variation_attributes, 'products' );

 		return $variation_attributes;
 	}
@@ -700,16 +703,21 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 	 */
 	public function child_has_weight( $product ) {
 		global $wpdb;
-		$children = $product->get_visible_children();
-		if ( ! $children ) {
-			return false;
-		}

-		$format   = array_fill( 0, count( $children ), '%d' );
-		$query_in = '(' . implode( ',', $format ) . ')';
+		$has_weight = false;
+		$child_ids  = $product->get_visible_children();
+		if ( ! empty( $child_ids ) ) {
+			$placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
+			$has_weight   = (bool) $wpdb->get_var(
+				$wpdb->prepare(
+					// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+					"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN ( {$placeholders} ) LIMIT 1",
+					$child_ids
+				)
+			);
+		}

-		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
-		return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN {$query_in}", $children ) );
+		return $has_weight;
 	}

 	/**
@@ -722,16 +730,21 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 	 */
 	public function child_has_dimensions( $product ) {
 		global $wpdb;
-		$children = $product->get_visible_children();
-		if ( ! $children ) {
-			return false;
-		}

-		$format   = array_fill( 0, count( $children ), '%d' );
-		$query_in = '(' . implode( ',', $format ) . ')';
+		$has_dimensions = false;
+		$child_ids      = $product->get_visible_children();
+		if ( ! empty( $child_ids ) ) {
+			$placeholders   = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
+			$has_dimensions = (bool) $wpdb->get_var(
+				$wpdb->prepare(
+					// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+					"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN ( {$placeholders} ) LIMIT 1",
+					$child_ids
+				)
+			);
+		}

-		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
-		return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN {$query_in}", $children ) );
+		return $has_dimensions;
 	}

 	/**
@@ -758,30 +771,20 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 	public function child_has_stock_status( $product, $status ) {
 		global $wpdb;

-		$children = $product->get_children();
-
-		if ( $children ) {
-			$format     = array_fill( 0, count( $children ), '%d' );
-			$query_in   = '(' . implode( ',', $format ) . ')';
-			$query_args = array( 'stock_status' => $status ) + $children;
-			// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
+		$has_matches = false;
+		$child_ids   = $product->get_children();
+		if ( ! empty( $child_ids ) ) {
+			$placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
 			if ( get_option( 'woocommerce_product_lookup_table_is_generating' ) ) {
-				$query = "SELECT COUNT( post_id ) FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN {$query_in}";
+				$query = "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN ( {$placeholders} ) LIMIT 1";
 			} else {
-				$query = "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN {$query_in}";
+				$query = "SELECT product_id FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN ( {$placeholders} ) LIMIT 1";
 			}
-			$children_with_status = $wpdb->get_var(
-				$wpdb->prepare(
-					$query,
-					$query_args
-				)
-			);
-			// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
-		} else {
-			$children_with_status = 0;
+			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+			$has_matches = (bool) $wpdb->get_var( $wpdb->prepare( $query, $status, ...$child_ids ) );
 		}

-		return (bool) $children_with_status;
+		return $has_matches;
 	}

 	/**
@@ -831,32 +834,51 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 		global $wpdb;

 		if ( $product->get_manage_stock() ) {
-			$children    = $product->get_children();
-			$changed     = false;
-			$invalidator = wc_get_container()->get( ProductVersionStringInvalidator::class );
+			$child_ids = $product->get_children();
+			if ( ! empty( $child_ids ) ) {
+				$status       = $product->get_stock_status();
+				$placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
+				// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+				$children_to_sync     = $wpdb->get_col(
+					$wpdb->prepare(
+						"SELECT post_id
+						FROM {$wpdb->postmeta}
+						WHERE meta_key = '_stock_status'
+							AND meta_value != %s
+							AND post_id IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN ( {$placeholders} ) )",
+						$status,
+						...$child_ids
+					)
+				);
+				$children_to_backfill = $wpdb->get_col(
+					$wpdb->prepare(
+						"SELECT post_id
+						FROM {$wpdb->postmeta}
+						WHERE meta_key = '_manage_stock'
+							AND meta_value != 'yes'
+							AND post_id IN ( {$placeholders} )
+							AND post_id NOT IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value IS NOT NULL AND post_id IN ( {$placeholders} ) )",
+						...$child_ids,
+						...$child_ids
+					)
+				);
+				// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

-			if ( $children ) {
-				$status   = $product->get_stock_status();
-				$format   = array_fill( 0, count( $children ), '%d' );
-				$query_in = '(' . implode( ',', $format ) . ')';
-				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
-				$managed_children = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN {$query_in}", $children ) ) );
-				foreach ( $managed_children as $managed_child ) {
-					if ( update_post_meta( $managed_child, '_stock_status', $status ) ) {
-						$this->update_lookup_table( $managed_child, 'wc_product_meta_lookup' );
-						$changed = true;
-
-						$invalidator->invalidate( $managed_child );
-					}
-				}
-			}
+				$children_to_sync = array_unique( array_merge( $children_to_sync, $children_to_backfill ) );
+				if ( ! empty( $children_to_sync ) ) {
+					$invalidator = wc_get_container()->get( ProductVersionStringInvalidator::class );

-			if ( $changed ) {
-				$children = $this->read_children( $product, true );
-				$product->set_children( $children['all'] );
-				$product->set_visible_children( $children['visible'] );
+					foreach ( $children_to_sync as $child_id ) {
+						update_post_meta( $child_id, '_stock_status', $status );
+						$this->update_lookup_table( $child_id, 'wc_product_meta_lookup' );
+						$invalidator->invalidate( $child_id );
+					}

-				$invalidator->invalidate( $product->get_id() );
+					$children = $this->read_children( $product, true );
+					$product->set_children( $children['all'] );
+					$product->set_visible_children( $children['visible'] );
+					$invalidator->invalidate( $product->get_id() );
+				}
 			}
 		}
 	}
@@ -871,19 +893,25 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 	public function sync_price( &$product ) {
 		global $wpdb;

-		$children = $product->get_visible_children();
-		if ( $children ) {
-			$format   = array_fill( 0, count( $children ), '%d' );
-			$query_in = '(' . implode( ',', $format ) . ')';
-			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
-			$prices = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN {$query_in}", $children ) ) );
-		} else {
-			$prices = array();
+		$prices    = array();
+		$child_ids = $product->get_visible_children();
+		if ( ! empty( $child_ids ) ) {
+			$placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
+			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+			$prices = $wpdb->get_col(
+				$wpdb->prepare(
+					"SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_price' AND post_id IN ( {$placeholders} )",
+					$child_ids
+				)
+			);
+			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
 		}

-		delete_post_meta( $product->get_id(), '_price' );
-		delete_post_meta( $product->get_id(), '_sale_price' );
-		delete_post_meta( $product->get_id(), '_regular_price' );
+		$product_id = $product->get_id();
+
+		delete_post_meta( $product_id, '_price' );
+		delete_post_meta( $product_id, '_sale_price' );
+		delete_post_meta( $product_id, '_regular_price' );

 		if ( $prices ) {
 			sort( $prices, SORT_NUMERIC );
@@ -892,11 +920,11 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 				if ( is_null( $price ) || '' === $price ) {
 					continue;
 				}
-				add_post_meta( $product->get_id(), '_price', $price, false );
+				add_post_meta( $product_id, '_price', $price, false );
 			}
 		}

-		$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );
+		$this->update_lookup_table( $product_id, 'wc_product_meta_lookup' );

 		/**
 		 * Fire an action for this direct update so it can be detected by other code.
@@ -904,18 +932,19 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 		 * @since 3.6
 		 * @param int $product_id Product ID that was updated directly.
 		 */
-		do_action( 'woocommerce_updated_product_price', $product->get_id() );
+		do_action( 'woocommerce_updated_product_price', $product_id );
 	}

 	/**
 	 * Sync variable product stock status with children.
 	 * Change does not persist unless saved by caller.
 	 *
-	 * @param WC_Product $product Product object.
+	 * @param WC_Product_Variable $product Product object.
 	 *
 	 * @since 3.0.0
 	 */
 	public function sync_stock_status( &$product ) {
+		// Performance note: direct DB fetch would be faster, but child_is_in_stock/child_is_on_backorder are overridable — keep delegation.
 		if ( $product->child_is_in_stock() ) {
 			$product->set_stock_status( ProductStockStatus::IN_STOCK );
 		} elseif ( $product->child_is_on_backorder() ) {
@@ -987,6 +1016,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 		}

 		delete_transient( 'wc_product_children_' . $product_id );
+		delete_transient( 'wc_var_prices_' . $product_id );
 	}

 	/**
@@ -1014,6 +1044,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 		}

 		delete_transient( 'wc_product_children_' . $product_id );
+		delete_transient( 'wc_var_prices_' . $product_id );
 	}

 	/**
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 36b15f4e32d..14a946dc738 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -17886,18 +17886,6 @@ parameters:
 			count: 1
 			path: includes/data-stores/class-wc-product-grouped-data-store-cpt.php

-		-
-			message: '#^Call to an undefined method WC_Product\:\:child_is_in_stock\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: includes/data-stores/class-wc-product-variable-data-store-cpt.php
-
-		-
-			message: '#^Call to an undefined method WC_Product\:\:child_is_on_backorder\(\)\.$#'
-			identifier: method.notFound
-			count: 1
-			path: includes/data-stores/class-wc-product-variable-data-store-cpt.php
-
 		-
 			message: '#^Call to an undefined method WC_Product\:\:get_visible_children\(\)\.$#'
 			identifier: method.notFound
diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
index 826d221eb1c..d8c13e7df67 100644
--- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
+++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
@@ -384,13 +384,13 @@ class WC_Helper_Product {

 		foreach ( $terms as $term ) {
 			$result = term_exists( $term, $attribute->slug );
-
-			if ( ! $result ) {
-				$result               = wp_insert_term( $term, $attribute->slug );
-				$return['term_ids'][] = $result['term_id'];
-			} else {
-				$return['term_ids'][] = $result['term_id'];
+			if ( $result ) {
+				// Delete pre-existing term data to guarantee clean testing environment.
+				wp_delete_term( $result['term_id'], $attribute->slug );
 			}
+
+			$result               = wp_insert_term( $term, $attribute->slug );
+			$return['term_ids'][] = $result['term_id'];
 		}

 		return $return;
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 6219e93abe8..3c461cc897c 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
@@ -1,5 +1,7 @@
 <?php

+use Automattic\WooCommerce\Enums\ProductStockStatus;
+
 /**
  * Class WC_Product_Variable_Data_Store_CPT_Test
  */
@@ -90,6 +92,27 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		return $product;
 	}

+	/**
+	 * Cleans up global state that individual tests may leave behind.
+	 */
+	public function tearDown(): void {
+		delete_option( 'woocommerce_product_lookup_table_is_generating' );
+		parent::tearDown();
+	}
+
+	/**
+	 * Provides two cases: one where the product lookup table is available, and one where it is
+	 * still being generated and the code must fall back to postmeta queries.
+	 *
+	 * @return array[]
+	 */
+	public function provider_lookup_table_generating(): array {
+		return array(
+			'lookup table available'                      => array( false ),
+			'lookup table generating (postmeta fallback)' => array( true ),
+		);
+	}
+
 	/**
 	 * Helper filter to force prices inclusive of tax.
 	 */
@@ -308,7 +331,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {

 		$this->assertFalse(
 			$this->invokeMethod( $data_store, 'validate_prices_data', array( $uninitialized_prices, $current_version ) ),
-			'Uninitialized prices data should pass validation as could be new product'
+			'Uninitialized prices data should fail validation to trigger a rebuild'
 		);
 	}

@@ -380,15 +403,15 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 			'Empty children data with version should fail validation as likely corrupt'
 		);

-		// Test empty children data without version (new product, should pass).
+		// Test empty children data without a stored version (no prior cache entry).
 		$empty_children_no_version = array(
 			'all'     => array(),
 			'visible' => array(),
 		);

 		$this->assertFalse(
-			$this->invokeMethod( $data_store, 'validate_children_data', array( $empty_children_no_version, $current_version ) ),
-			'Empty children data without version should fail validation as likely corrupt'
+			$this->invokeMethod( $data_store, 'validate_children_data', array( $empty_children_no_version, null ) ),
+			'Empty children data without a stored version should fail validation to trigger a rebuild'
 		);

 		// Test uninitialized children data.
@@ -748,6 +771,32 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		remove_filter( 'woocommerce_matched_rates', array( $this, '__return_rates' ) );
 	}

+	/**
+	 * @testdox get_price_hash includes callback signatures via CallbackUtil when the legacy algorithm is disabled.
+	 */
+	public function test_get_price_hash_uses_callback_util_when_legacy_algorithm_is_disabled(): void {
+		add_filter( 'woocommerce_use_legacy_get_variations_price_hash', '__return_false' );
+
+		$product             = WC_Helper_Product::create_variation_product();
+		$extended_data_store = $this->get_data_store_with_public_get_price_hash();
+
+		$hash_without_callback = $extended_data_store->get_price_hash( $product, false );
+
+		$callback = static fn( $price ) => $price;
+		add_filter( 'woocommerce_variation_prices_price', $callback );
+
+		$hash_with_callback = $extended_data_store->get_price_hash( $product, false );
+
+		// Adding a price callback must change the hash, proving the CallbackUtil path
+		// is active and captures new hook registrations.
+		$this->assertNotSame( $hash_without_callback, $hash_with_callback );
+
+		remove_filter( 'woocommerce_variation_prices_price', $callback );
+		remove_filter( 'woocommerce_use_legacy_get_variations_price_hash', '__return_false' );
+
+		$product->delete();
+	}
+
 	/**
 	 * Get an instance of WC_Product_Variable_Data_Store_CPT whose get_price_hash method is public.
 	 *
@@ -798,7 +847,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 			'rate'     => 10,
 			'label'    => 'rate',
 			'shipping' => 'no',
-			'compund'  => 'no',
+			'compound' => 'no',
 		);
 	}

@@ -882,7 +931,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 	}

 	/**
-	 * Tests `read_attributes` for handling metas migration due to sanitize_title BC breaks.
+	 * @testdox read_attributes migrates child variation meta keys affected by the sanitize_title BC break.
 	 */
 	public function test_read_attributes_addresses_bc_break_in_sanitize(): void {
 		$product    = $this->get_variation_product_fixture();
@@ -911,6 +960,138 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		}
 	}

+	/**
+	 * @testdox read_attributes skips the child meta migration DB query when the product has no children.
+	 */
+	public function test_read_attributes_handles_bc_break_migration_when_no_children(): void {
+		$product = new WC_Product_Variable();
+		$product->set_name( 'Dummy Variable Product' );
+		$product->save();
+		$product_id = $product->get_id();
+
+		// Simulate a pre-BC-break attribute key containing a slash (is_variation required to trigger migration).
+		update_post_meta(
+			$product_id,
+			'_product_attributes',
+			array(
+				'Size/Size' => array(
+					'name'         => 'Size/Size',
+					'value'        => 'small | large',
+					'position'     => 0,
+					'is_visible'   => 1,
+					'is_variation' => 1,
+					'is_taxonomy'  => 0,
+				),
+			)
+		);
+
+		// Reload — no children to migrate; should load without a DB error.
+		$product = wc_get_product( $product_id );
+
+		$this->assertInstanceOf( WC_Product_Variable::class, $product );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox read_attributes forces update_attributes() even when no child meta rows need migrating.
+	 */
+	public function test_read_attributes_bc_migration_runs_force_update_when_no_old_rows_exist(): void {
+		$product    = WC_Helper_Product::create_variation_product();
+		$product_id = $product->get_id();
+
+		// Inject a slash-containing attribute key that triggers the BC migration condition.
+		// No child has an 'attribute_Size/Size' meta row, so $old_meta_rows will be empty.
+		update_post_meta(
+			$product_id,
+			'_product_attributes',
+			array(
+				'Size/Size' => array(
+					'name'         => 'Size/Size',
+					'value'        => 'small | large',
+					'position'     => 0,
+					'is_visible'   => 1,
+					'is_variation' => 1,
+					'is_taxonomy'  => 0,
+				),
+			)
+		);
+
+		// Reload — $force_update = true fires even though no rows were migrated, so
+		// update_attributes() must re-persist the attribute with the sanitised key.
+		$product = wc_get_product( $product_id );
+
+		// The attribute must survive intact.
+		$attributes = $product->get_attributes();
+		$this->assertCount( 1, $attributes );
+		$this->assertSame( 'Size/Size', array_values( $attributes )[0]->get_name() );
+
+		// update_attributes() must have re-written _product_attributes with the normalised key.
+		$stored = get_post_meta( $product_id, '_product_attributes', true );
+		$this->assertArrayHasKey( sanitize_title( 'Size/Size' ), $stored );
+		$this->assertArrayNotHasKey( 'Size/Size', $stored );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox read_attributes silently skips an attribute whose taxonomy is no longer registered.
+	 */
+	public function test_read_attributes_skips_unknown_taxonomy(): void {
+		$product    = WC_Helper_Product::create_variation_product();
+		$product_id = $product->get_id();
+
+		// Inject a stale taxonomy attribute that is no longer registered.
+		$stored                   = get_post_meta( $product_id, '_product_attributes', true );
+		$stored['pa_nonexistent'] = array(
+			'name'         => 'pa_nonexistent',
+			'value'        => '',
+			'position'     => 1,
+			'is_visible'   => 1,
+			'is_variation' => 0,
+			'is_taxonomy'  => 1,
+		);
+		update_post_meta( $product_id, '_product_attributes', $stored );
+
+		// Reload — the unknown taxonomy must not surface as an attribute.
+		$product         = wc_get_product( $product_id );
+		$attribute_names = array_map( static fn( $attribute ) => $attribute->get_name(), $product->get_attributes() );
+
+		$this->assertNotContains( 'pa_nonexistent', $attribute_names );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox read_attributes loads a text (non-taxonomy) attribute from the stored meta value string.
+	 */
+	public function test_read_attributes_returns_text_attribute(): void {
+		$attribute = new WC_Product_Attribute();
+		$attribute->set_id( 0 );
+		$attribute->set_name( 'Size' );
+		$attribute->set_options( array( 'Small', 'Medium', 'Large' ) );
+		$attribute->set_position( 0 );
+		$attribute->set_visible( true );
+		$attribute->set_variation( true );
+
+		$product = new WC_Product_Variable();
+		$product->set_name( 'Dummy Variable Product' );
+		$product->set_attributes( array( $attribute ) );
+		$product->save();
+
+		// Reload from DB so read_attributes() runs the non-taxonomy branch.
+		$product    = wc_get_product( $product->get_id() );
+		$attributes = $product->get_attributes();
+
+		$this->assertCount( 1, $attributes );
+		$loaded = array_values( $attributes )[0];
+		$this->assertSame( 0, $loaded->get_id() );
+		$this->assertSame( 'Size', $loaded->get_name() );
+		$this->assertSame( array( 'Small', 'Medium', 'Large' ), $loaded->get_options() );
+
+		$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.
 	 */
@@ -953,4 +1134,753 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 			);
 		}
 	}
+
+	/**
+	 * @testdox read_variation_attributes fetches all variation attribute values from DB on cache miss.
+	 */
+	public function test_read_variation_attributes_fetches_on_cache_miss(): void {
+		$product    = WC_Helper_Product::create_variation_product();
+		$product_id = $product->get_id();
+
+		$cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . 'product_variation_attributes_' . $product_id;
+		wp_cache_delete( $cache_key, 'products' );
+
+		$attributes = ( new WC_Product_Variable_Data_Store_CPT() )->read_variation_attributes( $product );
+
+		$this->assertSame(
+			array(
+				'pa_size'   => array( 'small', 'large', 'huge' ),
+				'pa_colour' => array(
+					0 => 'red',
+					2 => 'blue',
+				),
+				'pa_number' => array( '0', '1', '2' ),
+			),
+			$attributes
+		);
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox read_variation_attributes returns the cached result on a second call.
+	 */
+	public function test_read_variation_attributes_returns_cached_result(): void {
+		$data_store = new WC_Product_Variable_Data_Store_CPT();
+		$product    = WC_Helper_Product::create_variation_product();
+		$product_id = $product->get_id();
+
+		$cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . 'product_variation_attributes_' . $product_id;
+		wp_cache_delete( $cache_key, 'products' );
+
+		$first  = $data_store->read_variation_attributes( $product );
+		$second = $data_store->read_variation_attributes( $product );
+
+		$this->assertSame( $first, $second );
+		$this->assertSame( $first, wp_cache_get( $cache_key, 'products' ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox read_variation_attributes returns an empty array when the product has no variation attributes.
+	 */
+	public function test_read_variation_attributes_returns_empty_for_no_attributes(): void {
+		$product = new WC_Product_Variable();
+		$product->save();
+
+		$this->assertSame( array(), ( new WC_Product_Variable_Data_Store_CPT() )->read_variation_attributes( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox read_variation_attributes falls back to all taxonomy terms when the product has variation attributes but no children.
+	 */
+	public function test_read_variation_attributes_returns_all_terms_when_no_children(): void {
+		$product = new WC_Product_Variable();
+		$product->set_attributes( array( WC_Helper_Product::create_product_attribute_object( 'pattern', array( 'dots', 'stripes' ) ) ) );
+		$product->save();
+
+		$attributes = ( new WC_Product_Variable_Data_Store_CPT() )->read_variation_attributes( $product );
+
+		$this->assertSame( array( 'dots', 'stripes' ), $attributes['pa_pattern'] );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox read_variation_attributes returns only the assigned values for a non-taxonomy text attribute.
+	 */
+	public function test_read_variation_attributes_returns_assigned_text_attribute_values(): void {
+		$attribute = new WC_Product_Attribute();
+		$attribute->set_name( 'Size' );
+		$attribute->set_options( array( 'Small', 'Large', 'XL' ) );
+		$attribute->set_variation( true );
+		$attribute->set_id( 0 );
+
+		$product = new WC_Product_Variable();
+		$product->set_attributes( array( $attribute ) );
+		$product->save();
+		$product_id = $product->get_id();
+
+		WC_Helper_Product::create_product_variation_object(
+			$product_id,
+			'DUMMY SKU TEXT ATTR',
+			10,
+			array( 'size' => 'Small' )
+		);
+
+		$product   = wc_get_product( $product_id );
+		$cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . 'product_variation_attributes_' . $product_id;
+		wp_cache_delete( $cache_key, 'products' );
+
+		$attributes = ( new WC_Product_Variable_Data_Store_CPT() )->read_variation_attributes( $product );
+
+		$this->assertSame( array( 'Small' ), $attributes['Size'] );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox read_variation_attributes resolves text attribute values via slug matching on pre-2.4 products.
+	 */
+	public function test_read_variation_attributes_resolves_pre24_text_attribute_slugs(): void {
+		$attribute = new WC_Product_Attribute();
+		$attribute->set_name( 'Size' );
+		$attribute->set_options( array( 'Small', 'Large', 'XL' ) );
+		$attribute->set_variation( true );
+		$attribute->set_id( 0 );
+
+		$product = new WC_Product_Variable();
+		$product->set_attributes( array( $attribute ) );
+		$product->save();
+		$product_id = $product->get_id();
+
+		// Mark this as a pre-2.4 product so the slug-matching branch is taken.
+		update_post_meta( $product_id, '_product_version', '2.3.0' );
+
+		// Create a variation whose attribute is stored as the slug ('small'), not the display value ('Small').
+		WC_Helper_Product::create_product_variation_object(
+			$product_id,
+			'DUMMY SKU PRE24',
+			10,
+			array( 'size' => 'Small' )
+		);
+
+		$product      = wc_get_product( $product_id );
+		$child_ids    = $product->get_children();
+		$variation_id = current( $child_ids );
+
+		// Overwrite the stored value with the slug form to simulate pre-2.4 database state.
+		update_post_meta( $variation_id, 'attribute_size', 'small' );
+
+		$cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . 'product_variation_attributes_' . $product_id;
+		wp_cache_delete( $cache_key, 'products' );
+
+		$attributes = ( new WC_Product_Variable_Data_Store_CPT() )->read_variation_attributes( $product );
+
+		// The slug 'small' must be resolved back to the full display value 'Small'.
+		$this->assertSame( array( 'Small' ), $attributes['Size'] );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_weight returns true when at least one visible child has a weight set.
+	 */
+	public function test_child_has_weight_returns_true_when_child_has_weight(): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_visible_children();
+
+		update_post_meta( current( $child_ids ), '_weight', '1.5' );
+
+		$this->assertTrue( ( new WC_Product_Variable_Data_Store_CPT() )->child_has_weight( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_weight returns false when no visible child has a weight set.
+	 */
+	public function test_child_has_weight_returns_false_when_no_child_has_weight(): void {
+		$data_store = new WC_Product_Variable_Data_Store_CPT();
+		$product    = WC_Helper_Product::create_variation_product();
+		$child_ids  = $product->get_visible_children();
+
+		foreach ( $child_ids as $child_id ) {
+			update_post_meta( $child_id, '_weight', '1.5' );
+		}
+
+		$this->assertTrue( $data_store->child_has_weight( $product ) );
+
+		foreach ( $child_ids as $child_id ) {
+			delete_post_meta( $child_id, '_weight' );
+		}
+
+		$this->assertFalse( $data_store->child_has_weight( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_weight returns false when the product has no visible children.
+	 */
+	public function test_child_has_weight_returns_false_for_no_children(): void {
+		$product = new WC_Product_Variable();
+		$product->save();
+
+		$this->assertFalse( ( new WC_Product_Variable_Data_Store_CPT() )->child_has_weight( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_weight returns false when a child has weight set to zero.
+	 */
+	public function test_child_has_weight_returns_false_when_child_weight_is_zero(): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_visible_children();
+
+		update_post_meta( current( $child_ids ), '_weight', '0' );
+
+		$this->assertFalse( ( new WC_Product_Variable_Data_Store_CPT() )->child_has_weight( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_dimensions returns true when at least one visible child has a dimension set.
+	 */
+	public function test_child_has_dimensions_returns_true_when_child_has_dimension(): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_visible_children();
+
+		update_post_meta( current( $child_ids ), '_length', '10' );
+
+		$this->assertTrue( ( new WC_Product_Variable_Data_Store_CPT() )->child_has_dimensions( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_dimensions returns false when no visible child has any dimension set.
+	 */
+	public function test_child_has_dimensions_returns_false_when_no_child_has_dimensions(): void {
+		$data_store = new WC_Product_Variable_Data_Store_CPT();
+		$product    = WC_Helper_Product::create_variation_product();
+		$child_ids  = $product->get_visible_children();
+
+		foreach ( $child_ids as $child_id ) {
+			update_post_meta( $child_id, '_length', '10' );
+		}
+
+		$this->assertTrue( $data_store->child_has_dimensions( $product ) );
+
+		foreach ( $child_ids as $child_id ) {
+			delete_post_meta( $child_id, '_length' );
+		}
+
+		$this->assertFalse( $data_store->child_has_dimensions( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_dimensions returns false when the product has no visible children.
+	 */
+	public function test_child_has_dimensions_returns_false_for_no_children(): void {
+		$product = new WC_Product_Variable();
+		$product->save();
+
+		$this->assertFalse( ( new WC_Product_Variable_Data_Store_CPT() )->child_has_dimensions( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_dimensions returns true when a child has only width set.
+	 */
+	public function test_child_has_dimensions_returns_true_when_child_has_width(): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_visible_children();
+
+		update_post_meta( current( $child_ids ), '_width', '5' );
+
+		$this->assertTrue( ( new WC_Product_Variable_Data_Store_CPT() )->child_has_dimensions( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_dimensions returns false when a child has a dimension set to zero.
+	 */
+	public function test_child_has_dimensions_returns_false_when_dimension_is_zero(): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_visible_children();
+
+		update_post_meta( current( $child_ids ), '_length', '0' );
+
+		$this->assertFalse( ( new WC_Product_Variable_Data_Store_CPT() )->child_has_dimensions( $product ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @dataProvider provider_lookup_table_generating
+	 * @testdox child_has_stock_status returns true when at least one child has the given status.
+	 *
+	 * @param bool $lookup_table_generating Whether the lookup table is currently being generated.
+	 */
+	public function test_child_has_stock_status_returns_true_when_child_matches( bool $lookup_table_generating ): void {
+		$data_store = new WC_Product_Variable_Data_Store_CPT();
+		$product    = WC_Helper_Product::create_variation_product();
+		$child_ids  = $product->get_children();
+
+		$variation = wc_get_product( current( $child_ids ) );
+		$variation->set_stock_status( ProductStockStatus::ON_BACKORDER );
+		$variation->save();
+
+		update_option( 'woocommerce_product_lookup_table_is_generating', $lookup_table_generating );
+
+		$this->assertTrue( $data_store->child_has_stock_status( $product, ProductStockStatus::ON_BACKORDER ) );
+		$this->assertFalse( $data_store->child_has_stock_status( $product, ProductStockStatus::OUT_OF_STOCK ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox child_has_stock_status returns false when the product has no children.
+	 */
+	public function test_child_has_stock_status_returns_false_when_no_children(): void {
+		$product = new WC_Product_Variable();
+		$product->set_stock_status( ProductStockStatus::IN_STOCK );
+		$product->save();
+
+		$this->assertFalse( ( new WC_Product_Variable_Data_Store_CPT() )->child_has_stock_status( $product, ProductStockStatus::IN_STOCK ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_managed_variation_stock_status updates children that do not manage their own stock.
+	 */
+	public function test_sync_managed_variation_stock_status_updates_unmanaged_children(): void {
+		$product = WC_Helper_Product::create_variation_product();
+		$product->set_manage_stock( true );
+		$product->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+		$product->save();
+
+		$child_ids = $product->get_children();
+		foreach ( $child_ids as $child_id ) {
+			update_post_meta( $child_id, '_manage_stock', 'no' );
+			update_post_meta( $child_id, '_stock_status', ProductStockStatus::IN_STOCK );
+		}
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_managed_variation_stock_status( $product );
+
+		foreach ( $child_ids as $child_id ) {
+			$this->assertSame( ProductStockStatus::OUT_OF_STOCK, get_post_meta( $child_id, '_stock_status', true ) );
+		}
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_managed_variation_stock_status does not touch children that manage their own stock.
+	 */
+	public function test_sync_managed_variation_stock_status_skips_managed_children(): void {
+		$product = WC_Helper_Product::create_variation_product();
+		$product->set_manage_stock( true );
+		$product->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+		$product->save();
+
+		$child_ids = $product->get_children();
+		foreach ( $child_ids as $child_id ) {
+			update_post_meta( $child_id, '_manage_stock', 'yes' );
+			update_post_meta( $child_id, '_stock_status', ProductStockStatus::IN_STOCK );
+		}
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_managed_variation_stock_status( $product );
+
+		foreach ( $child_ids as $child_id ) {
+			$this->assertSame( ProductStockStatus::IN_STOCK, get_post_meta( $child_id, '_stock_status', true ) );
+		}
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_managed_variation_stock_status does nothing when the parent does not manage stock.
+	 */
+	public function test_sync_managed_variation_stock_status_skips_when_parent_does_not_manage_stock(): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_children();
+
+		// Save children as out of stock (updates lookup table) so WC computes the parent as out of stock.
+		foreach ( $child_ids as $child_id ) {
+			$variation = wc_get_product( $child_id );
+			$variation->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+			$variation->save();
+		}
+		$product->set_manage_stock( false );
+		$product->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+		$product->save();
+
+		// Now set children to in stock via raw postmeta — the method should ignore them since the parent does not manage stock.
+		foreach ( $child_ids as $child_id ) {
+			update_post_meta( $child_id, '_manage_stock', 'no' );
+			update_post_meta( $child_id, '_stock_status', ProductStockStatus::IN_STOCK );
+		}
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_managed_variation_stock_status( $product );
+
+		foreach ( $child_ids as $child_id ) {
+			$this->assertSame( ProductStockStatus::IN_STOCK, get_post_meta( $child_id, '_stock_status', true ) );
+		}
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_managed_variation_stock_status skips unmanaged children already at the correct status.
+	 */
+	public function test_sync_managed_variation_stock_status_skips_children_already_at_correct_status(): void {
+		$product = WC_Helper_Product::create_variation_product();
+		$product->set_manage_stock( true );
+		$product->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+		$product->save();
+
+		$child_ids = $product->get_children();
+		foreach ( $child_ids as $child_id ) {
+			update_post_meta( $child_id, '_manage_stock', 'no' );
+			update_post_meta( $child_id, '_stock_status', ProductStockStatus::OUT_OF_STOCK );
+		}
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_managed_variation_stock_status( $product );
+
+		foreach ( $child_ids as $child_id ) {
+			$this->assertSame( ProductStockStatus::OUT_OF_STOCK, get_post_meta( $child_id, '_stock_status', true ) );
+		}
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_managed_variation_stock_status updates only unmanaged children when the product has a mix of managed and unmanaged children.
+	 */
+	public function test_sync_managed_variation_stock_status_updates_only_unmanaged_in_mixed_children(): void {
+		$product = WC_Helper_Product::create_variation_product();
+		$product->set_manage_stock( true );
+		$product->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+		$product->save();
+
+		$child_ids   = $product->get_children();
+		$half        = intdiv( count( $child_ids ), 2 );
+		$managed_ids = array_slice( $child_ids, 0, $half );
+
+		foreach ( $child_ids as $child_id ) {
+			$is_managed = in_array( $child_id, $managed_ids, true );
+			update_post_meta( $child_id, '_manage_stock', $is_managed ? 'yes' : 'no' );
+			update_post_meta( $child_id, '_stock_status', ProductStockStatus::IN_STOCK );
+		}
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_managed_variation_stock_status( $product );
+
+		foreach ( $child_ids as $child_id ) {
+			$expected_status = in_array( $child_id, $managed_ids, true ) ? ProductStockStatus::IN_STOCK : ProductStockStatus::OUT_OF_STOCK;
+			$this->assertSame( $expected_status, get_post_meta( $child_id, '_stock_status', true ) );
+		}
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_managed_variation_stock_status backfills unmanaged children that have no _stock_status meta row or a NULL value.
+	 */
+	public function test_sync_managed_variation_stock_status_backfills_children_with_missing_status(): void {
+		global $wpdb;
+
+		$product = WC_Helper_Product::create_variation_product();
+		$product->set_manage_stock( true );
+		$product->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+		$product->save();
+
+		$child_ids = $product->get_children();
+		foreach ( array_values( $child_ids ) as $i => $child_id ) {
+			update_post_meta( $child_id, '_manage_stock', 'no' );
+			if ( 1 === $i % 2 ) {
+				// Even index: simulate a NULL meta_value being stored.
+				$wpdb->update(
+					$wpdb->postmeta,
+					array( 'meta_value' => null ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+					array(
+						'post_id'  => $child_id,
+						'meta_key' => '_stock_status', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+					),
+					array( '%s' ),
+					array( '%d', '%s' )
+				);
+			} else {
+				// Odd index: simulate a variation created outside WC's save pipeline — no _stock_status row.
+				$wpdb->delete(
+					$wpdb->postmeta,
+					array(
+						'post_id'  => $child_id,
+						'meta_key' => '_stock_status', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+					)
+				);
+			}
+		}
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_managed_variation_stock_status( $product );
+
+		foreach ( $child_ids as $child_id ) {
+			$this->assertSame( ProductStockStatus::OUT_OF_STOCK, get_post_meta( $child_id, '_stock_status', true ) );
+		}
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_managed_variation_stock_status does nothing when the product has no children.
+	 */
+	public function test_sync_managed_variation_stock_status_does_nothing_when_no_children(): void {
+		$product = new WC_Product_Variable();
+		$product->set_manage_stock( true );
+		$product->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+		$product->save();
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_managed_variation_stock_status( $product );
+
+		$this->assertSame( array(), $product->get_children() );
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_price skips empty-string and SQL-NULL prices while still storing valid sibling prices from the same set.
+	 */
+	public function test_sync_price_stores_valid_prices_when_mixed_with_empty(): void {
+		global $wpdb;
+
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_visible_children();
+
+		// First child: empty string — exercises the `'' === $price` branch.
+		$empty_string_child = $child_ids[0];
+		update_post_meta( $empty_string_child, '_price', '' );
+
+		// Second child: SQL NULL — exercises the `is_null( $price )` branch. Direct SQLs as update_post_meta( null ) saves empty string.
+		$null_child_id = $child_ids[1];
+		unset( $child_ids[0], $child_ids[1] );
+		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_price'", $null_child_id ) );
+		$wpdb->query( $wpdb->prepare( "INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value) VALUES (%d, %s, NULL)", $null_child_id, '_price' ) );
+
+		// Remaining children: a known valid price.
+		foreach ( $child_ids as $child_id ) {
+			update_post_meta( $child_id, '_price', '12.00' );
+		}
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_price( $product );
+
+		// Only '12.00' must appear — neither the empty-string nor the NULL price must reach the parent.
+		$this->assertSame( array( '12.00' ), get_post_meta( $product->get_id(), '_price', false ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_price stores only distinct child prices on the parent.
+	 */
+	public function test_sync_price_stores_only_distinct_child_prices_on_parent(): void {
+		$data_store = new WC_Product_Variable_Data_Store_CPT();
+		$product    = WC_Helper_Product::create_variation_product();
+		$child_ids  = $product->get_visible_children();
+
+		$data_store->sync_price( $product );
+
+		$this->assertSame(
+			array( 10.0, 15.0, 16.0, 17.0, 18.0, 19.0 ),
+			array_map( 'floatval', get_post_meta( $product->get_id(), '_price', false ) )
+		);
+
+		foreach ( $child_ids as $child_id ) {
+			update_post_meta( $child_id, '_price', '9.99' );
+		}
+
+		$data_store->sync_price( $product );
+
+		$this->assertSame( array( 9.99 ), array_map( 'floatval', get_post_meta( $product->get_id(), '_price', false ) ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_price removes all price meta when the product has no visible children.
+	 */
+	public function test_sync_price_clears_price_when_no_visible_children(): void {
+		$product = new WC_Product_Variable();
+		$product->save();
+
+		add_post_meta( $product->get_id(), '_price', '99' );
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_price( $product );
+
+		$this->assertSame( array(), get_post_meta( $product->get_id(), '_price', false ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_price skips children with an empty price and stores no price meta.
+	 */
+	public function test_sync_price_skips_empty_child_prices(): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_visible_children();
+
+		foreach ( $child_ids as $child_id ) {
+			update_post_meta( $child_id, '_price', '' );
+		}
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_price( $product );
+
+		$this->assertSame( array(), get_post_meta( $product->get_id(), '_price', false ) );
+
+		$product->delete();
+	}
+
+	/**
+	 * @dataProvider provider_lookup_table_generating
+	 * @testdox sync_stock_status sets instock when at least one child is in stock.
+	 *
+	 * @param bool $lookup_table_generating Whether the lookup table is currently being generated.
+	 */
+	public function test_sync_stock_status_sets_instock_when_any_child_in_stock( bool $lookup_table_generating ): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_children();
+
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		foreach ( $child_ids as $child_id ) {
+			$variation = wc_get_product( $child_id );
+			$variation->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+			$variation->save();
+		}
+		$variation = wc_get_product( current( $child_ids ) );
+		$variation->set_stock_status( ProductStockStatus::IN_STOCK );
+		$variation->save();
+
+		update_option( 'woocommerce_product_lookup_table_is_generating', $lookup_table_generating );
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_stock_status( $product );
+
+		$this->assertSame( ProductStockStatus::IN_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @dataProvider provider_lookup_table_generating
+	 * @testdox sync_stock_status sets on_backorder when no child is in stock but at least one is on backorder.
+	 *
+	 * @param bool $lookup_table_generating Whether the lookup table is currently being generated.
+	 */
+	public function test_sync_stock_status_sets_on_backorder_when_backorder_exists_and_none_in_stock( bool $lookup_table_generating ): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_children();
+
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		foreach ( $child_ids as $child_id ) {
+			$variation = wc_get_product( $child_id );
+			$variation->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+			$variation->save();
+		}
+		$variation = wc_get_product( current( $child_ids ) );
+		$variation->set_stock_status( ProductStockStatus::ON_BACKORDER );
+		$variation->save();
+
+		update_option( 'woocommerce_product_lookup_table_is_generating', $lookup_table_generating );
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_stock_status( $product );
+
+		$this->assertSame( ProductStockStatus::ON_BACKORDER, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_stock_status sets outofstock when all children are out of stock.
+	 */
+	public function test_sync_stock_status_sets_outofstock_when_all_children_outofstock(): void {
+		$product = WC_Helper_Product::create_variation_product();
+
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		foreach ( $product->get_children() as $child_id ) {
+			$variation = wc_get_product( $child_id );
+			$variation->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+			$variation->save();
+		}
+
+		$product->set_stock_status( ProductStockStatus::IN_STOCK );
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_stock_status( $product );
+
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_stock_status sets outofstock when the product has no children.
+	 */
+	public function test_sync_stock_status_sets_outofstock_when_no_children(): void {
+		$product = new WC_Product_Variable();
+		$product->set_stock_status( ProductStockStatus::IN_STOCK );
+		$product->save();
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_stock_status( $product );
+
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
+
+	/**
+	 * @testdox sync_stock_status sets instock when children include both in-stock and on-backorder.
+	 */
+	public function test_sync_stock_status_instock_takes_priority_over_on_backorder(): void {
+		$product   = WC_Helper_Product::create_variation_product();
+		$child_ids = $product->get_children();
+
+		$this->assertSame( ProductStockStatus::OUT_OF_STOCK, $product->get_stock_status() );
+
+		foreach ( $child_ids as $child_id ) {
+			$variation = wc_get_product( $child_id );
+			$variation->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+			$variation->save();
+		}
+
+		$variation = wc_get_product( current( $child_ids ) );
+		$variation->set_stock_status( ProductStockStatus::ON_BACKORDER );
+		$variation->save();
+
+		$variation = wc_get_product( next( $child_ids ) );
+		$variation->set_stock_status( ProductStockStatus::IN_STOCK );
+		$variation->save();
+
+		( new WC_Product_Variable_Data_Store_CPT() )->sync_stock_status( $product );
+
+		$this->assertSame( ProductStockStatus::IN_STOCK, $product->get_stock_status() );
+
+		$product->delete();
+	}
 }