Commit 7fd068fee42 for woocommerce

commit 7fd068fee42f112a3c8709e68bbdfd9bf354300c
Author: Liam Sarsfield <43409125+LiamSarsfield@users.noreply.github.com>
Date:   Mon Jul 6 18:46:51 2026 +0100

    Revert "Performance: skip redundant meta existence checks on product create (#65595)" (#66325)

diff --git a/plugins/woocommerce/changelog/65595-fix-rest-batch-postmeta-create-fast-path b/plugins/woocommerce/changelog/65595-fix-rest-batch-postmeta-create-fast-path
deleted file mode 100644
index aedf64b4ba5..00000000000
--- a/plugins/woocommerce/changelog/65595-fix-rest-batch-postmeta-create-fast-path
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: performance
-
-Improve product and variation creation performance by avoiding redundant internal meta existence checks.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/revert-65595-product-create-meta-fast-path b/plugins/woocommerce/changelog/revert-65595-product-create-meta-fast-path
new file mode 100644
index 00000000000..48afdd8d1a5
--- /dev/null
+++ b/plugins/woocommerce/changelog/revert-65595-product-create-meta-fast-path
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Revert #65595 (product-create meta fast-path). Added and reverted within the 11.0 cycle, so no net user-facing change to document.
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php b/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php
index cf941854c27..657d7c2c1d6 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-data-store-wp.php
@@ -247,78 +247,24 @@ class WC_Data_Store_WP {
 	 *
 	 * Note: WordPress `get_metadata` function returns an empty string when meta data does not exist.
 	 *
-	 * @param WC_Data $data_object The WP_Data object (WC_Coupon for coupons, etc).
+	 * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc).
 	 * @param string  $meta_key Meta key to update.
 	 * @param mixed   $meta_value Value to save.
-	 * @param bool    $creating Whether the object is being created.
-	 * @param array   $existing_meta_keys Existing meta keys map, maintained across calls during creation. Passed by reference.
+	 *
 	 * @since 3.6.0 Added to prevent empty meta being stored unless required.
 	 *
-	 * @return bool True if added/updated/deleted.
+	 * @return bool True if updated/deleted.
 	 */
-	protected function update_or_delete_post_meta( $data_object, $meta_key, $meta_value, $creating = false, &$existing_meta_keys = null ) {
-		$is_empty_meta_value = in_array( $meta_value, array( array(), '' ), true );
-		$must_exist          = in_array( $meta_key, $this->must_exist_meta_keys, true );
-		$object_id           = $data_object->get_id();
-
-		if ( $creating ) {
-			if ( ! is_array( $existing_meta_keys ) ) {
-				$existing_meta_keys = $this->get_existing_meta_keys( $object_id );
-			}
-
-			$meta_exists = isset( $existing_meta_keys[ $meta_key ] );
-
-			if ( $is_empty_meta_value && ! $must_exist ) {
-				if ( ! $meta_exists ) {
-					return false;
-				}
-
-				$updated = delete_post_meta( $object_id, $meta_key );
-				unset( $existing_meta_keys[ $meta_key ] );
-
-				return (bool) $updated;
-			}
-
-			if ( $meta_exists ) {
-				$updated = update_post_meta( $object_id, $meta_key, $meta_value );
-			} else {
-				$updated = add_post_meta( $object_id, $meta_key, $meta_value );
-				if ( $updated ) {
-					$existing_meta_keys[ $meta_key ] = true;
-				}
-			}
-
-			return (bool) $updated;
-		}
-
-		if ( $is_empty_meta_value && ! $must_exist ) {
-			$updated = delete_post_meta( $object_id, $meta_key );
+	protected function update_or_delete_post_meta( $object, $meta_key, $meta_value ) {
+		if ( in_array( $meta_value, array( array(), '' ), true ) && ! in_array( $meta_key, $this->must_exist_meta_keys, true ) ) {
+			$updated = delete_post_meta( $object->get_id(), $meta_key );
 		} else {
-			$updated = update_post_meta( $object_id, $meta_key, $meta_value );
+			$updated = update_post_meta( $object->get_id(), $meta_key, $meta_value );
 		}

 		return (bool) $updated;
 	}

-	/**
-	 * Get the set of meta keys an object already has, to avoid per-key existence queries during creation.
-	 *
-	 * @param int $object_id Object ID.
-	 * @return array Map of existing meta key => true.
-	 */
-	protected function get_existing_meta_keys( $object_id ) {
-		global $wpdb;
-
-		$meta_keys = $wpdb->get_col(
-			$wpdb->prepare(
-				"SELECT meta_key FROM {$wpdb->postmeta} WHERE post_id = %d",
-				$object_id
-			)
-		);
-
-		return array_fill_keys( $meta_keys, true );
-	}
-
 	/**
 	 * Get valid WP_Query args from a WC_Object_Query's query variables.
 	 *
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index 59798c645e5..de9b861600d 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -248,7 +248,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 			$post_object = get_post( $product->get_id() );
 			$product->set_status( $post_object->post_status );

-			$this->update_post_meta_internal( $product, true, true );
+			$this->update_post_meta( $product, true );
 			$this->update_terms( $product, true );
 			$this->update_visibility( $product, true );
 			$this->update_attributes( $product, true );
@@ -697,31 +697,12 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 	/**
 	 * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
 	 *
-	 * Subclasses that override this method to write custom product-type meta should also override
-	 * update_post_meta_internal() so those writes run during object creation.
-	 *
 	 * @param WC_Product $product Product object.
 	 * @param bool       $force Force update. Used during create.
 	 * @since 3.0.0
 	 * @return void
 	 */
 	protected function update_post_meta( &$product, $force = false ) {
-		$this->update_post_meta_internal( $product, $force, false );
-	}
-
-	/**
-	 * Internal implementation of update_post_meta() that also knows whether the product is being created.
-	 *
-	 * Subclasses that override update_post_meta() to write custom product-type meta should also override
-	 * this method so those writes run during object creation.
-	 *
-	 * @param WC_Product $product Product object.
-	 * @param bool       $force Force update. Used during create.
-	 * @param bool       $creating Whether the product is being created.
-	 * @param array      $existing_meta_keys Existing meta keys map, maintained across calls during creation. Passed by reference.
-	 * @return void
-	 */
-	protected function update_post_meta_internal( &$product, $force, $creating, &$existing_meta_keys = null ) {
 		$meta_key_to_props = array(
 			'_sku'                   => 'sku',
 			'_global_unique_id'      => 'global_unique_id',
@@ -807,7 +788,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 					break;
 			}

-			$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value, $creating, $existing_meta_keys );
+			$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );

 			if ( $updated ) {
 				$this->updated_props[] = $prop;
@@ -829,7 +810,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 			$cogs_value = apply_filters( 'woocommerce_save_product_cogs_value', $cogs_value, $product );

 			if ( false !== $cogs_value ) {
-				$updated = $this->update_or_delete_post_meta( $product, '_cogs_total_value', is_null( $cogs_value ) ? '' : $cogs_value, $creating, $existing_meta_keys );
+				$updated = $this->update_or_delete_post_meta( $product, '_cogs_total_value', is_null( $cogs_value ) ? '' : $cogs_value );
 				if ( $updated ) {
 					$this->updated_props[] = 'cogs_value';
 				}
@@ -847,7 +828,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 				if ( is_callable( array( $product, $function ) ) ) {
 					$value   = $product->{$function}( 'edit' );
 					$value   = is_string( $value ) ? wp_slash( $value ) : $value;
-					$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value, $creating, $existing_meta_keys );
+					$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );

 					if ( $updated ) {
 						$this->updated_props[] = $key;
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php
index 53188472c12..f21b9e6fa74 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-grouped-data-store-cpt.php
@@ -24,19 +24,6 @@ class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implem
 	 * @since 3.0.0
 	 */
 	protected function update_post_meta( &$product, $force = false ) {
-		$this->update_post_meta_internal( $product, $force, false );
-	}
-
-	/**
-	 * Internal implementation of update_post_meta() that also knows whether the product is being created.
-	 *
-	 * @param WC_Product $product Product object.
-	 * @param bool       $force Force update. Used during create.
-	 * @param bool       $creating Whether the product is being created.
-	 * @param array      $existing_meta_keys Existing meta keys map, maintained across calls during creation. Passed by reference.
-	 * @return void
-	 */
-	protected function update_post_meta_internal( &$product, $force, $creating, &$existing_meta_keys = null ) {
 		$meta_key_to_props = array(
 			'_children' => 'children',
 		);
@@ -51,7 +38,7 @@ class WC_Product_Grouped_Data_Store_CPT extends WC_Product_Data_Store_CPT implem
 			}
 		}

-		parent::update_post_meta_internal( $product, $force, $creating, $existing_meta_keys );
+		parent::update_post_meta( $product, $force );
 	}

 	/**
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
index 6df569ac85a..731f2d0a242 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php
@@ -20,18 +20,6 @@ if ( ! defined( 'ABSPATH' ) ) {
  * @version  3.0.0
  */
 class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface {
-	/**
-	 * Meta data which should exist in the DB, even if empty.
-	 *
-	 * @since 3.6.0
-	 *
-	 * @var array
-	 */
-	protected $must_exist_meta_keys = array(
-		'_tax_class',
-		'_variation_description',
-	);
-
 	/**
 	 * Callback to remove unwanted meta data.
 	 *
@@ -169,7 +157,7 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
 		if ( $id && ! is_wp_error( $id ) ) {
 			$product->set_id( $id );

-			$this->update_post_meta_internal( $product, true, true );
+			$this->update_post_meta( $product, true );
 			$this->update_terms( $product, true );
 			$this->update_visibility( $product, true );
 			$this->update_attributes( $product, true );
@@ -607,19 +595,6 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
 	 * @param bool       $force Force update. Used during create.
 	 */
 	public function update_post_meta( &$product, $force = false ) {
-		$this->update_post_meta_internal( $product, $force, false );
-	}
-
-	/**
-	 * Internal implementation of update_post_meta() that also knows whether the variation is being created.
-	 *
-	 * @since 3.0.0
-	 * @param WC_Product $product Product object.
-	 * @param bool       $force Force update. Used during create.
-	 * @param bool       $creating Whether the variation is being created.
-	 * @param array      $existing_meta_keys Existing meta keys map, maintained across calls during creation. Passed by reference.
-	 */
-	protected function update_post_meta_internal( &$product, $force, $creating, &$existing_meta_keys = null ) {
 		$meta_key_to_props = array(
 			'_variation_description' => 'description',
 		);
@@ -628,7 +603,7 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl

 		foreach ( $props_to_update as $meta_key => $prop ) {
 			$value   = $product->{"get_$prop"}( 'edit' );
-			$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value, $creating, $existing_meta_keys );
+			$updated = update_post_meta( $product->get_id(), $meta_key, $value );
 			if ( $updated ) {
 				$this->updated_props[] = $prop;
 			}
@@ -650,14 +625,14 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
 			$cogs_value_is_additive = apply_filters( 'woocommerce_save_product_cogs_is_additive_flag', $cogs_value_is_additive, $product );

 			if ( ! is_null( $cogs_value_is_additive ) ) {
-				$updated = $this->update_or_delete_post_meta( $product, '_cogs_value_is_additive', $cogs_value_is_additive ? 'yes' : '', $creating, $existing_meta_keys );
+				$updated = $this->update_or_delete_post_meta( $product, '_cogs_value_is_additive', $cogs_value_is_additive ? 'yes' : '' );
 				if ( $updated ) {
 					$this->updated_props[] = 'cogs_value_is_additive';
 				}
 			}
 		}

-		parent::update_post_meta_internal( $product, $force, $creating, $existing_meta_keys );
+		parent::update_post_meta( $product, $force );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
index 33a2a9e800d..27c688253fa 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
@@ -362,57 +362,6 @@ class WC_Product_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		$this->assertSame( '30.000000', get_post_meta( $product_id, '_stock', true ) );
 	}

-	/**
-	 * @testdox Creating a product updates internal meta pre-created by save_post hooks instead of duplicating it.
-	 */
-	public function test_create_updates_internal_meta_precreated_by_save_post_hooks(): void {
-		$precreate_stock_meta = function ( $post_id, $post, $update ) {
-			if ( $update || metadata_exists( 'post', $post_id, '_stock' ) ) {
-				return;
-			}
-
-			add_post_meta( $post_id, '_manage_stock', 'no' );
-			add_post_meta( $post_id, '_stock', '0' );
-		};
-
-		add_action( 'save_post_product', $precreate_stock_meta, 10, 3 );
-
-		try {
-			$product = new WC_Product();
-			$product->set_name( 'Pre-created meta product' );
-			$product->set_manage_stock( true );
-			$product->set_stock_quantity( 5 );
-			$product->save();
-		} finally {
-			remove_action( 'save_post_product', $precreate_stock_meta, 10 );
-		}
-
-		$product_id = $product->get_id();
-
-		$this->assertSame( array( 'yes' ), get_post_meta( $product_id, '_manage_stock', false ) );
-		$this->assertSame( array( '5' ), get_post_meta( $product_id, '_stock', false ) );
-		$this->assertTrue( wc_get_product( $product_id )->get_manage_stock() );
-		$this->assertSame( 5, wc_get_product( $product_id )->get_stock_quantity() );
-	}
-
-	/**
-	 * @testdox Creating a grouped product persists children on the initial save.
-	 */
-	public function test_grouped_product_create_persists_children_on_initial_save(): void {
-		$child_1 = WC_Helper_Product::create_simple_product();
-		$child_2 = WC_Helper_Product::create_simple_product();
-
-		$product = new WC_Product_Grouped();
-		$product->set_name( 'Grouped product' );
-		$product->set_children( array( $child_1->get_id(), $child_2->get_id() ) );
-		$product->save();
-
-		$product_id = $product->get_id();
-
-		$this->assertSame( array( $child_1->get_id(), $child_2->get_id() ), get_post_meta( $product_id, '_children', true ) );
-		$this->assertSame( array( $child_1->get_id(), $child_2->get_id() ), wc_get_product( $product_id )->get_children() );
-	}
-
 	/**
 	 * Test update_product_sales updates on the meta-entry.
 	 */
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php
index b61bca2786a..d1fdb94a2dc 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variations-data-store-cpt-test.php
@@ -295,73 +295,6 @@ class WC_Product_Variation_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		$this->assertEquals( $flag_value ? 'yes' : '', get_post_meta( $product->get_id(), '_cogs_value_is_additive', true ) );
 	}

-	/**
-	 * @testdox Creating a variation inserts known-new internal meta without update/delete hooks.
-	 */
-	public function test_create_inserts_known_new_internal_meta_without_update_or_delete_hooks() {
-		$parent = new WC_Product_Variable();
-		$parent->set_name( 'Variable product' );
-		$parent_id = $parent->save();
-
-		$added_meta_events   = array();
-		$updated_meta_events = array();
-		$deleted_meta_events = array();
-
-		$added_meta_observer = function ( $meta_id, $object_id, $meta_key ) use ( &$added_meta_events ) {
-			$added_meta_events[] = array( $object_id, $meta_key );
-		};
-
-		$updated_meta_observer = function ( $meta_id, $object_id, $meta_key ) use ( &$updated_meta_events ) {
-			$updated_meta_events[] = array( $object_id, $meta_key );
-		};
-
-		$deleted_meta_observer = function ( $meta_ids, $object_id, $meta_key ) use ( &$deleted_meta_events ) {
-			$deleted_meta_events[] = array( $object_id, $meta_key );
-		};
-
-		add_action( 'added_post_meta', $added_meta_observer, 10, 3 );
-		add_action( 'updated_post_meta', $updated_meta_observer, 10, 3 );
-		add_action( 'deleted_post_meta', $deleted_meta_observer, 10, 3 );
-
-		try {
-			$variation = new WC_Product_Variation();
-			$variation->set_parent_id( $parent_id );
-			$variation->set_sku( 'test-variation-fast-path' );
-			$variation->set_regular_price( '12.50' );
-			$variation->set_manage_stock( true );
-			$variation->set_stock_quantity( 4 );
-			$variation->save();
-		} finally {
-			remove_action( 'added_post_meta', $added_meta_observer, 10 );
-			remove_action( 'updated_post_meta', $updated_meta_observer, 10 );
-			remove_action( 'deleted_post_meta', $deleted_meta_observer, 10 );
-		}
-
-		$variation_id           = $variation->get_id();
-		$filter_to_variation_id = static function ( array $events ) use ( $variation_id ) {
-			return array_values(
-				array_map(
-					static fn( $event ) => $event[1],
-					array_filter( $events, static fn( $event ) => (int) $event[0] === $variation_id )
-				)
-			);
-		};
-
-		$added_meta_keys   = $filter_to_variation_id( $added_meta_events );
-		$updated_meta_keys = $filter_to_variation_id( $updated_meta_events );
-		$deleted_meta_keys = $filter_to_variation_id( $deleted_meta_events );
-
-		$this->assertContains( '_sku', $added_meta_keys );
-		$this->assertContains( '_regular_price', $added_meta_keys );
-		$this->assertContains( '_price', $added_meta_keys );
-		$this->assertContains( '_manage_stock', $added_meta_keys );
-		$this->assertContains( '_stock', $added_meta_keys );
-		$this->assertContains( '_variation_description', $added_meta_keys );
-		$this->assertEmpty( $updated_meta_keys );
-		$this->assertEmpty( $deleted_meta_keys );
-		$this->assertSame( array( '' ), get_post_meta( $variation_id, '_variation_description', false ) );
-	}
-
 	/**
 	 * Create a variable product and return one of its variations.
 	 *