Commit abc4b29a92a for woocommerce
commit abc4b29a92a6f5d36a8195060c4c9c4c7e2ab60e
Author: Tung Du <dinhtungdu@gmail.com>
Date: Mon Nov 9 18:09:54 2026 +0700
Show all attribute options when a variation has no stored value for that attribute (#70612)
* Add failing test for variations without an attribute meta row
A variation with no attribute_* row for an attribute accepts any value,
but read_variation_attributes() only offers the pinned values of its
siblings, so the product page dropdown collapses (issue #44830).
Amp-Thread-ID: https://ampcode.com/threads/T-01a17cc7-7469-75b0-9e55-ad4146bc3f72
Co-authored-by: Amp <amp@ampcode.com>
* Treat variations without an attribute meta row as "any" value
read_variation_attributes() offered only the pinned values when some
variations had no attribute_* row, although wc_get_product_variation_attributes()
reads that missing row as "any". The product page dropdown then lost every
other option (issue #44830). Count the distinct variations that have a row
and fall back to all parent options when any variation lacks one.
Variations created by the REST API, CSV import or code without every
attribute, or before an attribute was added to the parent, have this shape.
Amp-Thread-ID: https://ampcode.com/threads/T-01a17cc7-7469-75b0-9e55-ad4146bc3f72
Co-authored-by: Amp <amp@ampcode.com>
* Use a new cache key for variation attribute lists
Stores with a persistent object cache kept the old, shorter list until the
product was saved again. A new key makes them build the list again after the
update.
Amp-Thread-ID: https://ampcode.com/threads/T-01a1a42f-76e7-71ec-99c3-ae1373ef7fdd
Co-authored-by: Amp <amp@ampcode.com>
* Revert "Use a new cache key for variation attribute lists"
Not worth a new key. A store that does not purge its cache after updating
only keeps the old list until the product is saved again.
Amp-Thread-ID: https://ampcode.com/threads/T-01a1a42f-76e7-71ec-99c3-ae1373ef7fdd
Co-authored-by: Amp <amp@ampcode.com>
---------
Co-authored-by: Amp <amp@ampcode.com>
diff --git a/plugins/woocommerce/changelog/fix-44830-missing-wildcard-variation-meta b/plugins/woocommerce/changelog/fix-44830-missing-wildcard-variation-meta
new file mode 100644
index 00000000000..67e4b199c18
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-44830-missing-wildcard-variation-meta
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show every attribute option on the product page when an "Any" variation has no stored value for that attribute.
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 dac5845469c..9562c09f560 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
@@ -268,7 +268,8 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
$child_ids = $product->get_children();
$attributes = array_values( array_filter( $attributes, static fn ( $attribute ) => ! empty( $attribute['is_variation'] ) ) );
- $attributes_values = array();
+ $attributes_values = array();
+ $attributes_children = 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' ) );
@@ -277,7 +278,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
$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} )",
+ "SELECT post_id, 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 )
)
@@ -285,15 +286,21 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
foreach ( $prefetch as $row ) {
// Defensive: normalize non-canonical meta_key casing (e.g. attribute_pa_Size → attribute_pa_size).
// sanitize_title over wc_variation_attribute_name as the latter adds 'attribute_' prefix.
- $attributes_values[ sanitize_title( $row->meta_key ) ][] = $row->meta_value;
+ $meta_key = sanitize_title( $row->meta_key );
+ $attributes_values[ $meta_key ][] = $row->meta_value;
+ $attributes_children[ $meta_key ][ $row->post_id ] = true;
}
}
foreach ( $attributes as $attribute ) {
- $values = $attributes_values[ wc_variation_attribute_name( $attribute['name'] ) ] ?? array();
+ $meta_key = wc_variation_attribute_name( $attribute['name'] );
+ $values = $attributes_values[ $meta_key ] ?? array();
+
+ // A variation without a row for this attribute accepts any value, as in wc_get_product_variation_attributes().
+ $has_child_without_row = count( $attributes_children[ $meta_key ] ?? array() ) < count( $child_ids );
// Empty value indicates that all options for given attribute are available.
- if ( empty( $values ) || in_array( null, $values, true ) || in_array( '', $values, true ) ) {
+ if ( empty( $values ) || $has_child_without_row || 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'] ) {
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/product/data-store.php b/plugins/woocommerce/tests/legacy/unit-tests/product/data-store.php
index d9455d846d0..85ada028e34 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/product/data-store.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/product/data-store.php
@@ -296,10 +296,7 @@ class WC_Tests_Product_Data_Store extends WC_Unit_Test_Case {
$expected_attributes = array(
'pa_size' => array( 'small', 'large', 'huge' ),
- 'pa_colour' => array(
- 0 => 'red',
- 2 => 'blue',
- ),
+ 'pa_colour' => array( 'blue', 'red' ),
'pa_number' => array( '0', '1', '2' ),
);
$this->assertEquals( $expected_attributes, $product->get_variation_attributes() );
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 c9e45bb30e6..de488d89b2f 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
@@ -1174,10 +1174,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$this->assertSame(
array(
'pa_size' => array( 'small', 'large', 'huge' ),
- 'pa_colour' => array(
- 0 => 'red',
- 2 => 'blue',
- ),
+ 'pa_colour' => array( 'blue', 'red' ),
'pa_number' => array( '0', '1', '2' ),
),
$attributes
@@ -1310,6 +1307,44 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$product->delete();
}
+ /**
+ * @testdox read_variation_attributes treats a variation with no meta row for an attribute as "any" value.
+ */
+ public function test_read_variation_attributes_treats_missing_meta_row_as_any_value(): void {
+ $product = new WC_Product_Variable();
+ $product->set_attributes(
+ array(
+ WC_Helper_Product::create_product_attribute_object( 'shade', array( 'blue', 'green', 'red' ) ),
+ WC_Helper_Product::create_product_attribute_object( 'fabric', array( 'cotton', 'silk', 'wool' ) ),
+ )
+ );
+ $product->save();
+ $product_id = $product->get_id();
+
+ $blue_cotton = WC_Helper_Product::create_product_variation_object(
+ $product_id,
+ 'DUMMY SKU BLUE COTTON',
+ 10,
+ array(
+ 'pa_shade' => 'blue',
+ 'pa_fabric' => 'cotton',
+ )
+ );
+ // A duplicate row must not stand in for the shade row the silk variation lacks.
+ add_post_meta( $blue_cotton->get_id(), 'attribute_pa_shade', 'blue' );
+ WC_Helper_Product::create_product_variation_object( $product_id, 'DUMMY SKU ANY SILK', 15, array( 'pa_fabric' => 'silk' ) );
+
+ $product = wc_get_product( $product_id );
+ wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . 'product_variation_attributes_' . $product_id, 'products' );
+
+ $attributes = ( new WC_Product_Variable_Data_Store_CPT() )->read_variation_attributes( $product );
+
+ $this->assertSame( array( 'blue', 'green', 'red' ), $attributes['pa_shade'], 'The silk variation has no shade row, so every shade is available.' );
+ $this->assertSame( array( 'cotton', 'silk' ), $attributes['pa_fabric'], 'Every variation pins a fabric, so only those fabrics are available.' );
+
+ $product->delete();
+ }
+
/**
* @testdox child_has_weight returns true when at least one visible child has a weight set.
*/