Commit 829b10018cb for woocommerce

commit 829b10018cbb877c789e62b829e2119ee8890b9a
Author: Darren Ethier <darren@roughsmootheng.in>
Date:   Fri Aug 28 15:39:21 2026 -0400

    Cache repeated CSV importer original-ID lookups (#68018)

diff --git a/plugins/woocommerce/changelog/fix-wooplug-7558-original-id-cache b/plugins/woocommerce/changelog/fix-wooplug-7558-original-id-cache
new file mode 100644
index 00000000000..3a0cf376017
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-7558-original-id-cache
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Cache CSV importer original-ID lookups within each import batch, so CSVs with repeated IDs or many product references (grouped, upsell, cross-sell) no longer run the same postmeta query for every occurrence.
diff --git a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
index 5adf3b4ace1..f7ddebbdec4 100644
--- a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
+++ b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php
@@ -47,6 +47,13 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
 	 */
 	private $cogs_is_enabled = false;

+	/**
+	 * Product IDs keyed by their original CSV IDs for this import request.
+	 *
+	 * @var array<int, int>
+	 */
+	private $original_id_map = array();
+
 	/**
 	 * Initialize importer.
 	 *
@@ -195,6 +202,49 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
 		}
 	}

+	/**
+	 * Find a product previously mapped from an original CSV ID.
+	 *
+	 * Only mappings backed by an `_original_id` meta row are cached: successful
+	 * lookups and placeholders created by this instance. Misses are not cached
+	 * because a later row or a hook can still create the mapping. The cache
+	 * assumes placeholders are not deleted while a batch is being parsed.
+	 *
+	 * @param int $original_id Original product ID from the CSV file.
+	 * @return int Mapped product ID, or 0 when no mapping exists.
+	 */
+	private function get_product_id_by_original_id( $original_id ): int {
+		global $wpdb;
+
+		$original_id = absint( $original_id );
+		if ( isset( $this->original_id_map[ $original_id ] ) ) {
+			return $this->original_id_map[ $original_id ];
+		}
+
+		$product_id = absint( $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $original_id ) ) );
+
+		if ( $product_id ) {
+			$this->original_id_map[ $original_id ] = $product_id;
+		}
+
+		return $product_id;
+	}
+
+	/**
+	 * Remember a placeholder created for an original CSV ID during this import request.
+	 *
+	 * @param int $original_id Original product ID from the CSV file.
+	 * @param int $product_id  Placeholder product ID.
+	 */
+	private function remember_original_id_mapping( $original_id, $product_id ): void {
+		$original_id = absint( $original_id );
+		$product_id  = absint( $product_id );
+
+		if ( $original_id && $product_id ) {
+			$this->original_id_map[ $original_id ] = $product_id;
+		}
+	}
+
 	/**
 	 * Parse relative field and return product ID.
 	 *
@@ -222,10 +272,10 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
 			$id = intval( $matches[1] );

 			// If original_id is found, use that instead of the given ID since a new placeholder must have been created already.
-			$original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok.
+			$mapped_product_id = $this->get_product_id_by_original_id( $id );

-			if ( $original_id ) {
-				return absint( $original_id );
+			if ( $mapped_product_id ) {
+				return $mapped_product_id;
 			}

 			// See if the given ID maps to a valid product already.
@@ -241,7 +291,10 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
 				$product->set_name( 'Import placeholder for ' . $id );
 				$product->set_status( 'importing' );
 				$product->add_meta_data( '_original_id', $id, true );
-				$id = $product->save();
+				$placeholder_id = $product->save();
+				$this->remember_original_id_mapping( $id, $placeholder_id );
+
+				return $placeholder_id;
 			}

 			return $id;
@@ -281,8 +334,6 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
 	 * @return int
 	 */
 	public function parse_id_field( $value ) {
-		global $wpdb;
-
 		$id = absint( $value );

 		if ( ! $id ) {
@@ -290,10 +341,10 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
 		}

 		// See if this maps to an ID placeholder already.
-		$original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok.
+		$mapped_product_id = $this->get_product_id_by_original_id( $id );

-		if ( $original_id ) {
-			return absint( $original_id );
+		if ( $mapped_product_id ) {
+			return $mapped_product_id;
 		}

 		// Not updating? Make sure we have a new placeholder for this ID.
@@ -317,10 +368,13 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
 			if ( $row_sku ) {
 				$product->set_sku( $row_sku );
 			}
-			$id = $product->save();
+			$placeholder_id = $product->save();
+			$this->remember_original_id_mapping( $id, $placeholder_id );
+
+			return $placeholder_id;
 		}

-		return $id && ! is_wp_error( $id ) ? $id : 0;
+		return $id;
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php b/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
index 6a895df04ae..67a191edaf9 100644
--- a/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
+++ b/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
@@ -125,6 +125,99 @@ class WC_Product_CSV_Importer_Test extends \WC_Unit_Test_Case {
 		$this->assertEquals( 100, $importer->get_percent_complete() );
 	}

+	/**
+	 * @testdox Resolved original IDs are reused consistently without repeated lookups
+	 */
+	public function test_original_id_mapping_is_reused_within_an_import() {
+		$original_id      = 987654321;
+		$importer         = new WC_Product_CSV_Importer( __DIR__ . '/sample.csv' );
+		$existing_product = WC_Helper_Product::create_simple_product();
+		$query_count      = 0;
+		$count_query      = static function ( $query ) use ( &$query_count ) {
+			if ( false !== strpos( $query, 'SELECT post_id' ) && false !== strpos( $query, "meta_key = '_original_id'" ) ) {
+				++$query_count;
+			}
+
+			return $query;
+		};
+
+		$referenced_product_id = 0;
+
+		add_filter( 'query', $count_query );
+
+		try {
+			$referenced_product_id = $importer->parse_relative_field( 'id:' . $original_id );
+			$row_product_id        = $importer->parse_id_field( (string) $original_id );
+			$duplicate_product_id  = $importer->parse_id_field( (string) $original_id );
+
+			$first_existing_product_id = $importer->parse_relative_field( 'id:' . $existing_product->get_id() );
+			$next_existing_product_id  = $importer->parse_relative_field( 'id:' . $existing_product->get_id() );
+
+			$this->assertGreaterThan( 0, $referenced_product_id );
+			$this->assertSame( $referenced_product_id, $row_product_id );
+			$this->assertSame( $referenced_product_id, $duplicate_product_id );
+			$this->assertSame( $existing_product->get_id(), $first_existing_product_id );
+			$this->assertSame( $existing_product->get_id(), $next_existing_product_id );
+			$this->assertSame( 3, $query_count, 'Only mappings backed by _original_id meta are cached; references to existing products still query.' );
+		} finally {
+			remove_filter( 'query', $count_query );
+			WC_Helper_Product::delete_product( $existing_product->get_id() );
+			if ( $referenced_product_id ) {
+				WC_Helper_Product::delete_product( $referenced_product_id );
+			}
+		}
+	}
+
+	/**
+	 * @testdox A reference to an existing product does not map a later row with the same ID onto that product
+	 */
+	public function test_reference_to_existing_product_does_not_map_later_row_with_same_id() {
+		$existing_product = WC_Helper_Product::create_simple_product();
+		$existing_id      = $existing_product->get_id();
+		$importer         = new WC_Product_CSV_Importer( __DIR__ . '/sample.csv', array( 'update_existing' => false ) );
+		$placeholder_id   = 0;
+
+		try {
+			$this->assertSame( $existing_id, $importer->parse_relative_field( 'id:' . $existing_id ) );
+
+			$placeholder_id = $importer->parse_id_field( (string) $existing_id );
+			$placeholder    = wc_get_product( $placeholder_id );
+
+			$this->assertNotSame( $existing_id, $placeholder_id );
+			$this->assertSame( 'importing', $placeholder->get_status() );
+			$this->assertEquals( $existing_id, $placeholder->get_meta( '_original_id' ) );
+			$this->assertSame( $placeholder_id, $importer->parse_relative_field( 'id:' . $existing_id ) );
+		} finally {
+			WC_Helper_Product::delete_product( $existing_id );
+			if ( $placeholder_id ) {
+				WC_Helper_Product::delete_product( $placeholder_id );
+			}
+		}
+	}
+
+	/**
+	 * @testdox An original-ID lookup that initially misses can resolve a mapping created later in the import
+	 */
+	public function test_original_id_lookup_does_not_cache_misses() {
+		$original_id = 987654322;
+		$importer    = new WC_Product_CSV_Importer(
+			__DIR__ . '/sample.csv',
+			array( 'update_existing' => true )
+		);
+
+		$this->assertSame( $original_id, $importer->parse_relative_field( 'id:' . $original_id ) );
+
+		$product = WC_Helper_Product::create_simple_product();
+		$product->add_meta_data( '_original_id', $original_id, true );
+		$product->save();
+
+		try {
+			$this->assertSame( $product->get_id(), $importer->parse_id_field( (string) $original_id ) );
+		} finally {
+			WC_Helper_Product::delete_product( $product->get_id() );
+		}
+	}
+
 	/**
 	 * @testdox Test that the importer skips updating products with the same SKU.
 	 */