Commit 3683411989f for woocommerce
commit 3683411989fd9007263951ec3cc75e57ababd3bf
Author: Mike Jolley <mike.jolley@me.com>
Date: Fri Oct 30 15:43:55 2026 +0000
Fix replaced order items not saving on SQLite sites (#69986)
* Fix order item meta deletes failing on the SQLite database integration
* Add changelog entry for SQLite order item meta delete fix
diff --git a/plugins/woocommerce/changelog/fix-sqlite-order-item-meta-delete b/plugins/woocommerce/changelog/fix-sqlite-order-item-meta-delete
new file mode 100644
index 00000000000..10ad4234da1
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-sqlite-order-item-meta-delete
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix replaced order items not being saved on sites using the SQLite database integration, such as WordPress Studio and Playground.
diff --git a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
index fe7296e2b50..8dde869ba80 100644
--- a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
@@ -874,14 +874,27 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
$ids_placeholders = implode( ', ', array_fill( 0, count( $sanitized_ids ), '%d' ) );
- $result = $wpdb->query(
+ // Only delete meta of items that belong to this order.
+ $owned_ids = $wpdb->get_col(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $ids_placeholders is generated above.
- "DELETE itemmeta FROM {$wpdb->prefix}woocommerce_order_itemmeta as itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items as items WHERE itemmeta.order_item_id = items.order_item_id AND items.order_id = %d AND items.order_item_id IN ($ids_placeholders)",
+ "SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d AND order_item_id IN ($ids_placeholders)",
array_merge( array( $order->get_id() ), $sanitized_ids )
)
);
- if ( false === $result ) {
+ if ( '' !== $wpdb->last_error ) {
+ wc_get_logger()->error(
+ 'Failed to read order item IDs for deletion.',
+ array(
+ 'source' => 'order-data-store',
+ 'order_id' => $order->get_id(),
+ 'error' => $wpdb->last_error,
+ )
+ );
+ throw new Exception( esc_html__( 'Unable to delete order item metadata.', 'woocommerce' ) );
+ }
+
+ if ( false === $this->delete_order_item_meta( $owned_ids ) ) {
wc_get_logger()->error(
'Failed to delete order item metadata.',
array(
@@ -934,16 +947,43 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
}
if ( ! empty( $type ) ) {
- $wpdb->query( $wpdb->prepare( "DELETE itemmeta FROM {$wpdb->prefix}woocommerce_order_itemmeta as itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items as items WHERE itemmeta.order_item_id = items.order_item_id AND items.order_id = %d AND items.order_item_type = %s", $order_id, $type ) );
+ $this->delete_order_item_meta( $wpdb->get_col( $wpdb->prepare( "SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d AND order_item_type = %s", $order_id, $type ) ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d AND order_item_type = %s", $order_id, $type ) );
} else {
- $wpdb->query( $wpdb->prepare( "DELETE itemmeta FROM {$wpdb->prefix}woocommerce_order_itemmeta as itemmeta INNER JOIN {$wpdb->prefix}woocommerce_order_items as items WHERE itemmeta.order_item_id = items.order_item_id and items.order_id = %d", $order_id ) );
+ $this->delete_order_item_meta( $wpdb->get_col( $wpdb->prepare( "SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d", $order_id ) ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d", $order_id ) );
}
$this->clear_caches( $order );
}
+ /**
+ * Delete the metadata of the given order items.
+ *
+ * Uses a single-table DELETE because the SQLite database integration used by Studio and Playground can't run a multi-table one.
+ *
+ * @param int[] $item_ids Order item IDs.
+ * @return int|bool Number of deleted rows, true when there was nothing to delete, or false on failure.
+ */
+ private function delete_order_item_meta( array $item_ids ) {
+ global $wpdb;
+
+ $item_ids = array_map( 'intval', $item_ids );
+ if ( empty( $item_ids ) ) {
+ return true;
+ }
+
+ $placeholders = implode( ', ', array_fill( 0, count( $item_ids ), '%d' ) );
+
+ return $wpdb->query(
+ $wpdb->prepare(
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders is generated above.
+ "DELETE FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id IN ($placeholders)",
+ $item_ids
+ )
+ );
+ }
+
/**
* Get token ids for an order.
*
diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php
index 9d6991f68b6..338e9061481 100644
--- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php
+++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php
@@ -1805,7 +1805,8 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
*/
public static function provide_failed_order_item_delete_queries(): array {
return array(
- 'item metadata delete' => array( 'DELETE itemmeta FROM' ),
+ 'item ID lookup' => array( 'SELECT order_item_id FROM {prefix}woocommerce_order_items WHERE order_id' ),
+ 'item metadata delete' => array( 'DELETE FROM {prefix}woocommerce_order_itemmeta' ),
'item row delete' => array( 'DELETE FROM {prefix}woocommerce_order_items' ),
);
}
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
index c9fe0c02e2b..c1140d06c25 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
@@ -576,6 +576,9 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$this->assertFalse( $r_order->get_item( $fee_item_2->get_id() ) );
$this->assertTrue( $r_order->get_item( $product_item->get_id() )->get_id() === $product_item->get_id() );
$this->assertTrue( $r_order->get_item( $shipping_item->get_id() )->get_id() === $shipping_item->get_id() );
+ $this->assertSame( 0, $this->get_item_meta_count( $fee_item_1->get_id() ), 'Deleted items should not leave metadata behind.' );
+ $this->assertSame( 0, $this->get_item_meta_count( $fee_item_2->get_id() ), 'Deleted items should not leave metadata behind.' );
+ $this->assertGreaterThan( 0, $this->get_item_meta_count( $product_item->get_id() ), 'Metadata of items of other types should remain.' );
// Deleting all items should all items.
$r_order->get_data_store()->delete_items( $r_order );
@@ -583,6 +586,8 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$this->assertFalse( $r_order->get_item( $fee_item_2->get_id() ) );
$this->assertFalse( $r_order->get_item( $product_item->get_id() ) );
$this->assertFalse( $r_order->get_item( $shipping_item->get_id() ) );
+ $this->assertSame( 0, $this->get_item_meta_count( $product_item->get_id() ), 'Deleted items should not leave metadata behind.' );
+ $this->assertSame( 0, $this->get_item_meta_count( $shipping_item->get_id() ), 'Deleted items should not leave metadata behind.' );
}
/**
@@ -603,6 +608,20 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$this->assertFalse( WC_Order_Factory::get_order_item( $order_1_item_id ), 'A valid item belonging to the target order should be deleted.' );
$this->assertInstanceOf( WC_Order_Item::class, WC_Order_Factory::get_order_item( $order_2_item_id ), 'An item belonging to another order should remain.' );
+
+ $this->assertSame( 0, $this->get_item_meta_count( $order_1_item_id ), 'The deleted item should not leave metadata behind.' );
+ $this->assertGreaterThan( 0, $this->get_item_meta_count( $order_2_item_id ), 'Metadata of an item belonging to another order should remain.' );
+ }
+
+ /**
+ * Count the metadata rows stored for an order item.
+ *
+ * @param int $item_id Order item ID.
+ * @return int
+ */
+ private function get_item_meta_count( int $item_id ): int {
+ global $wpdb;
+ return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d", $item_id ) );
}
/**
@@ -634,6 +653,7 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$this->assertFalse( $order_1->get_item( $product_item_1->get_id() ) );
$this->assertTrue( $order_2->get_item( $product_item_2->get_id() )->get_id() === $product_item_2->get_id() );
+ $this->assertGreaterThan( 0, $this->get_item_meta_count( $product_item_2->get_id() ), 'Metadata of items belonging to another order should remain.' );
}
/**