Commit 01632275493 for woocommerce
commit 016322754934b5cc2cbf5b8ff2706138c9674b5f
Author: agent-sandbox-woocommerce[bot] <282076412+agent-sandbox-woocommerce[bot]@users.noreply.github.com>
Date: Thu Nov 5 13:14:20 2026 +0000
Keep multi-value order meta in HPOS order when syncing to posts (#70108)
diff --git a/plugins/woocommerce/changelog/fxlt-10-hpos-sync-multi-value-meta-order b/plugins/woocommerce/changelog/fxlt-10-hpos-sync-multi-value-meta-order
new file mode 100644
index 00000000000..5cf60d066cc
--- /dev/null
+++ b/plugins/woocommerce/changelog/fxlt-10-hpos-sync-multi-value-meta-order
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep order meta in the posts tables in step with HPOS when sync is on: same order for multi-value meta, and no duplicate rows on sync-on-read.
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
index e120cffa81a..b472c4efd49 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
@@ -3673,7 +3673,7 @@ CREATE TABLE $meta_table (
$delete_meta = $this->data_store_meta->delete_meta( $object, $meta );
$changes_applied = $this->after_meta_change( $object, $meta );
- if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() && isset( $meta->key ) ) {
+ if ( ! $changes_applied && $object instanceof WC_Abstract_Order && ! isset( self::$sync_on_read_order_ids[ $object->get_id() ] ) && $this->should_backfill_post_record() && isset( $meta->key ) ) {
self::$backfilling_order_ids[] = $object->get_id();
if ( is_object( $meta->value ) && '__PHP_Incomplete_Class' === get_class( $meta->value ) ) {
$meta_value = maybe_serialize( $meta->value );
@@ -3708,16 +3708,18 @@ CREATE TABLE $meta_table (
* @return int|bool meta ID or false on failure
*/
public function add_meta( &$object, $meta ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
- $add_meta = $this->data_store_meta->add_meta( $object, $meta );
- $meta->id = $add_meta;
- $changes_applied = $this->after_meta_change( $object, $meta );
+ $add_meta = $this->data_store_meta->add_meta( $object, $meta );
+ $meta->id = $add_meta;
- if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) {
+ // Write the post meta before after_meta_change(): a full order save there would add any later meta first.
+ if ( $object instanceof WC_Abstract_Order && ! isset( self::$sync_on_read_order_ids[ $object->get_id() ] ) && $this->should_backfill_post_record() ) {
self::$backfilling_order_ids[] = $object->get_id();
add_post_meta( $object->get_id(), $meta->key, $meta->value );
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
}
+ $this->after_meta_change( $object, $meta );
+
return $add_meta;
}
@@ -3733,7 +3735,7 @@ CREATE TABLE $meta_table (
$update_meta = $this->data_store_meta->update_meta( $object, $meta );
$changes_applied = $this->after_meta_change( $object, $meta );
- if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) {
+ if ( ! $changes_applied && $object instanceof WC_Abstract_Order && ! isset( self::$sync_on_read_order_ids[ $object->get_id() ] ) && $this->should_backfill_post_record() ) {
self::$backfilling_order_ids[] = $object->get_id();
update_post_meta( $object->get_id(), $meta->key, $meta->value );
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
index a9e5c3888f5..66c91c485db 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
@@ -1693,6 +1693,39 @@ class OrdersTableDataStoreTests extends \HposTestCase {
remove_all_filters( 'woocommerce_hpos_enable_sync_on_read' );
}
+ /**
+ * @testDox Sync-on-read does not write duplicate rows back to wp_postmeta when migrating meta.
+ */
+ public function test_sync_on_read_does_not_duplicate_postmeta() {
+ $this->enable_cot_sync();
+ $this->toggle_cot_feature_and_usage( true );
+ add_filter( 'woocommerce_hpos_enable_sync_on_read', '__return_true' );
+ $order = $this->create_complex_cot_order();
+ $order->add_meta_data( 'multi_meta', 'x' );
+ $order->add_meta_data( 'multi_meta', 'y' );
+ $order->add_meta_data( 'changed_meta', 'before' );
+ $order->save();
+
+ // Simulate direct writes to the posts table.
+ add_post_meta( $order->get_id(), 'new_meta', 'new_value' ); // add_meta path.
+ update_post_meta( $order->get_id(), 'changed_meta', 'after' ); // update_meta path.
+ delete_post_meta( $order->get_id(), 'multi_meta', 'x' ); // delete_meta path.
+ add_post_meta( $order->get_id(), 'multi_meta', 'z' );
+
+ $refreshed_order = new WC_Order();
+ $refreshed_order->set_id( $order->get_id() );
+ $this->switch_data_store( $refreshed_order, $this->sut );
+ $this->sut->read( $refreshed_order );
+
+ $this->assertEquals( 'new_value', $refreshed_order->get_meta( 'new_meta' ) );
+ $this->assertEquals( 'after', $refreshed_order->get_meta( 'changed_meta' ) );
+
+ $this->assertEquals( array( 'new_value' ), get_post_meta( $order->get_id(), 'new_meta', false ) );
+ $this->assertEquals( array( 'after' ), get_post_meta( $order->get_id(), 'changed_meta', false ) );
+ $this->assertEquals( array( 'y', 'z' ), get_post_meta( $order->get_id(), 'multi_meta', false ) );
+ remove_all_filters( 'woocommerce_hpos_enable_sync_on_read' );
+ }
+
/**
* @testDox Test that we are able to correctly detect when order and post are out of sync.
*/
@@ -3726,6 +3759,25 @@ class OrdersTableDataStoreTests extends \HposTestCase {
$this->assertFalse( in_array( 'test_value5', $post_meta, true ) );
}
+ /**
+ * @testdox Multi-value meta keeps its order in the posts table when a meta save triggers a full order save.
+ */
+ public function test_multi_value_meta_order_is_synced_when_meta_save_triggers_order_save(): void {
+ $this->toggle_cot_authoritative( true );
+ $this->enable_cot_sync();
+
+ $order = WC_Helper_Order::create_order();
+ // An earlier modified date makes the first add_meta() call trigger a full order save.
+ $order->set_date_modified( gmdate( 'Y-m-d H:i:s', strtotime( '-2 day' ) ) );
+ $order->save();
+
+ $order->add_meta_data( 'test_key', 'test_value1' );
+ $order->add_meta_data( 'test_key', 'test_value2' );
+ $order->save_meta_data();
+
+ $this->assertSame( array( 'test_value1', 'test_value2' ), get_post_meta( $order->get_id(), 'test_key', false ) );
+ }
+
/**
* @testDox Test that date queries correctly handle timezones.
*/