Commit 6952c567120 for woocommerce
commit 6952c567120e6f6bee89e7147a0aaf5ae64e179a
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date: Fri Oct 23 05:34:30 2026 -0500
Disambiguate orders using ID in admin order list when sorting by date (#69460)
* Keep the admin orders list in a stable order when creation dates tie
* Add tests for the stable ordering in the orders list tables
* Add changelog entry
diff --git a/plugins/woocommerce/changelog/wooplug-810-stable-order-list b/plugins/woocommerce/changelog/wooplug-810-stable-order-list
new file mode 100644
index 00000000000..8f07e5f40c3
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-810-stable-order-list
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep the admin orders list in a stable order when several orders share the same creation date.
diff --git a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-orders.php b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-orders.php
index ef7cf424a1d..83c30d1455a 100644
--- a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-orders.php
+++ b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-orders.php
@@ -642,6 +642,11 @@ class WC_Admin_List_Table_Orders extends WC_Admin_List_Table {
}
}
+ // Order by ID as well to keep the ordering stable.
+ if ( empty( $query_vars['orderby'] ) || 'date' === $query_vars['orderby'] ) {
+ $query_vars['orderby'] = 'date ID';
+ }
+
// Status.
if ( empty( $query_vars['post_status'] ) ) {
$post_statuses = wc_get_order_statuses();
diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php
index 547190b3dc4..65716f679a2 100644
--- a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php
+++ b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php
@@ -496,11 +496,15 @@ class ListTable extends WP_List_Table {
if ( ! in_array( $field, $sortable, true ) ) {
$this->order_query_args['orderby'] = 'date';
$this->order_query_args['order'] = 'DESC';
- return;
+ } else {
+ $this->order_query_args['orderby'] = $field;
+ $this->order_query_args['order'] = in_array( $direction, array( 'ASC', 'DESC' ), true ) ? $direction : 'ASC';
}
- $this->order_query_args['orderby'] = $field;
- $this->order_query_args['order'] = in_array( $direction, array( 'ASC', 'DESC' ), true ) ? $direction : 'ASC';
+ // Order by ID as well to keep the ordering stable.
+ if ( 'date' === $this->order_query_args['orderby'] ) {
+ $this->order_query_args['orderby'] = 'date ID';
+ }
}
/**
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableStatusUnionQuery.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableStatusUnionQuery.php
index 03e01a77aaf..ed2cbe081a1 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableStatusUnionQuery.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableStatusUnionQuery.php
@@ -132,14 +132,17 @@ class OrdersTableStatusUnionQuery {
/**
* Extracts the sort direction from the ORDER BY clause, or NULL when it isn't an ORDER BY on date_created_gmt
- * alone (the only ordering the type_status_date index can satisfy within each branch).
+ * alone, optionally followed by id in the same direction (the only ordering the type_status_date index can
+ * satisfy within each branch).
*
* @param string $orderby The ORDER BY clause, including the keyword.
* @return string|null 'ASC', 'DESC', or NULL when ineligible.
*/
private function extract_order_direction( string $orderby ): ?string {
foreach ( array( 'ASC', 'DESC' ) as $direction ) {
- if ( "ORDER BY {$this->orders_table}.date_created_gmt {$direction}" === $orderby ) {
+ $date_only = "ORDER BY {$this->orders_table}.date_created_gmt {$direction}";
+
+ if ( $date_only === $orderby || "{$date_only}, {$this->orders_table}.id {$direction}" === $orderby ) {
return $direction;
}
}
@@ -236,7 +239,7 @@ class OrdersTableStatusUnionQuery {
foreach ( $types as $type ) {
foreach ( $statuses as $status ) {
$branch = $wpdb->prepare(
- "SELECT id, date_created_gmt FROM {$this->orders_table} WHERE type = %s AND status = %s ORDER BY date_created_gmt {$direction} LIMIT {$branch_rows}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ "SELECT id, date_created_gmt FROM {$this->orders_table} WHERE type = %s AND status = %s ORDER BY date_created_gmt {$direction}, id {$direction} LIMIT {$branch_rows}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$type,
$status
);
@@ -245,7 +248,7 @@ class OrdersTableStatusUnionQuery {
}
}
- return 'SELECT id FROM ( ' . implode( ' UNION ALL ', $branches ) . " ) candidates ORDER BY date_created_gmt {$direction} {$limits}";
+ return 'SELECT id FROM ( ' . implode( ' UNION ALL ', $branches ) . " ) candidates ORDER BY date_created_gmt {$direction}, id {$direction} {$limits}";
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php
index b8f39a97c17..09b143ee6b7 100644
--- a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php
@@ -584,4 +584,21 @@ class WC_Admin_List_Table_Orders_Test extends WC_Unit_Test_Case {
unset( $_GET['order_date_type'], $_GET['m'], $GLOBALS['pagenow'] );
wp_delete_post( $order->get_id(), true );
}
+
+ /**
+ * Test that ordering by date also orders by ID, so orders created at the same time keep a stable order.
+ *
+ * @testWith [{"orderby": ""}]
+ * [{"orderby": "date"}]
+ * @param array $query_vars Query vars as they arrive via the 'request' filter.
+ */
+ public function test_ordering_by_date_also_orders_by_id( array $query_vars ) {
+ $GLOBALS['typenow'] = 'shop_order'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ $query_vars = ( new WC_Admin_List_Table_Orders() )->request_query( $query_vars );
+
+ unset( $GLOBALS['typenow'] );
+
+ $this->assertSame( 'date ID', $query_vars['orderby'] );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php
index 7daddbd35c7..e962a51cfb6 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php
@@ -492,4 +492,20 @@ class ListTableTest extends \WC_Unit_Test_Case {
$this->assertArrayNotHasKey( 'date_created', $query_args, 'An invalid date should not produce a date filter' );
$this->assertCount( 2, $items, 'An invalid date filter should be ignored and all orders shown' );
}
+
+ /**
+ * @testdox Ordering by date also orders by ID, so orders created at the same time keep a stable order.
+ * @testWith [{}]
+ * [{"orderby": "date"}]
+ * @param array $get_params $_GET values to simulate.
+ */
+ public function test_ordering_by_date_also_orders_by_id( array $get_params ): void {
+ foreach ( $get_params as $key => $value ) {
+ $_GET[ $key ] = $value;
+ }
+
+ $this->sut->prepare_items();
+
+ $this->assertSame( 'date ID', $this->get_order_query_args()['orderby'] );
+ }
}