Skip to content

Conversation

@ramonjd
Copy link
Member

@ramonjd ramonjd commented Dec 12, 2025

Follow up to #10394

Tests out an idea suggested in https://core.trac.wordpress.org/ticket/64390#comment:10 Props @azaozz

Note

This is mainly to discuss whether it's worth the change, if so good, if not, I can close.

Description

This PR is a follow-up to #10394 that makes the deterministic ordering changes more defensive and plugin-friendly. It ensures that when plugins modify the get_{$adjacent}_post_where or get_{$adjacent}_post_sort filters, the deterministic logic is not applied on top of their modifications.

Problem

In PR #10394, deterministic ID-based ordering was added to fix adjacent post navigation for posts with identical dates. However, the implementation applied the deterministic logic before the filters ran, which meant:

  1. Plugins modifying the WHERE or SORT clauses via filters would receive SQL that already included the deterministic changes
  2. Plugins couldn't opt out or modify the SQL in ways that might conflict with the deterministic logic
  3. This could break plugins that parse or manipulate the SQL clauses

Solution

This PR implements a defensive approach that:

  1. Applies filters first with the original (non-deterministic) SQL clauses
  2. Only applies deterministic logic if filters don't modify the clauses - checks if the filtered value equals the original prepared value
  3. Respects plugin modifications - when filters modify the SQL, their changes are preserved and deterministic logic is not applied

