From 80c8b68cdf82b0ba278b1ba75ebd3509169bdfa5 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 27 Oct 2025 12:25:05 +1100 Subject: [PATCH 1/5] Tests only for deterministic ordering. --- .../tests/query/deterministicOrdering.php | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 tests/phpunit/tests/query/deterministicOrdering.php diff --git a/tests/phpunit/tests/query/deterministicOrdering.php b/tests/phpunit/tests/query/deterministicOrdering.php new file mode 100644 index 0000000000000..b46ed02757f03 --- /dev/null +++ b/tests/phpunit/tests/query/deterministicOrdering.php @@ -0,0 +1,301 @@ +post->create( + array( + 'post_title' => "Post $i", + 'post_date' => $identical_date, + ) + ); + } + + // Get first page + $query1 = new WP_Query( + array( + 'orderby' => 'post_date', + 'order' => 'ASC', + 'posts_per_page' => 10, + 'paged' => 1, + ) + ); + + // Get second page + $query2 = new WP_Query( + array( + 'orderby' => 'post_date', + 'order' => 'ASC', + 'posts_per_page' => 10, + 'paged' => 2, + ) + ); + + $page1_ids = wp_list_pluck( $query1->posts, 'ID' ); + $page2_ids = wp_list_pluck( $query2->posts, 'ID' ); + + // Verify no overlap between pages (no duplicates) + $overlap = array_intersect( $page1_ids, $page2_ids ); + $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts' ); + + // Verify total count is correct + $this->assertEquals( 20, $query1->found_posts, 'Total posts should be 20' ); + $this->assertEquals( 10, count( $page1_ids ), 'First page should have 10 posts' ); + $this->assertEquals( 10, count( $page2_ids ), 'Second page should have 10 posts' ); + + // Verify deterministic ordering: same query should return same results + $query1_repeat = new WP_Query( + array( + 'orderby' => 'post_date', + 'order' => 'ASC', + 'posts_per_page' => 10, + 'paged' => 1, + ) + ); + $page1_repeat_ids = wp_list_pluck( $query1_repeat->posts, 'ID' ); + + $this->assertEquals( $page1_ids, $page1_repeat_ids, 'Same query should return same results' ); + } + + /** + * Test that deterministic ordering works with post_title field. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_with_post_title() { + $identical_title = 'Same Title'; + $post_ids = array(); + + for ( $i = 1; $i <= 15; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => $identical_title, + 'post_date' => "2023-01-0$i 10:00:00", + ) + ); + } + + // Get first page + $query1 = new WP_Query( + array( + 'orderby' => 'post_title', + 'order' => 'ASC', + 'posts_per_page' => 8, + 'paged' => 1, + ) + ); + + // Get second page + $query2 = new WP_Query( + array( + 'orderby' => 'post_title', + 'order' => 'ASC', + 'posts_per_page' => 8, + 'paged' => 2, + ) + ); + + $page1_ids = wp_list_pluck( $query1->posts, 'ID' ); + $page2_ids = wp_list_pluck( $query2->posts, 'ID' ); + + // Verify no duplicates across pages + $overlap = array_intersect( $page1_ids, $page2_ids ); + $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts when ordering by title' ); + } + + /** + * Test that deterministic ordering works with DESC order. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_with_desc_order() { + $identical_date = '2023-01-01 10:00:00'; + $post_ids = array(); + + for ( $i = 1; $i <= 12; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => "Post $i", + 'post_date' => $identical_date, + ) + ); + } + + // Get first page with DESC order + $query1 = new WP_Query( + array( + 'orderby' => 'post_date', + 'order' => 'DESC', + 'posts_per_page' => 6, + 'paged' => 1, + ) + ); + + // Get second page with DESC order + $query2 = new WP_Query( + array( + 'orderby' => 'post_date', + 'order' => 'DESC', + 'posts_per_page' => 6, + 'paged' => 2, + ) + ); + + $page1_ids = wp_list_pluck( $query1->posts, 'ID' ); + $page2_ids = wp_list_pluck( $query2->posts, 'ID' ); + + // Verify no duplicates across pages + $overlap = array_intersect( $page1_ids, $page2_ids ); + $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts with DESC order' ); + } + + /** + * Test that deterministic ordering works with array orderby. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_with_array_orderby() { + $identical_date = '2023-01-01 10:00:00'; + $post_ids = array(); + + for ( $i = 1; $i <= 16; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => "Post $i", + 'post_date' => $identical_date, + ) + ); + } + + // Test with array orderby + $query1 = new WP_Query( + array( + 'orderby' => array( + 'post_date' => 'ASC', + 'post_title' => 'ASC', + ), + 'posts_per_page' => 8, + 'paged' => 1, + ) + ); + + $query2 = new WP_Query( + array( + 'orderby' => array( + 'post_date' => 'ASC', + 'post_title' => 'ASC', + ), + 'posts_per_page' => 8, + 'paged' => 2, + ) + ); + + $page1_ids = wp_list_pluck( $query1->posts, 'ID' ); + $page2_ids = wp_list_pluck( $query2->posts, 'ID' ); + + // Verify no duplicates across pages + $overlap = array_intersect( $page1_ids, $page2_ids ); + $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts with array orderby' ); + } + + /** + * Test that deterministic ordering doesn't add ID when ID is already present. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_does_not_duplicate_id() { + $identical_date = '2023-01-01 10:00:00'; + $post_ids = array(); + + for ( $i = 1; $i <= 10; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => "Post $i", + 'post_date' => $identical_date, + ) + ); + } + + $query = new WP_Query( + array( + 'orderby' => 'ID', + 'order' => 'ASC', + 'posts_per_page' => 10, + ) + ); + + // Should not add duplicate ID ordering + $this->assertStringContainsString( 'ID ASC', $query->request ); + $this->assertStringNotContainsString( 'ID ASC, ID ASC', $query->request ); + } + + /** + * Test that deterministic ordering works with search queries. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_with_search() { + $identical_date = '2023-01-01 10:00:00'; + $post_ids = array(); + + for ( $i = 1; $i <= 12; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => "Test Post $i", + 'post_content' => 'This is a test post', + 'post_date' => $identical_date, + ) + ); + } + + // Test with search + $query1 = new WP_Query( + array( + 's' => 'test', + 'orderby' => 'post_date', + 'order' => 'ASC', + 'posts_per_page' => 6, + 'paged' => 1, + ) + ); + + $query2 = new WP_Query( + array( + 's' => 'test', + 'orderby' => 'post_date', + 'order' => 'ASC', + 'posts_per_page' => 6, + 'paged' => 2, + ) + ); + + $page1_ids = wp_list_pluck( $query1->posts, 'ID' ); + $page2_ids = wp_list_pluck( $query2->posts, 'ID' ); + + // Verify no duplicates across pages even with search + $overlap = array_intersect( $page1_ids, $page2_ids ); + $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts even with search' ); + } +} From 558e6e09da06517d0c0d846e4aa46b81a70f67b9 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 27 Oct 2025 12:43:19 +1100 Subject: [PATCH 2/5] Modify date for title test. --- tests/phpunit/tests/query/deterministicOrdering.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/query/deterministicOrdering.php b/tests/phpunit/tests/query/deterministicOrdering.php index b46ed02757f03..294090eab20d6 100644 --- a/tests/phpunit/tests/query/deterministicOrdering.php +++ b/tests/phpunit/tests/query/deterministicOrdering.php @@ -89,10 +89,11 @@ public function test_deterministic_ordering_with_post_title() { $post_ids = array(); for ( $i = 1; $i <= 15; $i++ ) { + $post_day = str_pad( (string) $i, 2, '0', STR_PAD_LEFT ); $post_ids[] = self::factory()->post->create( array( 'post_title' => $identical_title, - 'post_date' => "2023-01-0$i 10:00:00", + 'post_date' => "2023-01-{$post_day} 10:00:00", ) ); } From 1eee2fea5b5f1ee094067849ae64c35046a455ac Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 28 Oct 2025 12:31:22 +1100 Subject: [PATCH 3/5] Try stuff --- .../tests/query/deterministicOrdering.php | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/query/deterministicOrdering.php b/tests/phpunit/tests/query/deterministicOrdering.php index 294090eab20d6..296d21e68d079 100644 --- a/tests/phpunit/tests/query/deterministicOrdering.php +++ b/tests/phpunit/tests/query/deterministicOrdering.php @@ -33,6 +33,17 @@ public function test_deterministic_ordering_prevents_duplicates_across_pages() { ); } + sleep( 1 ); // Ensure modified time changes + // Update every second post if we can trigger the error. + for ( $i = 1; $i <= 20; $i += 2 ) { + wp_update_post( + array( + 'ID' => $post_ids[ $i - 1 ], + 'post_content' => "Updated post $i content", + ) + ); + } + // Get first page $query1 = new WP_Query( array( @@ -89,15 +100,26 @@ public function test_deterministic_ordering_with_post_title() { $post_ids = array(); for ( $i = 1; $i <= 15; $i++ ) { - $post_day = str_pad( (string) $i, 2, '0', STR_PAD_LEFT ); $post_ids[] = self::factory()->post->create( array( 'post_title' => $identical_title, - 'post_date' => "2023-01-{$post_day} 10:00:00", + 'post_date' => "2023-01-0$i 10:00:00", ) ); } + sleep( 1 ); // Ensure modified time changes + // Update every second post if we can trigger the error. + for ( $i = 1; $i <= 15; $i += 2 ) { + wp_update_post( + array( + 'ID' => $post_ids[ $i - 1 ], + 'post_content' => "Updated post $i content", + ) + ); + } + + // Get first page $query1 = new WP_Query( array( @@ -144,6 +166,17 @@ public function test_deterministic_ordering_with_desc_order() { ); } + sleep( 1 ); // Ensure modified time changes + // Update every second post if we can trigger the error. + for ( $i = 1; $i <= 12; $i += 2 ) { + wp_update_post( + array( + 'ID' => $post_ids[ $i - 1 ], + 'post_content' => "Updated post $i content", + ) + ); + } + // Get first page with DESC order $query1 = new WP_Query( array( @@ -189,6 +222,16 @@ public function test_deterministic_ordering_with_array_orderby() { ) ); } + sleep( 1 ); // Ensure modified time changes + // Update every second post if we can trigger the error. + for ( $i = 1; $i <= 15; $i += 2 ) { + wp_update_post( + array( + 'ID' => $post_ids[ $i - 1 ], + 'post_content' => "Updated post $i content", + ) + ); + } // Test with array orderby $query1 = new WP_Query( @@ -238,6 +281,16 @@ public function test_deterministic_ordering_does_not_duplicate_id() { ) ); } + sleep( 1 ); // Ensure modified time changes + // Update every second post if we can trigger the error. + for ( $i = 1; $i <= 10; $i += 2 ) { + wp_update_post( + array( + 'ID' => $post_ids[ $i - 1 ], + 'post_content' => "Updated post $i content", + ) + ); + } $query = new WP_Query( array( @@ -271,6 +324,17 @@ public function test_deterministic_ordering_with_search() { ); } + sleep( 1 ); // Ensure modified time changes + // Update every second post if we can trigger the error. + for ( $i = 1; $i <= 12; $i += 2 ) { + wp_update_post( + array( + 'ID' => $post_ids[ $i - 1 ], + 'post_content' => "Updated test post $i content", + ) + ); + } + // Test with search $query1 = new WP_Query( array( From e5131fe7e5e229cd0e5d14cba66c2fedd850458f Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 28 Oct 2025 12:37:12 +1100 Subject: [PATCH 4/5] OK --- tests/phpunit/tests/query/deterministicOrdering.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/tests/query/deterministicOrdering.php b/tests/phpunit/tests/query/deterministicOrdering.php index 296d21e68d079..7bf9abeecdce3 100644 --- a/tests/phpunit/tests/query/deterministicOrdering.php +++ b/tests/phpunit/tests/query/deterministicOrdering.php @@ -119,7 +119,6 @@ public function test_deterministic_ordering_with_post_title() { ); } - // Get first page $query1 = new WP_Query( array( From c1fdbdfacad426f95a35387798fbd38ca8394e96 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 5 Dec 2025 23:52:11 +1100 Subject: [PATCH 5/5] Test deterministic ordering Added shared fixtures for tests involving identical post dates, titles, and other attributes to prevent duplicate records across paginated results. --- .../tests/query/deterministicOrdering.php | 531 +++++++++++++----- 1 file changed, 401 insertions(+), 130 deletions(-) diff --git a/tests/phpunit/tests/query/deterministicOrdering.php b/tests/phpunit/tests/query/deterministicOrdering.php index 7bf9abeecdce3..1f6941e9fbf6e 100644 --- a/tests/phpunit/tests/query/deterministicOrdering.php +++ b/tests/phpunit/tests/query/deterministicOrdering.php @@ -11,42 +11,157 @@ class Tests_Query_DeterministicOrdering extends WP_UnitTestCase { /** - * Test that deterministic ordering prevents duplicate records across pages. + * Post IDs for posts with identical dates (for date ordering tests). * - * This is the core test for the bug fix. When multiple posts have the same - * value for a field (like post_date), pagination can show duplicate records - * without deterministic ordering. + * @var array + */ + protected static $date_identical_post_ids = array(); + + /** + * Post IDs for posts with identical titles (for title ordering tests). * - * @ticket xxxxx + * @var array */ - public function test_deterministic_ordering_prevents_duplicates_across_pages() { - // Create multiple posts with identical post_date to trigger the bug - $identical_date = '2023-01-01 10:00:00'; - $post_ids = array(); + protected static $title_identical_post_ids = array(); + /** + * Post IDs for search tests. + * + * @var array + */ + protected static $search_post_ids = array(); + + /** + * Post IDs for menu_order tests. + * + * @var array + */ + protected static $menu_order_post_ids = array(); + + /** + * Post IDs for search relevance tests. + * + * @var array + */ + protected static $search_relevance_post_ids = array(); + + /** + * Set up shared fixtures for all tests. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + // Register custom post types for test isolation. + register_post_type( + 'wptests_time_ident', + array( + 'public' => true, + ) + ); + + register_post_type( + 'wptests_title_ident', + array( + 'public' => true, + ) + ); + + // Create posts with identical dates for date ordering tests. + $identical_date = '2023-01-01 10:00:00'; for ( $i = 1; $i <= 20; $i++ ) { - $post_ids[] = self::factory()->post->create( + self::$date_identical_post_ids[] = self::factory()->post->create( array( + 'post_type' => 'wptests_time_ident', 'post_title' => "Post $i", 'post_date' => $identical_date, ) ); } - sleep( 1 ); // Ensure modified time changes - // Update every second post if we can trigger the error. - for ( $i = 1; $i <= 20; $i += 2 ) { - wp_update_post( + // Create posts with identical titles for title ordering tests. + $identical_title = 'Same Title'; + for ( $i = 1; $i <= 15; $i++ ) { + self::$title_identical_post_ids[] = self::factory()->post->create( + array( + 'post_type' => 'wptests_title_ident', + 'post_title' => $identical_title, + 'post_date' => '2023-01-' . str_pad( (string) $i, 2, '0', STR_PAD_LEFT ) . ' 10:00:00', + ) + ); + } + + // Create posts for search tests. + $identical_date = '2023-01-01 10:00:00'; + for ( $i = 1; $i <= 12; $i++ ) { + self::$search_post_ids[] = self::factory()->post->create( + array( + 'post_type' => 'wptests_time_ident', + 'post_title' => "Test Post $i", + 'post_content' => 'This is a test post', + 'post_date' => $identical_date, + ) + ); + } + + // Create pages with identical menu_order for menu_order tests. + for ( $i = 1; $i <= 20; $i++ ) { + self::$menu_order_post_ids[] = self::factory()->post->create( array( - 'ID' => $post_ids[ $i - 1 ], - 'post_content' => "Updated post $i content", + 'post_type' => 'page', + 'post_title' => "Page $i", + 'menu_order' => 0, // All pages have same menu_order ) ); } + // Create posts for search relevance tests. + // All posts will have the same content to ensure same relevance scores. + $identical_content = 'This is a search test post with identical content'; + for ( $i = 1; $i <= 20; $i++ ) { + self::$search_relevance_post_ids[] = self::factory()->post->create( + array( + 'post_type' => 'wptests_time_ident', + 'post_title' => "Search Post $i", + 'post_content' => $identical_content, + 'post_excerpt' => $identical_content, + ) + ); + } + } + + /** + * Clean up after all tests. + */ + public static function tear_down_after_class() { + _unregister_post_type( 'wptests_time_ident' ); + _unregister_post_type( 'wptests_title_ident' ); + + self::$date_identical_post_ids = array(); + self::$title_identical_post_ids = array(); + self::$search_post_ids = array(); + self::$menu_order_post_ids = array(); + self::$search_relevance_post_ids = array(); + + parent::tear_down_after_class(); + } + + /** + * Test that deterministic ordering prevents duplicate records across pages. + * + * This is the core test for the bug fix. When multiple posts have the same + * value for a field (like post_date), pagination can show duplicate records + * without deterministic ordering. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_prevents_duplicates_across_pages() { + // Use shared fixtures with identical post_date + // Get first page $query1 = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$date_identical_post_ids, 'orderby' => 'post_date', 'order' => 'ASC', 'posts_per_page' => 10, @@ -57,6 +172,8 @@ public function test_deterministic_ordering_prevents_duplicates_across_pages() { // Get second page $query2 = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$date_identical_post_ids, 'orderby' => 'post_date', 'order' => 'ASC', 'posts_per_page' => 10, @@ -79,6 +196,8 @@ public function test_deterministic_ordering_prevents_duplicates_across_pages() { // Verify deterministic ordering: same query should return same results $query1_repeat = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$date_identical_post_ids, 'orderby' => 'post_date', 'order' => 'ASC', 'posts_per_page' => 10, @@ -96,32 +215,12 @@ public function test_deterministic_ordering_prevents_duplicates_across_pages() { * @ticket xxxxx */ public function test_deterministic_ordering_with_post_title() { - $identical_title = 'Same Title'; - $post_ids = array(); - - for ( $i = 1; $i <= 15; $i++ ) { - $post_ids[] = self::factory()->post->create( - array( - 'post_title' => $identical_title, - 'post_date' => "2023-01-0$i 10:00:00", - ) - ); - } - - sleep( 1 ); // Ensure modified time changes - // Update every second post if we can trigger the error. - for ( $i = 1; $i <= 15; $i += 2 ) { - wp_update_post( - array( - 'ID' => $post_ids[ $i - 1 ], - 'post_content' => "Updated post $i content", - ) - ); - } - + // Use shared fixtures with identical post_title // Get first page $query1 = new WP_Query( array( + 'post_type' => 'wptests_title_ident', + 'post__in' => self::$title_identical_post_ids, 'orderby' => 'post_title', 'order' => 'ASC', 'posts_per_page' => 8, @@ -132,6 +231,8 @@ public function test_deterministic_ordering_with_post_title() { // Get second page $query2 = new WP_Query( array( + 'post_type' => 'wptests_title_ident', + 'post__in' => self::$title_identical_post_ids, 'orderby' => 'post_title', 'order' => 'ASC', 'posts_per_page' => 8, @@ -153,32 +254,12 @@ public function test_deterministic_ordering_with_post_title() { * @ticket xxxxx */ public function test_deterministic_ordering_with_desc_order() { - $identical_date = '2023-01-01 10:00:00'; - $post_ids = array(); - - for ( $i = 1; $i <= 12; $i++ ) { - $post_ids[] = self::factory()->post->create( - array( - 'post_title' => "Post $i", - 'post_date' => $identical_date, - ) - ); - } - - sleep( 1 ); // Ensure modified time changes - // Update every second post if we can trigger the error. - for ( $i = 1; $i <= 12; $i += 2 ) { - wp_update_post( - array( - 'ID' => $post_ids[ $i - 1 ], - 'post_content' => "Updated post $i content", - ) - ); - } - + // Use shared fixtures with identical post_date // Get first page with DESC order $query1 = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$date_identical_post_ids, 'orderby' => 'post_date', 'order' => 'DESC', 'posts_per_page' => 6, @@ -189,6 +270,8 @@ public function test_deterministic_ordering_with_desc_order() { // Get second page with DESC order $query2 = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$date_identical_post_ids, 'orderby' => 'post_date', 'order' => 'DESC', 'posts_per_page' => 6, @@ -210,31 +293,12 @@ public function test_deterministic_ordering_with_desc_order() { * @ticket xxxxx */ public function test_deterministic_ordering_with_array_orderby() { - $identical_date = '2023-01-01 10:00:00'; - $post_ids = array(); - - for ( $i = 1; $i <= 16; $i++ ) { - $post_ids[] = self::factory()->post->create( - array( - 'post_title' => "Post $i", - 'post_date' => $identical_date, - ) - ); - } - sleep( 1 ); // Ensure modified time changes - // Update every second post if we can trigger the error. - for ( $i = 1; $i <= 15; $i += 2 ) { - wp_update_post( - array( - 'ID' => $post_ids[ $i - 1 ], - 'post_content' => "Updated post $i content", - ) - ); - } - + // Use shared fixtures with identical post_date // Test with array orderby $query1 = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$date_identical_post_ids, 'orderby' => array( 'post_date' => 'ASC', 'post_title' => 'ASC', @@ -246,6 +310,8 @@ public function test_deterministic_ordering_with_array_orderby() { $query2 = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$date_identical_post_ids, 'orderby' => array( 'post_date' => 'ASC', 'post_title' => 'ASC', @@ -269,30 +335,11 @@ public function test_deterministic_ordering_with_array_orderby() { * @ticket xxxxx */ public function test_deterministic_ordering_does_not_duplicate_id() { - $identical_date = '2023-01-01 10:00:00'; - $post_ids = array(); - - for ( $i = 1; $i <= 10; $i++ ) { - $post_ids[] = self::factory()->post->create( - array( - 'post_title' => "Post $i", - 'post_date' => $identical_date, - ) - ); - } - sleep( 1 ); // Ensure modified time changes - // Update every second post if we can trigger the error. - for ( $i = 1; $i <= 10; $i += 2 ) { - wp_update_post( - array( - 'ID' => $post_ids[ $i - 1 ], - 'post_content' => "Updated post $i content", - ) - ); - } - + // Use shared fixtures with identical post_date $query = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$date_identical_post_ids, 'orderby' => 'ID', 'order' => 'ASC', 'posts_per_page' => 10, @@ -310,33 +357,12 @@ public function test_deterministic_ordering_does_not_duplicate_id() { * @ticket xxxxx */ public function test_deterministic_ordering_with_search() { - $identical_date = '2023-01-01 10:00:00'; - $post_ids = array(); - - for ( $i = 1; $i <= 12; $i++ ) { - $post_ids[] = self::factory()->post->create( - array( - 'post_title' => "Test Post $i", - 'post_content' => 'This is a test post', - 'post_date' => $identical_date, - ) - ); - } - - sleep( 1 ); // Ensure modified time changes - // Update every second post if we can trigger the error. - for ( $i = 1; $i <= 12; $i += 2 ) { - wp_update_post( - array( - 'ID' => $post_ids[ $i - 1 ], - 'post_content' => "Updated test post $i content", - ) - ); - } - + // Use shared fixtures for search tests // Test with search $query1 = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$search_post_ids, 's' => 'test', 'orderby' => 'post_date', 'order' => 'ASC', @@ -347,6 +373,8 @@ public function test_deterministic_ordering_with_search() { $query2 = new WP_Query( array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$search_post_ids, 's' => 'test', 'orderby' => 'post_date', 'order' => 'ASC', @@ -362,4 +390,247 @@ public function test_deterministic_ordering_with_search() { $overlap = array_intersect( $page1_ids, $page2_ids ); $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts even with search' ); } + + /** + * Test that deterministic ordering works with menu_order field. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_with_menu_order() { + // Use shared fixtures with identical menu_order + // Get first page + $query1 = new WP_Query( + array( + 'post_type' => 'page', + 'post__in' => self::$menu_order_post_ids, + 'orderby' => 'menu_order', + 'order' => 'ASC', + 'posts_per_page' => 10, + 'paged' => 1, + ) + ); + + // Get second page + $query2 = new WP_Query( + array( + 'post_type' => 'page', + 'post__in' => self::$menu_order_post_ids, + 'orderby' => 'menu_order', + 'order' => 'ASC', + 'posts_per_page' => 10, + 'paged' => 2, + ) + ); + + $page1_ids = wp_list_pluck( $query1->posts, 'ID' ); + $page2_ids = wp_list_pluck( $query2->posts, 'ID' ); + + // Verify no overlap between pages (no duplicates) + $overlap = array_intersect( $page1_ids, $page2_ids ); + $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts when ordering by menu_order' ); + + // Verify total count is correct + $this->assertEquals( 20, $query1->found_posts, 'Total pages should be 20' ); + $this->assertEquals( 10, count( $page1_ids ), 'First page should have 10 pages' ); + $this->assertEquals( 10, count( $page2_ids ), 'Second page should have 10 pages' ); + + // Verify deterministic ordering: same query should return same results + $query1_repeat = new WP_Query( + array( + 'post_type' => 'page', + 'post__in' => self::$menu_order_post_ids, + 'orderby' => 'menu_order', + 'order' => 'ASC', + 'posts_per_page' => 10, + 'paged' => 1, + ) + ); + $page1_repeat_ids = wp_list_pluck( $query1_repeat->posts, 'ID' ); + + $this->assertEquals( $page1_ids, $page1_repeat_ids, 'Same query should return same results when ordering by menu_order' ); + } + + /** + * Test that deterministic ordering works with metadata ordering. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_with_metadata() { + $post_ids = array(); + + // Create posts with identical meta values to trigger the bug + $identical_meta_value = 'same_price'; + for ( $i = 1; $i <= 20; $i++ ) { + $post_id = self::factory()->post->create( + array( + 'post_type' => 'wptests_time_ident', + 'post_title' => "Post $i", + ) + ); + add_post_meta( $post_id, 'price', $identical_meta_value ); + $post_ids[] = $post_id; + } + + // Get first page ordering by metadata + $query1 = new WP_Query( + array( + 'post_type' => 'wptests_time_ident', + 'post__in' => $post_ids, + 'meta_query' => array( + 'price_key' => array( + 'key' => 'price', + 'compare' => 'EXISTS', + ), + ), + 'orderby' => 'price_key', + 'order' => 'ASC', + 'posts_per_page' => 10, + 'paged' => 1, + ) + ); + + // Get second page ordering by metadata + $query2 = new WP_Query( + array( + 'post_type' => 'wptests_time_ident', + 'post__in' => $post_ids, + 'meta_query' => array( + 'price_key' => array( + 'key' => 'price', + 'compare' => 'EXISTS', + ), + ), + 'orderby' => 'price_key', + 'order' => 'ASC', + 'posts_per_page' => 10, + 'paged' => 2, + ) + ); + + $page1_ids = wp_list_pluck( $query1->posts, 'ID' ); + $page2_ids = wp_list_pluck( $query2->posts, 'ID' ); + + // Verify no overlap between pages (no duplicates) + $overlap = array_intersect( $page1_ids, $page2_ids ); + $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts when ordering by metadata' ); + + // Verify total count is correct + $this->assertEquals( 20, $query1->found_posts, 'Total posts should be 20' ); + $this->assertEquals( 10, count( $page1_ids ), 'First page should have 10 posts' ); + $this->assertEquals( 10, count( $page2_ids ), 'Second page should have 10 posts' ); + } + + /** + * Test that deterministic ordering works with search relevance ordering. + * + * When ordering by search relevance, multiple posts can have the same relevance score, + * causing duplicate records across pages without deterministic ordering. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_with_search_relevance() { + // Use shared fixtures with identical content (same relevance scores) + // Get first page ordering by relevance + $query1 = new WP_Query( + array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$search_relevance_post_ids, + 's' => 'search test', + 'orderby' => 'relevance', + 'order' => 'DESC', + 'posts_per_page' => 10, + 'paged' => 1, + ) + ); + + // Get second page ordering by relevance + $query2 = new WP_Query( + array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$search_relevance_post_ids, + 's' => 'search test', + 'orderby' => 'relevance', + 'order' => 'DESC', + 'posts_per_page' => 10, + 'paged' => 2, + ) + ); + + $page1_ids = wp_list_pluck( $query1->posts, 'ID' ); + $page2_ids = wp_list_pluck( $query2->posts, 'ID' ); + + // Verify no overlap between pages (no duplicates) + $overlap = array_intersect( $page1_ids, $page2_ids ); + $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts when ordering by search relevance' ); + + // Verify total count is correct + $this->assertEquals( 20, $query1->found_posts, 'Total posts should be 20' ); + $this->assertEquals( 10, count( $page1_ids ), 'First page should have 10 posts' ); + $this->assertEquals( 10, count( $page2_ids ), 'Second page should have 10 posts' ); + + // Verify deterministic ordering: same query should return same results + $query1_repeat = new WP_Query( + array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$search_relevance_post_ids, + 's' => 'search test', + 'orderby' => 'relevance', + 'order' => 'DESC', + 'posts_per_page' => 10, + 'paged' => 1, + ) + ); + $page1_repeat_ids = wp_list_pluck( $query1_repeat->posts, 'ID' ); + + $this->assertEquals( $page1_ids, $page1_repeat_ids, 'Same query should return same results when ordering by search relevance' ); + } + + /** + * Test that deterministic ordering works with search when orderby is empty (defaults to relevance). + * + * When orderby is empty and search is present, WordPress orders by relevance. + * Multiple posts can have the same relevance score, causing duplicate records across pages. + * + * @ticket xxxxx + */ + public function test_deterministic_ordering_with_search_empty_orderby() { + // Use shared fixtures with identical content (same relevance scores) + // Get first page with empty orderby (defaults to relevance) + $query1 = new WP_Query( + array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$search_relevance_post_ids, + 's' => 'search test', + 'orderby' => '', // Empty orderby with search defaults to relevance + 'order' => 'DESC', + 'posts_per_page' => 10, + 'paged' => 1, + ) + ); + + // Get second page with empty orderby + $query2 = new WP_Query( + array( + 'post_type' => 'wptests_time_ident', + 'post__in' => self::$search_relevance_post_ids, + 's' => 'search test', + 'orderby' => '', // Empty orderby with search defaults to relevance + 'order' => 'DESC', + 'posts_per_page' => 10, + 'paged' => 2, + ) + ); + + $page1_ids = wp_list_pluck( $query1->posts, 'ID' ); + $page2_ids = wp_list_pluck( $query2->posts, 'ID' ); + + // Verify no overlap between pages (no duplicates) + $overlap = array_intersect( $page1_ids, $page2_ids ); + $this->assertEmpty( $overlap, 'Pages should not contain duplicate posts when ordering by search relevance (empty orderby)' ); + + // Verify total count is correct + $this->assertEquals( 20, $query1->found_posts, 'Total posts should be 20' ); + $this->assertEquals( 10, count( $page1_ids ), 'First page should have 10 posts' ); + $this->assertEquals( 10, count( $page2_ids ), 'Second page should have 10 posts' ); + } }