From b98b347504a572a7685ad627f79df4e8e8903578 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sun, 16 Mar 2025 22:55:11 +0000 Subject: [PATCH] Query: Fix performance regression starting the loop for `all` fields. Fixes a performance regression starting the loop after calling `WP_Query( [ 'fields' => 'all' ] )`. This changes how `WP_Query::the_post()` determines whether there is a need to traverse the posts for cache warming. If IDs are queried, `WP_Query::$posts` is assumed to be an array of post IDs. If all fields are queried, `WP_Query::$posts` is assumed to be an array of fully populated post objects. Follow up to [59919], [59937]. Props joemcgill, peterwilsoncc, SirLouen. Fixes #56992. git-svn-id: https://develop.svn.wordpress.org/trunk@59993 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-query.php | 68 ++++++++++++++-------- tests/phpunit/tests/query/thePost.php | 84 ++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 27 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index c2844c591d..277ef0826c 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -2067,6 +2067,15 @@ class WP_Query { case 'id=>parent': $fields = "{$wpdb->posts}.ID, {$wpdb->posts}.post_parent"; break; + case '': + /* + * Set the default to 'all'. + * + * This is used in `WP_Query::the_post` to determine if the + * entire post object has been queried. + */ + $q['fields'] = 'all'; + // Falls through. default: $fields = "{$wpdb->posts}.*"; } @@ -3739,28 +3748,30 @@ class WP_Query { global $post; if ( ! $this->in_the_loop ) { - // Get post IDs to prime incomplete post objects. - $post_ids = array_reduce( - $this->posts, - function ( $carry, $post ) { - if ( is_numeric( $post ) && $post > 0 ) { - // Query for post ID. - $carry[] = $post; - } + if ( 'all' === $this->query_vars['fields'] ) { + // Full post objects queried. + $post_objects = $this->posts; + } else { + if ( 'ids' === $this->query_vars['fields'] ) { + // Post IDs queried. + $post_ids = $this->posts; + } else { + // Only partial objects queried, need to prime the cache for the loop. + $post_ids = array_reduce( + $this->posts, + function ( $carry, $post ) { + if ( isset( $post->ID ) ) { + $carry[] = $post->ID; + } - if ( is_object( $post ) && isset( $post->ID ) ) { - // Query for object, either WP_Post or stdClass. - $carry[] = $post->ID; - } - - return $carry; - }, - array() - ); - if ( $post_ids ) { + return $carry; + }, + array() + ); + } _prime_post_caches( $post_ids, $this->query_vars['update_post_term_cache'], $this->query_vars['update_post_meta_cache'] ); + $post_objects = array_map( 'get_post', $post_ids ); } - $post_objects = array_map( 'get_post', $post_ids ); update_post_author_caches( $post_objects ); } @@ -3781,12 +3792,19 @@ class WP_Query { $post = $this->next_post(); // Ensure a full post object is available. - if ( $post instanceof stdClass ) { - // stdClass indicates that a partial post object was queried. - $post = get_post( $post->ID ); - } elseif ( is_numeric( $post ) ) { - // Numeric indicates that only post IDs were queried. - $post = get_post( $post ); + if ( 'all' !== $this->query_vars['fields'] ) { + if ( 'ids' === $this->query_vars['fields'] ) { + // Post IDs queried. + $post = get_post( $post ); + } elseif ( isset( $post->ID ) ) { + /* + * Partial objecct queried. + * + * The post object was queried with a partial set of + * fields, populate the entire object for the loop. + */ + $post = get_post( $post->ID ); + } } // Set up the global post object for the loop. diff --git a/tests/phpunit/tests/query/thePost.php b/tests/phpunit/tests/query/thePost.php index e99a517872..6032a01dbe 100644 --- a/tests/phpunit/tests/query/thePost.php +++ b/tests/phpunit/tests/query/thePost.php @@ -48,6 +48,86 @@ class Tests_Query_ThePost extends WP_UnitTestCase { } } + /** + * Ensure custom 'fields' values are respected. + * + * @ticket 56992 + */ + public function test_wp_query_respects_custom_fields_values() { + global $wpdb; + add_filter( + 'posts_fields', + function ( $fields, $query ) { + global $wpdb; + + if ( $query->get( 'fields' ) === 'custom' ) { + $fields = "$wpdb->posts.ID,$wpdb->posts.post_author"; + } + + return $fields; + }, + 10, + 2 + ); + + $query = new WP_Query( + array( + 'fields' => 'custom', + 'post_type' => 'page', + 'post__in' => self::$page_child_ids, + ) + ); + + $this->assertNotEmpty( $query->posts, 'The query is expected to return results' ); + $this->assertSame( $query->get( 'fields' ), 'custom', 'The WP_Query class is expected to use the custom fields value' ); + $this->assertStringContainsString( "$wpdb->posts.ID,$wpdb->posts.post_author", $query->request, 'The database query is expected to use the custom fields value' ); + } + + /** + * Ensure custom 'fields' populates the global post in the loop. + * + * @ticket 56992 + */ + public function test_wp_query_with_custom_fields_value_populates_the_global_post() { + global $wpdb; + add_filter( + 'posts_fields', + function ( $fields, $query ) { + global $wpdb; + + if ( $query->get( 'fields' ) === 'custom' ) { + $fields = "$wpdb->posts.ID,$wpdb->posts.post_author"; + } + + return $fields; + }, + 10, + 2 + ); + + $query = new WP_Query( + array( + 'fields' => 'custom', + 'post_type' => 'page', + 'post__in' => self::$page_child_ids, + 'orderby' => 'id', + 'order' => 'ASC', + ) + ); + + $query->the_post(); + + // Get the global post and specific post. + $global_post = get_post(); + $specific_post = get_post( self::$page_child_ids[0], ARRAY_A ); + + $this->assertSameSetsWithIndex( $specific_post, $global_post->to_array(), 'The global post is expected to be fully populated.' ); + + $this->assertNotEmpty( get_the_title(), 'The title is expected to be populated.' ); + $this->assertNotEmpty( get_the_content(), 'The content is expected to be populated.' ); + $this->assertNotEmpty( get_the_excerpt(), 'The excerpt is expected to be populated.' ); + } + /** * Ensure that a secondary loop populates the global post completely regardless of the fields parameter. * @@ -75,11 +155,11 @@ class Tests_Query_ThePost extends WP_UnitTestCase { $global_post = get_post(); $specific_post = get_post( self::$page_child_ids[0], ARRAY_A ); + $this->assertSameSetsWithIndex( $specific_post, $global_post->to_array(), 'The global post is expected to be fully populated.' ); + $this->assertNotEmpty( get_the_title(), 'The title is expected to be populated.' ); $this->assertNotEmpty( get_the_content(), 'The content is expected to be populated.' ); $this->assertNotEmpty( get_the_excerpt(), 'The excerpt is expected to be populated.' ); - - $this->assertSameSetsWithIndex( $specific_post, $global_post->to_array(), 'The global post is expected to be fully populated.' ); } /**