Implementation:

  • For WHERE clause: Only applies ID-based fallback if $where === $where_prepared (filter didn't modify it)
  • For SORT clause: Only applies ID-based sorting if $sort === $sort_prepared (filter didn't modify it)

Test Steps

Automated Tests

Run the PHPUnit tests:

npm run test:php -- --filter Tests_Link_GetAdjacentPost

Manual Testing

1. Verify Default Behavior (No Filters)

  1. Create test scenario:

    • Create multiple posts with identical post_date values (bulk publish drafts)
    • Navigate between posts
  2. Expected Results:

    • Navigation works deterministically, you should be able to navigate through all posts with identical dates

2. Verify Filter Modifications Are Respected

Test WHERE Filter:

  1. Add test filter to functions.php or a test plugin:
// Test filter for next post WHERE clause
add_filter( 'get_next_post_where', function( $where ) {
	// Modify the WHERE clause - deterministic fallback should NOT be applied.
	return $where . ' AND 1=1';
}, 10, 1 );

// Test filter for previous post WHERE clause
add_filter( 'get_previous_post_where', function( $where ) {
	// Modify the WHERE clause - deterministic fallback should NOT be applied.
	return $where . ' AND 1=1';
}, 10, 1 );
  1. Verify behavior:
    • Navigate between posts, rhe deterministic ID fallback should NOT be applied (no AND p.ID in WHERE clause)

Test SORT Filter:

  1. Remove WHERE filters, add SORT filter:
add_filter( 'get_next_post_sort', function( $sort, $post, $order ) {
	// Remove ID from sort - deterministic ID sort should NOT be applied.
	return "ORDER BY p.post_date $order LIMIT 1";
}, 10, 3 );

add_filter( 'get_previous_post_sort', function( $sort, $post, $order ) {
	return "ORDER BY p.post_date $order LIMIT 1";
}, 10, 3 );
  1. Verify behavior:
    • Create posts with identical dates
    • Navigate between posts, deterministic ID sort is NOT applied

3. Verify Unmodified Filters Still Get Deterministic Logic

  1. Add filters that don't modify the clauses:
add_filter( 'get_next_post_where', function( $where ) {
	// Return unchanged - deterministic fallback should be applied.
	return $where;
}, 10, 1 );

add_filter( 'get_next_post_sort', function( $sort ) {
	// Return unchanged - deterministic ID sort should be applied.
	return $sort;
}, 10, 1 );
  1. Verify behavior:
    • Navigation works deterministically, you should be able to navigate through all posts with identical dates

Trac ticket: https://core.trac.wordpress.org/ticket/64390

@github-actions
Copy link

github-actions bot commented Dec 12, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props ramonopoly, peterwilsoncc, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Copy link
Contributor

@peterwilsoncc peterwilsoncc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a few notes inline.

Comment on lines 661 to 662

remove_all_filters( 'get_next_post_where' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test suite does this automatically so it can be removed.

🔢 Applies in the tests below so I won't repeat myself.

Suggested change
remove_all_filters( 'get_next_post_where' );

// Next post should be the 4th post (higher ID, same date) - deterministic.
$next = get_adjacent_post( false, '', false );
$this->assertInstanceOf( 'WP_Post', $next );
$this->assertEquals( $post_ids[3], $next->ID );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$this->assertEquals( $post_ids[3], $next->ID );
$this->assertSame( $post_ids[3], $next->ID );

🔢 Looks to apply elsewhere so I won't repeat myself.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻


// Only force deterministic fallback if the where clause has not been modified by a filter.
if ( $where === $where_prepared ) {
$where = $where_prepared_with_deterministic_fallback;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason you're defining $where_prepared_with_deterministic_fallback above rather than here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to reduce my carbon footprint by limiting my Ctrl+V and Ctrl+P :trollface:

No reason at all actually. I can move.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I knew there was a better reason than that 😄

I wanted to use the original $where (with post_status conditions) without having to store it in a new var before the filter.

* @param string $order Sort order. 'DESC' for previous post, 'ASC' for next.
*/
$sort = apply_filters( "get_{$adjacent}_post_sort", "ORDER BY p.post_date $order, p.ID $order LIMIT 1", $post, $order );
$sort_prepared = "ORDER BY p.post_date $order LIMIT 1";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to be done above the docblock so docs-parsers can figure out the filter it applies to.

* @since 4.4.0 Added the `$taxonomy` and `$post` parameters.
* @since 6.9.0 Adds ID-based fallback for posts with identical dates in adjacent post queries.
*
* @param string $where The `WHERE` clause in the SQL.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yours truly missed this last time.

Suggested change
* @param string $where The `WHERE` clause in the SQL.
* @param string $where_prepared The `WHERE` clause in the SQL.

* Can either be next or previous post.
*
* @since 2.5.0
* @since 6.9.1 Adds deterministic fallback for sort clause if not modified by a filter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably include the since changes for 6.0.0 that yours truly also missed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are they? 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like this?

Suggested change
* @since 6.9.1 Adds deterministic fallback for sort clause if not modified by a filter.
* @since 6.9.0 Introduce deterministic fallback based in IDs to account for date collisions.
* @since 6.9.1 Remove deterministic fallback for sites modifying the WHERE clause via a filter.
* See https://core.trac.wordpress.org/ticket/64390

@ramonjd
Copy link
Member Author

ramonjd commented Dec 12, 2025

Thanks @peterwilsoncc I'll fix those things up next week.

// Prepare the where clause for the adjacent post query.
$where_prepared = $wpdb->prepare( "WHERE (p.post_date $comparison_operator %s OR (p.post_date = %s AND p.ID $comparison_operator %d)) AND p.post_type = %s $where", $current_post_date, $current_post_date, $post->ID, $post->post_type ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $comparison_operator is a string literal, either '<' or '>'.

$where_prepared_with_deterministic_fallback = $wpdb->prepare( "WHERE (p.post_date $comparison_operator %s OR (p.post_date = %s AND p.ID $comparison_operator %d)) AND p.post_type = %s $where", $current_post_date, $current_post_date, $post->ID, $post->post_type ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $comparison_operator is a string literal, either '<' or '>'.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is quite long. How about just $deterministic_where_prepared?

…ery hasn't been modified.

This update introduces a deterministic fallback for the SQL `WHERE` and `ORDER BY` clauses in the `get_adjacent_post` function when posts have identical dates. The fallback is applied only if the respective clauses have not been modified by filters, ensuring consistent behavior.

Unit tests have been added to verify the correct application of this fallback under various conditions, including scenarios where filters are applied or not.

See Trac ticket https://core.trac.wordpress.org/ticket/64390.
Unit tests have been updated according to feedback (using assertSame for simple comparisons), and removing filter removes.
@ramonjd ramonjd force-pushed the update/get_adjacent_post_honor-filtered-values branch from 199f7e2 to f001ac2 Compare December 19, 2025 04:21
@westonruter westonruter requested a review from Copilot January 20, 2026 16:11
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request enhances the adjacent post navigation feature (introduced in PR #10394) to be more defensive and plugin-friendly. It ensures that when plugins modify the get_{$adjacent}_post_where or get_{$adjacent}_post_sort filters, the deterministic ID-based ordering logic is not applied on top of their modifications.

Changes:

  • Modified filter application order to pass original (non-deterministic) SQL clauses to filters first
  • Added conditional logic to only apply deterministic WHERE/SORT clauses when filters don't modify the original clauses
  • Added comprehensive test coverage for the new conditional behavior
  • Updated documentation to reflect the changes in version 6.9.1

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 10 comments.

File Description
src/wp-includes/link-template.php Modified get_adjacent_post() to conditionally apply deterministic ID-based ordering only when filters don't modify WHERE/SORT clauses; updated documentation
tests/phpunit/tests/link/getAdjacentPost.php Added 5 new test methods to verify deterministic logic is applied/skipped based on filter modifications

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

*
* @since 2.5.0
* @since 6.9.0 Introduce deterministic fallback based in IDs to account for date collisions.
* @since 6.9.1 Remove deterministic fallback for sites modifying the WHERE clause via a filter. See #64390.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states "Remove deterministic fallback for sites modifying the WHERE clause" but the code only removes it when the WHERE filter is modified. The SORT clause is handled separately. Consider updating to "Remove deterministic fallback for sites modifying the WHERE or SORT clause via filters" to be more accurate about what this version changes.

Suggested change
* @since 6.9.1 Remove deterministic fallback for sites modifying the WHERE clause via a filter. See #64390.
* @since 6.9.1 Remove deterministic fallback for sites modifying the WHERE or SORT clause via filters. See #64390.

Copilot uses AI. Check for mistakes.
* @since 6.9.0 Adds ID-based fallback for posts with identical dates in adjacent post queries.
*
* @param string $where The `WHERE` clause in the SQL.
* @param string $where_prepared The `WHERE` clause in the SQL.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter name in the docblock was changed from "$where" to "$where_prepared", but this parameter represents the actual value passed to the filter (the prepared WHERE clause). However, this is a breaking change to the filter documentation. Plugins that read the documentation would expect the parameter to be named "$where" as it has been historically. Consider keeping the parameter name as "$where" in the docblock for consistency with the historical API, even though internally the variable is named "$where_prepared".

Suggested change
* @param string $where_prepared The `WHERE` clause in the SQL.
* @param string $where The `WHERE` clause in the SQL.

Copilot uses AI. Check for mistakes.
* Can either be next or previous post.
*
* @since 2.5.0
* @since 6.9.0 Introduce deterministic fallback based in IDs to account for date collisions.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in documentation: "based in IDs" should be "based on IDs".

Suggested change
* @since 6.9.0 Introduce deterministic fallback based in IDs to account for date collisions.
* @since 6.9.0 Introduce deterministic fallback based on IDs to account for date collisions.

Copilot uses AI. Check for mistakes.
@@ -2005,12 +2014,18 @@ function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previo
* @since 4.4.0 Added the `$post` parameter.
* @since 4.9.0 Added the `$order` parameter.
* @since 6.9.0 Adds ID sort to ensure deterministic ordering for posts with identical dates.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @SInCE 6.9.0 note in the docblock states "Adds ID sort to ensure deterministic ordering" which is now conditional on whether the filter modifies the clause. This documentation should be updated or removed since it no longer accurately describes the behavior in 6.9.1 where the ID sort is only applied if filters don't modify the SORT clause.

Suggested change
* @since 6.9.0 Adds ID sort to ensure deterministic ordering for posts with identical dates.
* @since 6.9.0 Introduced ID sort for posts with identical dates.

Copilot uses AI. Check for mistakes.
* @since 4.4.0 Added the `$post` parameter.
* @since 4.9.0 Added the `$order` parameter.
* @since 6.9.0 Adds ID sort to ensure deterministic ordering for posts with identical dates.
* @since 6.9.1 Remove deterministic fallback for sites modifying the SORT clause via a filter. See #64390.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @SInCE 6.9.1 note mentions "Remove deterministic fallback for sites modifying the SORT clause" but should be more specific. It should clarify that the deterministic sort is still applied by default, but NOT applied when a filter modifies the SORT clause. Consider rewording to: "Conditionally applies deterministic fallback - skips it when filters modify the SORT clause".

Suggested change
* @since 6.9.1 Remove deterministic fallback for sites modifying the SORT clause via a filter. See #64390.
* @since 6.9.1 Conditionally applies deterministic fallback - skips it when filters modify the SORT clause. See #64390.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants