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.
*/