From 76e09beb25f3b7190227f150225cabdd7c6bce4a Mon Sep 17 00:00:00 2001 From: Morgan Tocker Date: Mon, 8 Jun 2020 10:00:13 -0600 Subject: [PATCH 01/71] Remove usage of deprecated MySQL SQL_CALC_FOUND_ROWS Signed-off-by: Morgan Tocker --- src/wp-includes/class-wp-query.php | 20 ++++++++++++-------- src/wp-includes/class-wp-user-query.php | 13 ++++++------- tests/phpunit/tests/query/noFoundRows.php | 8 ++++---- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 5027f21cf0ade..81ca01c7f38c7 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -83,6 +83,14 @@ class WP_Query { */ public $request; + /** + * Get post database count query + * + * @since xxx + * @var string + */ + public $request_count; + /** * List of posts. * @@ -2910,13 +2918,9 @@ public function get_posts() { $orderby = 'ORDER BY ' . $orderby; } - $found_rows = ''; - if ( ! $q['no_found_rows'] && ! empty( $limits ) ) { - $found_rows = 'SQL_CALC_FOUND_ROWS'; - } - - $old_request = "SELECT $found_rows $distinct $fields FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits"; + $old_request = "SELECT $distinct $fields FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits"; $this->request = $old_request; + $this->request_count = "SELECT COUNT($distinct {$wpdb->posts}.ID) FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby"; if ( ! $q['suppress_filters'] ) { /** @@ -2999,7 +3003,7 @@ public function get_posts() { if ( $split_the_query ) { // First get the IDs and then fill in the objects. - $this->request = "SELECT $found_rows $distinct {$wpdb->posts}.ID FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits"; + $this->request = "SELECT $distinct {$wpdb->posts}.ID FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits"; /** * Filters the Post IDs SQL request before sending. @@ -3238,7 +3242,7 @@ private function set_found_posts( $q, $limits ) { * @param string $found_posts The query to run to find the found posts. * @param WP_Query $this The WP_Query instance (passed by reference). */ - $this->found_posts = $wpdb->get_var( apply_filters_ref_array( 'found_posts_query', array( 'SELECT FOUND_ROWS()', &$this ) ) ); + $this->found_posts = $wpdb->get_var( apply_filters_ref_array( 'found_posts_query', array( $this->request_count, &$this ) ) ); } else { if ( is_array( $this->posts ) ) { $this->found_posts = count( $this->posts ); diff --git a/src/wp-includes/class-wp-user-query.php b/src/wp-includes/class-wp-user-query.php index 02b0ff3814498..ce2eac02e271d 100644 --- a/src/wp-includes/class-wp-user-query.php +++ b/src/wp-includes/class-wp-user-query.php @@ -248,10 +248,6 @@ public function prepare_query( $query = array() ) { $this->query_fields = "$wpdb->users.ID"; } - if ( isset( $qv['count_total'] ) && $qv['count_total'] ) { - $this->query_fields = 'SQL_CALC_FOUND_ROWS ' . $this->query_fields; - } - $this->query_from = "FROM $wpdb->users"; $this->query_where = 'WHERE 1=1'; @@ -621,8 +617,12 @@ public function query() { } if ( isset( $qv['count_total'] ) && $qv['count_total'] ) { + /** - * Filters SELECT FOUND_ROWS() query for the current WP_User_Query instance. + * Return a total count of users. + * Historically this ran SELECT FOUND_ROWS() after issuing the main query, + * but since SQL_CALC_FOUND_ROWS is now deprecated from MySQL, it instead repeats + * the same query with COUNT(*) instead of $this->query_fields. * * @since 3.2.0 * @since 5.1.0 Added the `$this` parameter. @@ -632,8 +632,7 @@ public function query() { * @param string $sql The SELECT FOUND_ROWS() query for the current WP_User_Query. * @param WP_User_Query $this The current WP_User_Query instance. */ - $found_users_query = apply_filters( 'found_users_query', 'SELECT FOUND_ROWS()', $this ); - + $found_users_query = apply_filters( 'found_users_query', "SELECT COUNT(*) $this->query_from $this->query_where", $this ); $this->total_users = (int) $wpdb->get_var( $found_users_query ); } } diff --git a/tests/phpunit/tests/query/noFoundRows.php b/tests/phpunit/tests/query/noFoundRows.php index 7731194d49752..e39f767da743f 100644 --- a/tests/phpunit/tests/query/noFoundRows.php +++ b/tests/phpunit/tests/query/noFoundRows.php @@ -11,7 +11,7 @@ public function test_no_found_rows_default() { ) ); - $this->assertContains( 'SQL_CALC_FOUND_ROWS', $q->request ); + $this->assertNotContains( 'SQL_CALC_FOUND_ROWS', $q->request ); } public function test_no_found_rows_false() { @@ -22,7 +22,7 @@ public function test_no_found_rows_false() { ) ); - $this->assertContains( 'SQL_CALC_FOUND_ROWS', $q->request ); + $this->assertNotContains( 'SQL_CALC_FOUND_ROWS', $q->request ); } public function test_no_found_rows_0() { @@ -33,7 +33,7 @@ public function test_no_found_rows_0() { ) ); - $this->assertContains( 'SQL_CALC_FOUND_ROWS', $q->request ); + $this->assertNotContains( 'SQL_CALC_FOUND_ROWS', $q->request ); } public function test_no_found_rows_empty_string() { @@ -44,7 +44,7 @@ public function test_no_found_rows_empty_string() { ) ); - $this->assertContains( 'SQL_CALC_FOUND_ROWS', $q->request ); + $this->assertNotContains( 'SQL_CALC_FOUND_ROWS', $q->request ); } public function test_no_found_rows_true() { From a6648d895ff229a638c3251a762e4e0ea5193d0b Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 6 Jan 2022 22:25:11 +0100 Subject: [PATCH 02/71] Add ticket references. --- tests/phpunit/tests/query/noFoundRows.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/phpunit/tests/query/noFoundRows.php b/tests/phpunit/tests/query/noFoundRows.php index 6c89f041131b6..81467e523565c 100644 --- a/tests/phpunit/tests/query/noFoundRows.php +++ b/tests/phpunit/tests/query/noFoundRows.php @@ -4,6 +4,10 @@ * @group query */ class Tests_Query_NoFoundRows extends WP_UnitTestCase { + + /** + * @ticket 47280 + */ public function test_no_found_rows_default() { $q = new WP_Query( array( @@ -14,6 +18,9 @@ public function test_no_found_rows_default() { $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $q->request ); } + /** + * @ticket 47280 + */ public function test_no_found_rows_false() { $q = new WP_Query( array( @@ -25,6 +32,9 @@ public function test_no_found_rows_false() { $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $q->request ); } + /** + * @ticket 47280 + */ public function test_no_found_rows_0() { $q = new WP_Query( array( @@ -36,6 +46,9 @@ public function test_no_found_rows_0() { $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $q->request ); } + /** + * @ticket 47280 + */ public function test_no_found_rows_empty_string() { $q = new WP_Query( array( @@ -47,6 +60,9 @@ public function test_no_found_rows_empty_string() { $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $q->request ); } + /** + * @ticket 47280 + */ public function test_no_found_rows_true() { $q = new WP_Query( array( @@ -58,6 +74,9 @@ public function test_no_found_rows_true() { $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $q->request ); } + /** + * @ticket 47280 + */ public function test_no_found_rows_non_bool_cast_to_true() { $q = new WP_Query( array( @@ -71,6 +90,7 @@ public function test_no_found_rows_non_bool_cast_to_true() { /** * @ticket 29552 + * @ticket 47280 */ public function test_no_found_rows_default_with_nopaging_true() { $p = $this->factory->post->create(); @@ -88,6 +108,7 @@ public function test_no_found_rows_default_with_nopaging_true() { /** * @ticket 29552 + * @ticket 47280 */ public function test_no_found_rows_default_with_postsperpage_minus1() { $p = $this->factory->post->create(); From 1a16fc354c79a705ba0568111d0c9179bdaf9552 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 6 Jan 2022 22:25:39 +0100 Subject: [PATCH 03/71] Docs. --- src/wp-includes/class-wp-comment-query.php | 2 +- src/wp-includes/class-wp-network-query.php | 2 +- src/wp-includes/class-wp-query.php | 2 +- src/wp-includes/class-wp-site-query.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-comment-query.php b/src/wp-includes/class-wp-comment-query.php index 902bf25714792..17c8dcedea659 100644 --- a/src/wp-includes/class-wp-comment-query.php +++ b/src/wp-includes/class-wp-comment-query.php @@ -183,7 +183,7 @@ public function __call( $name, $arguments ) { * When used with `$offset`, `$offset` takes precedence. Default 1. * @type int $offset Number of comments to offset the query. Used to build * LIMIT clause. Default 0. - * @type bool $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query. + * @type bool $no_found_rows Whether to disable the query to count found rows. * Default: true. * @type string|array $orderby Comment status or array of statuses. To use 'meta_value' * or 'meta_value_num', `$meta_key` must also be defined. diff --git a/src/wp-includes/class-wp-network-query.php b/src/wp-includes/class-wp-network-query.php index 94d89db1874cb..0af75acdae7e9 100644 --- a/src/wp-includes/class-wp-network-query.php +++ b/src/wp-includes/class-wp-network-query.php @@ -98,7 +98,7 @@ class WP_Network_Query { * @type int $number Maximum number of networks to retrieve. Default empty (no limit). * @type int $offset Number of networks to offset the query. Used to build LIMIT clause. * Default 0. - * @type bool $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query. Default true. + * @type bool $no_found_rows Whether to disable the query to count found rows. Default true. * @type string|array $orderby Network status or array of statuses. Accepts 'id', 'domain', 'path', * 'domain_length', 'path_length' and 'network__in'. Also accepts false, * an empty array, or 'none' to disable `ORDER BY` clause. Default 'id'. diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 8cf92d327fd4b..4a0af29521d0b 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -1943,7 +1943,7 @@ public function get_posts() { $q['page'] = absint( $q['page'] ); } - // If true, forcibly turns off SQL_CALC_FOUND_ROWS even when limits are present. + // If true, forcibly turns off the query to count found rows even when limits are present. if ( isset( $q['no_found_rows'] ) ) { $q['no_found_rows'] = (bool) $q['no_found_rows']; } else { diff --git a/src/wp-includes/class-wp-site-query.php b/src/wp-includes/class-wp-site-query.php index 703c9580f1e26..f2908f8b88065 100644 --- a/src/wp-includes/class-wp-site-query.php +++ b/src/wp-includes/class-wp-site-query.php @@ -127,7 +127,7 @@ class WP_Site_Query { * @type int $number Maximum number of sites to retrieve. Default 100. * @type int $offset Number of sites to offset the query. Used to build LIMIT clause. * Default 0. - * @type bool $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query. Default true. + * @type bool $no_found_rows Whether to disable the query to count found rows. Default true. * @type string|array $orderby Site status or array of statuses. Accepts: * - 'id' * - 'domain' From 43a8da524185e98638e0ef6c7bfea1514bb29906 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 6 Jan 2022 22:25:44 +0100 Subject: [PATCH 04/71] Todos. --- src/wp-includes/class-wp-comment-query.php | 2 +- src/wp-includes/class-wp-network-query.php | 2 +- src/wp-includes/class-wp-site-query.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-comment-query.php b/src/wp-includes/class-wp-comment-query.php index 17c8dcedea659..fa370acf734f2 100644 --- a/src/wp-includes/class-wp-comment-query.php +++ b/src/wp-includes/class-wp-comment-query.php @@ -951,7 +951,7 @@ protected function get_comment_ids() { $found_rows = ''; if ( ! $this->query_vars['no_found_rows'] ) { - $found_rows = 'SQL_CALC_FOUND_ROWS'; + $found_rows = 'SQL_CALC_FOUND_ROWS'; // @TODO } $this->sql_clauses['select'] = "SELECT $found_rows $fields"; diff --git a/src/wp-includes/class-wp-network-query.php b/src/wp-includes/class-wp-network-query.php index 0af75acdae7e9..eea0e870f483b 100644 --- a/src/wp-includes/class-wp-network-query.php +++ b/src/wp-includes/class-wp-network-query.php @@ -471,7 +471,7 @@ protected function get_network_ids() { $found_rows = ''; if ( ! $this->query_vars['no_found_rows'] ) { - $found_rows = 'SQL_CALC_FOUND_ROWS'; + $found_rows = 'SQL_CALC_FOUND_ROWS'; // @TODO } $this->sql_clauses['select'] = "SELECT $found_rows $fields"; diff --git a/src/wp-includes/class-wp-site-query.php b/src/wp-includes/class-wp-site-query.php index f2908f8b88065..25f155740af0e 100644 --- a/src/wp-includes/class-wp-site-query.php +++ b/src/wp-includes/class-wp-site-query.php @@ -674,7 +674,7 @@ protected function get_site_ids() { $found_rows = ''; if ( ! $this->query_vars['no_found_rows'] ) { - $found_rows = 'SQL_CALC_FOUND_ROWS'; + $found_rows = 'SQL_CALC_FOUND_ROWS'; // @TODO } $this->sql_clauses['select'] = "SELECT $found_rows $fields"; From 0b98da89be9b722399e3fdbe53992349b5252fe9 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 6 Jan 2022 22:52:26 +0100 Subject: [PATCH 05/71] More docs. --- src/wp-includes/class-wp-query.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 4a0af29521d0b..6a78ab21227e8 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3012,6 +3012,7 @@ public function get_posts() { * Filters the completed SQL query before sending. * * @since 2.0.0 + * @since x.x.x This query now no longer contains a `SQL_CALC_FOUND_ROWS` modifier. * * @param string $request The complete SQL query. * @param WP_Query $query The WP_Query instance (passed by reference). @@ -3096,6 +3097,7 @@ public function get_posts() { * Filters the Post IDs SQL request before sending. * * @since 3.4.0 + * @since x.x.x This query now no longer contains a `SQL_CALC_FOUND_ROWS` modifier. * * @param string $request The post ID request. * @param WP_Query $query The WP_Query instance. @@ -3332,6 +3334,7 @@ private function set_found_posts( $q, $limits ) { * Filters the query to run for retrieving the found posts. * * @since 2.1.0 + * @since x.x.x This query was changed... * * @param string $found_posts_query The query to run to find the found posts. * @param WP_Query $query The WP_Query instance (passed by reference). From d62ae5f350f372662b12d0f63e1ed8e1ad68cd13 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 6 Jan 2022 23:20:36 +0100 Subject: [PATCH 06/71] First pass at lazy loading the `found_posts` and `max_num_pages` property values. --- src/wp-includes/class-wp-query.php | 48 +++++++++++++++++++++++------- tests/phpunit/tests/post/query.php | 48 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 6a78ab21227e8..23a5654232f08 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -172,17 +172,21 @@ class WP_Query { * If limit clause was not used, equals $post_count. * * @since 2.1.0 + * @since x.x.x This value is now lazily loaded only when it's needed. + * * @var int */ - public $found_posts = 0; + private $found_posts = 0; /** * The number of pages. * * @since 2.1.0 + * @since x.x.x This value is now lazily loaded only when it's needed. + * * @var int */ - public $max_num_pages = 0; + private $max_num_pages = 0; /** * The number of comment pages. @@ -460,6 +464,14 @@ class WP_Query { */ private $stopwords; + /** + * Undocumented variable + * + * @since x.x.x + * @var string + */ + private $limits = ''; + private $compat_fields = array( 'query_vars_hash', 'query_vars_changed' ); private $compat_methods = array( 'init_query_flags', 'parse_tax_query' ); @@ -3046,7 +3058,7 @@ public function get_posts() { /** @var int[] */ $this->posts = array_map( 'intval', $this->posts ); $this->post_count = count( $this->posts ); - $this->set_found_posts( $q, $limits ); + $this->limits = $limits; return $this->posts; } @@ -3057,7 +3069,7 @@ public function get_posts() { } $this->post_count = count( $this->posts ); - $this->set_found_posts( $q, $limits ); + $this->limits = $limits; /** @var int[] */ $r = array(); @@ -3108,14 +3120,14 @@ public function get_posts() { if ( $ids ) { $this->posts = $ids; - $this->set_found_posts( $q, $limits ); + $this->limits = $limits; _prime_post_caches( $ids, $q['update_post_term_cache'], $q['update_post_meta_cache'] ); } else { $this->posts = array(); } } else { $this->posts = $wpdb->get_results( $this->request ); - $this->set_found_posts( $q, $limits ); + $this->limits = $limits; } } @@ -3314,15 +3326,24 @@ public function get_posts() { * for the current query. * * @since 3.5.0 + * @since x.x.x The `$q` and `$limits` parameters were made optional. * * @global wpdb $wpdb WordPress database abstraction object. * - * @param array $q Query variables. - * @param string $limits LIMIT clauses of the query. + * @param array $q Optional. Query variables. + * @param string $limits Optional. LIMIT clauses of the query. */ - private function set_found_posts( $q, $limits ) { + private function set_found_posts( $q = null, $limits = null ) { global $wpdb; + if ( null === $q ) { + $q = $this->query_vars; + } + + if ( null === $limits ) { + $limits = $this->limits; + } + // Bail if posts is an empty array. Continue if posts is an empty string, // null, or false to accommodate caching plugins that fill posts later. if ( $q['no_found_rows'] || ( is_array( $this->posts ) && ! $this->posts ) ) { @@ -3358,6 +3379,7 @@ private function set_found_posts( $q, $limits ) { * Filters the number of found posts for the query. * * @since 2.1.0 + * @since x.x.x This filter now only runs after the value is lazily loaded... * * @param int $found_posts The number of posts found. * @param WP_Query $query The WP_Query instance (passed by reference). @@ -3662,14 +3684,20 @@ public function __construct( $query = '' ) { } /** - * Make private properties readable for backward compatibility. + * Make private properties readable for backward compatibility and lazy loading. * * @since 4.0.0 + * @since x.x.x The `found_posts` and `max_num_pages` properties are now lazily loaded. * * @param string $name Property to get. * @return mixed Property. */ public function __get( $name ) { + if ( 'found_posts' === $name || 'max_num_pages' === $name ) { + $this->set_found_posts(); + return $this->$name; + } + if ( in_array( $name, $this->compat_fields, true ) ) { return $this->$name; } diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index e7def81a0c822..06119a1a13118 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -758,4 +758,52 @@ public function test_found_posts_should_be_integer_even_if_found_posts_filter_re $this->assertIsInt( $q->found_posts ); } + + public function test_found_posts_query_should_be_lazily_loaded() { + global $wpdb; + + self::factory()->post->create_many( 5 ); + + $start = $wpdb->num_queries; + + $q = new WP_Query( + array( + 'posts_per_page' => 2, + ) + ); + + // Count the queries + $before_found_posts = ( $wpdb->num_queries - $start ); + + // Fetch found posts + $found_posts = $q->found_posts; + $max_num_pages = $q->max_num_pages; + + // Count the queries after fetching found posts + $after_found_posts = ( $wpdb->num_queries - $start ); + + // Repeat + $found_posts = $q->found_posts; + $max_num_pages = $q->max_num_pages; + + $after_found_posts_again_1 = ( $wpdb->num_queries - $start ); + + // Repeat again + $found_posts = $q->found_posts; + $max_num_pages = $q->max_num_pages; + + $after_found_posts_again_2 = ( $wpdb->num_queries - $start ); + + // Ensure the posts were not initially counted + $this->assertSame( 1, $before_found_posts ); + + // Ensure the counts are correct + $this->assertSame( 5, $found_posts ); + $this->assertEquals( 3, $max_num_pages ); + + // Ensure subsequent counts only trigger one query + $this->assertSame( 2, $after_found_posts ); + $this->assertSame( 2, $after_found_posts_again_1 ); + $this->assertSame( 2, $after_found_posts_again_2 ); + } } From 69a5dd8967c3254ac69bca5bfc8be419d3ab91ca Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 6 Jan 2022 23:26:37 +0100 Subject: [PATCH 07/71] Ensure repeated calls to the `found_posts` or `max_num_pages` properties don't trigger additional queries. --- src/wp-includes/class-wp-query.php | 9 +++++++-- tests/phpunit/tests/post/query.php | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 23a5654232f08..2719f43ab1330 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -176,7 +176,7 @@ class WP_Query { * * @var int */ - private $found_posts = 0; + private $found_posts = null; /** * The number of pages. @@ -534,7 +534,7 @@ public function init() { unset( $this->comment ); $this->comment_count = 0; $this->current_comment = -1; - $this->found_posts = 0; + $this->found_posts = null; $this->max_num_pages = 0; $this->max_num_comment_pages = 0; @@ -3336,6 +3336,10 @@ public function get_posts() { private function set_found_posts( $q = null, $limits = null ) { global $wpdb; + if ( null !== $this->found_posts ) { + return; + } + if ( null === $q ) { $q = $this->query_vars; } @@ -3347,6 +3351,7 @@ private function set_found_posts( $q = null, $limits = null ) { // Bail if posts is an empty array. Continue if posts is an empty string, // null, or false to accommodate caching plugins that fill posts later. if ( $q['no_found_rows'] || ( is_array( $this->posts ) && ! $this->posts ) ) { + $this->found_posts = 0; return; } diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 06119a1a13118..7f32b81ef19a2 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -802,8 +802,8 @@ public function test_found_posts_query_should_be_lazily_loaded() { $this->assertEquals( 3, $max_num_pages ); // Ensure subsequent counts only trigger one query - $this->assertSame( 2, $after_found_posts ); - $this->assertSame( 2, $after_found_posts_again_1 ); - $this->assertSame( 2, $after_found_posts_again_2 ); + $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts ); + $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again_1 ); + $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again_2 ); } } From c25537db3ecad48cf0e38c64e93ec32129d9e85d Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 6 Jan 2022 23:31:32 +0100 Subject: [PATCH 08/71] Extra queries are generated when term and meta caches are updated. --- tests/phpunit/tests/post/query.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 7f32b81ef19a2..fc520577d8688 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -769,6 +769,8 @@ public function test_found_posts_query_should_be_lazily_loaded() { $q = new WP_Query( array( 'posts_per_page' => 2, + 'update_post_term_cache' => false, + 'update_post_meta_cache' => false, ) ); From acc3f1f1b44f002b994bce24d09d10de4d149487 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 7 Jan 2022 00:10:58 +0100 Subject: [PATCH 09/71] Add some extra bytes to keep the computer happy. --- tests/phpunit/tests/post/query.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index fc520577d8688..8ccd86ab871d5 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -768,7 +768,7 @@ public function test_found_posts_query_should_be_lazily_loaded() { $q = new WP_Query( array( - 'posts_per_page' => 2, + 'posts_per_page' => 2, 'update_post_term_cache' => false, 'update_post_meta_cache' => false, ) @@ -778,20 +778,20 @@ public function test_found_posts_query_should_be_lazily_loaded() { $before_found_posts = ( $wpdb->num_queries - $start ); // Fetch found posts - $found_posts = $q->found_posts; + $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; // Count the queries after fetching found posts $after_found_posts = ( $wpdb->num_queries - $start ); // Repeat - $found_posts = $q->found_posts; + $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; $after_found_posts_again_1 = ( $wpdb->num_queries - $start ); // Repeat again - $found_posts = $q->found_posts; + $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; $after_found_posts_again_2 = ( $wpdb->num_queries - $start ); From ea95d463243fcf088dca8fb27439b0357c992bd8 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 10 Jan 2022 23:26:00 +0100 Subject: [PATCH 10/71] No need to repeat this twice. --- tests/phpunit/tests/post/query.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 8ccd86ab871d5..405e2677f8ad0 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -788,13 +788,7 @@ public function test_found_posts_query_should_be_lazily_loaded() { $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; - $after_found_posts_again_1 = ( $wpdb->num_queries - $start ); - - // Repeat again - $found_posts = $q->found_posts; - $max_num_pages = $q->max_num_pages; - - $after_found_posts_again_2 = ( $wpdb->num_queries - $start ); + $after_found_posts_again = ( $wpdb->num_queries - $start ); // Ensure the posts were not initially counted $this->assertSame( 1, $before_found_posts ); @@ -805,7 +799,6 @@ public function test_found_posts_query_should_be_lazily_loaded() { // Ensure subsequent counts only trigger one query $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts ); - $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again_1 ); - $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again_2 ); + $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again ); } } From 87a27558e4a147c451858bbc232b02e184cac472 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 10 Jan 2022 23:34:00 +0100 Subject: [PATCH 11/71] Move all this into a data provider. --- tests/phpunit/tests/post/query.php | 46 ++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 405e2677f8ad0..24ef2adc423a1 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -759,19 +759,30 @@ public function test_found_posts_should_be_integer_even_if_found_posts_filter_re $this->assertIsInt( $q->found_posts ); } - public function test_found_posts_query_should_be_lazily_loaded() { + /** + * @ticket 47280 + * @dataProvider data_found_posts_queries + * + * @param array $args + * @param int $expected_posts + * @param int $expected_pages + * @param callable $factory + */ + public function test_found_posts_query_should_be_lazily_loaded( array $args, $expected_posts, $expected_pages, $factory ) { global $wpdb; - self::factory()->post->create_many( 5 ); + call_user_func( $factory, self::factory() ); $start = $wpdb->num_queries; $q = new WP_Query( - array( - 'posts_per_page' => 2, - 'update_post_term_cache' => false, - 'update_post_meta_cache' => false, - ) + array_merge( + $args, + array( + 'update_post_term_cache' => false, + 'update_post_meta_cache' => false, + ), + ), ); // Count the queries @@ -794,11 +805,28 @@ public function test_found_posts_query_should_be_lazily_loaded() { $this->assertSame( 1, $before_found_posts ); // Ensure the counts are correct - $this->assertSame( 5, $found_posts ); - $this->assertEquals( 3, $max_num_pages ); + $this->assertSame( $expected_posts, $found_posts ); + $this->assertEquals( $expected_pages, $max_num_pages ); // Ensure subsequent counts only trigger one query $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts ); $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again ); } + + public function data_found_posts_queries() { + return array( + + array( + 'args' => array( + 'posts_per_page' => 2, + ), + 'expected_posts' => 5, + 'expected_pages' => 3, + 'factory' => function( WP_UnitTest_Factory $factory ) { + $factory->post->create_many( 5 ); + } + ), + + ); + } } From 1ec9bf755b2897ac84d355533210b08991d71797 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 10 Jan 2022 23:41:26 +0100 Subject: [PATCH 12/71] Docs. --- tests/phpunit/tests/post/query.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 24ef2adc423a1..bab0d449d2065 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -768,7 +768,7 @@ public function test_found_posts_should_be_integer_even_if_found_posts_filter_re * @param int $expected_pages * @param callable $factory */ - public function test_found_posts_query_should_be_lazily_loaded( array $args, $expected_posts, $expected_pages, $factory ) { + public function test_found_posts_should_be_lazily_loaded( array $args, $expected_posts, $expected_pages, $factory ) { global $wpdb; call_user_func( $factory, self::factory() ); @@ -799,6 +799,7 @@ public function test_found_posts_query_should_be_lazily_loaded( array $args, $ex $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; + // Count the queries after fetching found posts a second time $after_found_posts_again = ( $wpdb->num_queries - $start ); // Ensure the posts were not initially counted @@ -808,7 +809,7 @@ public function test_found_posts_query_should_be_lazily_loaded( array $args, $ex $this->assertSame( $expected_posts, $found_posts ); $this->assertEquals( $expected_pages, $max_num_pages ); - // Ensure subsequent counts only trigger one query + // Ensure subsequent counts only triggered one query $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts ); $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again ); } @@ -816,7 +817,7 @@ public function test_found_posts_query_should_be_lazily_loaded( array $args, $ex public function data_found_posts_queries() { return array( - array( + 'basic query' => array( 'args' => array( 'posts_per_page' => 2, ), From 9dfbc3711dc7b20a681ac1eaff4f472ea1b5d382 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 11 Jan 2022 11:44:17 +0100 Subject: [PATCH 13/71] Add tests that ensure the newly private properties are publicly accessible. --- tests/phpunit/tests/post/query.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index bab0d449d2065..9dcd9d9881649 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -759,6 +759,36 @@ public function test_found_posts_should_be_integer_even_if_found_posts_filter_re $this->assertIsInt( $q->found_posts ); } + /** + * @ticket 47280 + */ + public function test_found_posts_property_is_present() { + $q = new WP_Query( + array( + 'post_type' => 'post', + ) + ); + + $this->assertTrue( property_exists( $q, 'found_posts' ) ); + $this->assertObjectHasAttribute( 'found_posts', $q ); + $this->assertNotNull( $q->found_posts ); + } + + /** + * @ticket 47280 + */ + public function test_max_num_pages_property_is_present() { + $q = new WP_Query( + array( + 'post_type' => 'post', + ) + ); + + $this->assertTrue( property_exists( $q, 'max_num_pages' ) ); + $this->assertObjectHasAttribute( 'max_num_pages', $q ); + $this->assertNotNull( $q->max_num_pages ); + } + /** * @ticket 47280 * @dataProvider data_found_posts_queries From 74c117efea42be720119ab59f302bb44cf6c94a0 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 11 Jan 2022 12:49:26 +0100 Subject: [PATCH 14/71] This is a private method so its signature can be safely changed. --- src/wp-includes/class-wp-query.php | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 2719f43ab1330..2703140028aaf 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3326,27 +3326,19 @@ public function get_posts() { * for the current query. * * @since 3.5.0 - * @since x.x.x The `$q` and `$limits` parameters were made optional. + * @since x.x.x The `$q` and `$limits` parameters were removed. * * @global wpdb $wpdb WordPress database abstraction object. - * - * @param array $q Optional. Query variables. - * @param string $limits Optional. LIMIT clauses of the query. */ - private function set_found_posts( $q = null, $limits = null ) { + private function set_found_posts() { global $wpdb; if ( null !== $this->found_posts ) { return; } - if ( null === $q ) { - $q = $this->query_vars; - } - - if ( null === $limits ) { - $limits = $this->limits; - } + $q = $this->query_vars; + $limits = $this->limits; // Bail if posts is an empty array. Continue if posts is an empty string, // null, or false to accommodate caching plugins that fill posts later. From 9a8be540346256659c9ab17a1b611e6b94122d62 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 11 Jan 2022 22:25:29 +0100 Subject: [PATCH 15/71] Whoops. --- tests/phpunit/tests/post/query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 9dcd9d9881649..a9d142e307a69 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -812,7 +812,7 @@ public function test_found_posts_should_be_lazily_loaded( array $args, $expected 'update_post_term_cache' => false, 'update_post_meta_cache' => false, ), - ), + ) ); // Count the queries From 48ebdd5f763f2d5893a48a8ab755df805d35fc37 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 11 Jan 2022 22:30:58 +0100 Subject: [PATCH 16/71] Keep the coding standards bot happy. --- tests/phpunit/tests/post/query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index a9d142e307a69..f90461a302402 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -855,7 +855,7 @@ public function data_found_posts_queries() { 'expected_pages' => 3, 'factory' => function( WP_UnitTest_Factory $factory ) { $factory->post->create_many( 5 ); - } + }, ), ); From d111d93a9d28a08602d360553fd62659913d6dba Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 11 Jan 2022 22:32:29 +0100 Subject: [PATCH 17/71] Another whoops. --- tests/phpunit/tests/post/query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index f90461a302402..66a57ddf5d655 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -811,7 +811,7 @@ public function test_found_posts_should_be_lazily_loaded( array $args, $expected array( 'update_post_term_cache' => false, 'update_post_meta_cache' => false, - ), + ) ) ); From 7bc9130c2c76ac63f318a0080b93293854be41ba Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 11 Jan 2022 23:14:04 +0100 Subject: [PATCH 18/71] Introduce a dedicated and simpler tests for testing the accuracy of post counts. --- tests/phpunit/tests/post/query.php | 49 ++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 66a57ddf5d655..c3a3ebce1b1d8 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -791,27 +791,19 @@ public function test_max_num_pages_property_is_present() { /** * @ticket 47280 - * @dataProvider data_found_posts_queries - * - * @param array $args - * @param int $expected_posts - * @param int $expected_pages - * @param callable $factory */ - public function test_found_posts_should_be_lazily_loaded( array $args, $expected_posts, $expected_pages, $factory ) { + public function test_found_posts_should_be_lazily_loaded() { global $wpdb; - call_user_func( $factory, self::factory() ); + self::factory()->post->create_many( 5 ); $start = $wpdb->num_queries; $q = new WP_Query( - array_merge( - $args, - array( - 'update_post_term_cache' => false, - 'update_post_meta_cache' => false, - ) + array( + 'posts_per_page' => 2, + 'update_post_term_cache' => false, + 'update_post_meta_cache' => false, ) ); @@ -836,14 +828,39 @@ public function test_found_posts_should_be_lazily_loaded( array $args, $expected $this->assertSame( 1, $before_found_posts ); // Ensure the counts are correct - $this->assertSame( $expected_posts, $found_posts ); - $this->assertEquals( $expected_pages, $max_num_pages ); + $this->assertSame( 5, $found_posts ); + $this->assertEquals( 3, $max_num_pages ); // Ensure subsequent counts only triggered one query $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts ); $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again ); } + /** + * @ticket 47280 + * @dataProvider data_found_posts_queries + * + * @param array $args + * @param int $expected_posts + * @param int $expected_pages + * @param callable $factory + */ + public function test_found_posts_are_correct( array $args, $expected_posts, $expected_pages, $factory ) { + call_user_func( $factory, self::factory() ); + + $q = new WP_Query( $args ); + + $message = sprintf( + "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", + $q->request, + $q->request_count + ); + + // Ensure the counts are correct + $this->assertSame( $expected_posts, $q->found_posts, $message ); + $this->assertEquals( $expected_pages, $q->max_num_pages, $message ); + } + public function data_found_posts_queries() { return array( From ef7249991a02412a7f3aa270ff97403001e29dc4 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 11 Jan 2022 23:20:39 +0100 Subject: [PATCH 19/71] Ensure the count properties can be read by `isset()`. --- src/wp-includes/class-wp-query.php | 6 +++++- tests/phpunit/tests/post/query.php | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 2703140028aaf..7c2a8bd100ddd 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3701,7 +3701,7 @@ public function __get( $name ) { } /** - * Make private properties checkable for backward compatibility. + * Make private properties checkable for backward compatibility and lazy loading. * * @since 4.0.0 * @@ -3709,6 +3709,10 @@ public function __get( $name ) { * @return bool Whether the property is set. */ public function __isset( $name ) { + if ( 'found_posts' === $name || 'max_num_pages' === $name ) { + return true; + } + if ( in_array( $name, $this->compat_fields, true ) ) { return isset( $this->$name ); } diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index c3a3ebce1b1d8..0e72371bb3856 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -769,6 +769,7 @@ public function test_found_posts_property_is_present() { ) ); + $this->assertTrue( isset( $q->found_posts ) ); $this->assertTrue( property_exists( $q, 'found_posts' ) ); $this->assertObjectHasAttribute( 'found_posts', $q ); $this->assertNotNull( $q->found_posts ); @@ -784,6 +785,7 @@ public function test_max_num_pages_property_is_present() { ) ); + $this->assertTrue( isset( $q->max_num_pages ) ); $this->assertTrue( property_exists( $q, 'max_num_pages' ) ); $this->assertObjectHasAttribute( 'max_num_pages', $q ); $this->assertNotNull( $q->max_num_pages ); From 6ebca810228f6f5fcce20b238b2879177f7a5b75 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 00:10:15 +0100 Subject: [PATCH 20/71] More tests, remove the data provider. --- tests/phpunit/tests/post/query.php | 83 +++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 0e72371bb3856..181d9bc5cf744 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -840,17 +840,15 @@ public function test_found_posts_should_be_lazily_loaded() { /** * @ticket 47280 - * @dataProvider data_found_posts_queries - * - * @param array $args - * @param int $expected_posts - * @param int $expected_pages - * @param callable $factory */ - public function test_found_posts_are_correct( array $args, $expected_posts, $expected_pages, $factory ) { - call_user_func( $factory, self::factory() ); + public function test_found_posts_are_correct_for_basic_query() { + self::factory()->post->create_many( 5 ); - $q = new WP_Query( $args ); + $q = new WP_Query( + array( + 'posts_per_page' => 2, + ) + ); $message = sprintf( "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", @@ -858,25 +856,64 @@ public function test_found_posts_are_correct( array $args, $expected_posts, $exp $q->request_count ); - // Ensure the counts are correct - $this->assertSame( $expected_posts, $q->found_posts, $message ); - $this->assertEquals( $expected_pages, $q->max_num_pages, $message ); + $this->assertSame( 5, $q->found_posts, $message ); + $this->assertEquals( 3, $q->max_num_pages, $message ); } - public function data_found_posts_queries() { - return array( + /** + * @ticket 47280 + */ + public function test_found_posts_are_correct_for_author_queries() { + $author = self::factory()->user->create(); + self::factory()->post->create_many( 5 ); + self::factory()->post->create_many( 5, array( + 'post_author' => $author, + ) ); - 'basic query' => array( - 'args' => array( - 'posts_per_page' => 2, - ), - 'expected_posts' => 5, - 'expected_pages' => 3, - 'factory' => function( WP_UnitTest_Factory $factory ) { - $factory->post->create_many( 5 ); - }, + $q = new WP_Query( + array( + 'posts_per_page' => 2, + 'author' => $author, + ) + ); + + $message = sprintf( + "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", + $q->request, + $q->request_count + ); + + $this->assertSame( 5, $q->found_posts, $message ); + $this->assertEquals( 3, $q->max_num_pages, $message ); + } + + /** + * @ticket 47280 + */ + public function test_found_posts_are_correct_for_term_queries() { + $term = self::factory()->term->create_and_get(); + self::factory()->post->create_many( 5 ); + self::factory()->post->create_many( 5, array( + 'tax_input' => array( + 'post_tag' => array( $term->slug ), ), + ) ); + + $q = new WP_Query( + array( + 'posts_per_page' => 2, + 'tag' => $term->slug, + ) + ); + $message = sprintf( + "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", + $q->request, + $q->request_count ); + + $this->assertSame( 5, $q->found_posts, $message ); + $this->assertEquals( 3, $q->max_num_pages, $message ); } + } From c5d974ce0913dbaaf602449788241359c857c741 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 11:09:22 +0100 Subject: [PATCH 21/71] More assertions. --- tests/phpunit/tests/post/query.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 181d9bc5cf744..fbb7af3448216 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -813,6 +813,7 @@ public function test_found_posts_should_be_lazily_loaded() { $before_found_posts = ( $wpdb->num_queries - $start ); // Fetch found posts + $post_count = $q->post_count; $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; @@ -830,6 +831,7 @@ public function test_found_posts_should_be_lazily_loaded() { $this->assertSame( 1, $before_found_posts ); // Ensure the counts are correct + $this->assertSame( 2, $post_count ); $this->assertSame( 5, $found_posts ); $this->assertEquals( 3, $max_num_pages ); @@ -856,6 +858,7 @@ public function test_found_posts_are_correct_for_basic_query() { $q->request_count ); + $this->assertSame( 2, $q->post_count, $message ); $this->assertSame( 5, $q->found_posts, $message ); $this->assertEquals( 3, $q->max_num_pages, $message ); } @@ -883,6 +886,7 @@ public function test_found_posts_are_correct_for_author_queries() { $q->request_count ); + $this->assertSame( 2, $q->post_count, $message ); $this->assertSame( 5, $q->found_posts, $message ); $this->assertEquals( 3, $q->max_num_pages, $message ); } @@ -912,6 +916,7 @@ public function test_found_posts_are_correct_for_term_queries() { $q->request_count ); + $this->assertSame( 2, $q->post_count, $message ); $this->assertSame( 5, $q->found_posts, $message ); $this->assertEquals( 3, $q->max_num_pages, $message ); } From c95af1fbf6c8880c5dc2bf47fa0a94da6e858a6b Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 11:29:36 +0100 Subject: [PATCH 22/71] Correct the post counts for term queries. --- src/wp-includes/class-wp-query.php | 2 +- tests/phpunit/tests/post/query.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 7c2a8bd100ddd..0ce9932846213 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3017,7 +3017,7 @@ public function get_posts() { $old_request = "SELECT $distinct $fields FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits"; $this->request = $old_request; - $this->request_count = "SELECT COUNT($distinct {$wpdb->posts}.ID) FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby"; + $this->request_count = "SELECT COUNT($distinct {$wpdb->posts}.ID) FROM {$wpdb->posts} $join WHERE 1=1 $where"; if ( ! $q['suppress_filters'] ) { /** diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index fbb7af3448216..119685a12bf5e 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -897,11 +897,11 @@ public function test_found_posts_are_correct_for_author_queries() { public function test_found_posts_are_correct_for_term_queries() { $term = self::factory()->term->create_and_get(); self::factory()->post->create_many( 5 ); - self::factory()->post->create_many( 5, array( - 'tax_input' => array( - 'post_tag' => array( $term->slug ), - ), - ) ); + $ids = self::factory()->post->create_many( 5 ); + + foreach ( $ids as $id ) { + wp_set_post_terms( $id, $term->slug ); + } $q = new WP_Query( array( From 5a89980e9d2d3a2238d321217ba18a91817c32a9 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 11:32:04 +0100 Subject: [PATCH 23/71] Test paged queries. --- tests/phpunit/tests/post/query.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 119685a12bf5e..bf75879e96997 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -863,6 +863,30 @@ public function test_found_posts_are_correct_for_basic_query() { $this->assertEquals( 3, $q->max_num_pages, $message ); } + /** + * @ticket 47280 + */ + public function test_found_posts_are_correct_for_paged_query() { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'posts_per_page' => 2, + 'paged' => 3, + ) + ); + + $message = sprintf( + "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", + $q->request, + $q->request_count + ); + + $this->assertSame( 1, $q->post_count, $message ); + $this->assertSame( 5, $q->found_posts, $message ); + $this->assertEquals( 3, $q->max_num_pages, $message ); + } + /** * @ticket 47280 */ From dd03845fd0c989c183546e50a2166765ecc6baf3 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 11:33:39 +0100 Subject: [PATCH 24/71] Test meta queries. --- tests/phpunit/tests/post/query.php | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index bf75879e96997..8d452c61e40da 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -918,7 +918,7 @@ public function test_found_posts_are_correct_for_author_queries() { /** * @ticket 47280 */ - public function test_found_posts_are_correct_for_term_queries() { + public function test_found_posts_are_correct_for_tax_queries() { $term = self::factory()->term->create_and_get(); self::factory()->post->create_many( 5 ); $ids = self::factory()->post->create_many( 5 ); @@ -945,4 +945,33 @@ public function test_found_posts_are_correct_for_term_queries() { $this->assertEquals( 3, $q->max_num_pages, $message ); } + /** + * @ticket 47280 + */ + public function test_found_posts_are_correct_for_meta_queries() { + self::factory()->post->create_many( 5 ); + $ids = self::factory()->post->create_many( 5 ); + + foreach ( $ids as $id ) { + add_post_meta( $id, 'my_meta', 'foo' ); + } + + $q = new WP_Query( + array( + 'posts_per_page' => 2, + 'meta_key' => 'my_meta', + ) + ); + + $message = sprintf( + "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", + $q->request, + $q->request_count + ); + + $this->assertSame( 2, $q->post_count, $message ); + $this->assertSame( 5, $q->found_posts, $message ); + $this->assertEquals( 3, $q->max_num_pages, $message ); + } + } From 457f708515c2b8e0a99cdadc72217f0cf41ebeb2 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 11:36:07 +0100 Subject: [PATCH 25/71] Remove some duplication. --- tests/phpunit/tests/post/query.php | 64 ++++++++++-------------------- 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 8d452c61e40da..16f24be1aedb6 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -852,15 +852,9 @@ public function test_found_posts_are_correct_for_basic_query() { ) ); - $message = sprintf( - "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", - $q->request, - $q->request_count - ); - - $this->assertSame( 2, $q->post_count, $message ); - $this->assertSame( 5, $q->found_posts, $message ); - $this->assertEquals( 3, $q->max_num_pages, $message ); + $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } /** @@ -876,15 +870,9 @@ public function test_found_posts_are_correct_for_paged_query() { ) ); - $message = sprintf( - "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", - $q->request, - $q->request_count - ); - - $this->assertSame( 1, $q->post_count, $message ); - $this->assertSame( 5, $q->found_posts, $message ); - $this->assertEquals( 3, $q->max_num_pages, $message ); + $this->assertSame( 1, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } /** @@ -904,15 +892,9 @@ public function test_found_posts_are_correct_for_author_queries() { ) ); - $message = sprintf( - "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", - $q->request, - $q->request_count - ); - - $this->assertSame( 2, $q->post_count, $message ); - $this->assertSame( 5, $q->found_posts, $message ); - $this->assertEquals( 3, $q->max_num_pages, $message ); + $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } /** @@ -934,15 +916,9 @@ public function test_found_posts_are_correct_for_tax_queries() { ) ); - $message = sprintf( - "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", - $q->request, - $q->request_count - ); - - $this->assertSame( 2, $q->post_count, $message ); - $this->assertSame( 5, $q->found_posts, $message ); - $this->assertEquals( 3, $q->max_num_pages, $message ); + $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } /** @@ -963,15 +939,17 @@ public function test_found_posts_are_correct_for_meta_queries() { ) ); - $message = sprintf( + $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); + } + + protected static function get_count_message( WP_Query $query ) { + return sprintf( "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", - $q->request, - $q->request_count + $query->request, + $query->request_count ); - - $this->assertSame( 2, $q->post_count, $message ); - $this->assertSame( 5, $q->found_posts, $message ); - $this->assertEquals( 3, $q->max_num_pages, $message ); } } From e2a1990133b3d401af64164e6175c21d8f129c5c Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 11:41:51 +0100 Subject: [PATCH 26/71] More coverage for `no_found_rows` values. --- tests/phpunit/tests/post/query.php | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 16f24be1aedb6..6ddccfd21a7f5 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -762,32 +762,40 @@ public function test_found_posts_should_be_integer_even_if_found_posts_filter_re /** * @ticket 47280 */ - public function test_found_posts_property_is_present() { + public function test_found_posts_property_is_present_with_no_found_rows_false() { $q = new WP_Query( array( - 'post_type' => 'post', + 'post_type' => 'post', + 'no_found_rows' => false, ) ); - $this->assertTrue( isset( $q->found_posts ) ); $this->assertTrue( property_exists( $q, 'found_posts' ) ); - $this->assertObjectHasAttribute( 'found_posts', $q ); + $this->assertTrue( isset( $q->found_posts ) ); $this->assertNotNull( $q->found_posts ); + + $this->assertTrue( property_exists( $q, 'max_num_pages' ) ); + $this->assertTrue( isset( $q->max_num_pages ) ); + $this->assertNotNull( $q->max_num_pages ); } /** * @ticket 47280 */ - public function test_max_num_pages_property_is_present() { + public function test_found_posts_property_is_present_with_no_found_rows_true() { $q = new WP_Query( array( - 'post_type' => 'post', + 'post_type' => 'post', + 'no_found_rows' => true, ) ); - $this->assertTrue( isset( $q->max_num_pages ) ); + $this->assertTrue( property_exists( $q, 'found_posts' ) ); + $this->assertTrue( isset( $q->found_posts ) ); + $this->assertNotNull( $q->found_posts ); + $this->assertTrue( property_exists( $q, 'max_num_pages' ) ); - $this->assertObjectHasAttribute( 'max_num_pages', $q ); + $this->assertTrue( isset( $q->max_num_pages ) ); $this->assertNotNull( $q->max_num_pages ); } From 620c392cb8a746c7e5fd25b53f6bf1dd1561b4c3 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 11:44:03 +0100 Subject: [PATCH 27/71] Coding standards. --- tests/phpunit/tests/post/query.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 6ddccfd21a7f5..966b799848149 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -889,9 +889,12 @@ public function test_found_posts_are_correct_for_paged_query() { public function test_found_posts_are_correct_for_author_queries() { $author = self::factory()->user->create(); self::factory()->post->create_many( 5 ); - self::factory()->post->create_many( 5, array( - 'post_author' => $author, - ) ); + self::factory()->post->create_many( + 5, + array( + 'post_author' => $author, + ) + ); $q = new WP_Query( array( From af266411a8d6d16abdedcd42bb5bb8729ab6825a Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 12:04:45 +0100 Subject: [PATCH 28/71] Ensure there are no errors and the counts are correct for empty queries. --- src/wp-includes/class-wp-query.php | 2 +- tests/phpunit/tests/post/query.php | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 0ce9932846213..c1d5fb2d3f2be 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3342,7 +3342,7 @@ private function set_found_posts() { // Bail if posts is an empty array. Continue if posts is an empty string, // null, or false to accommodate caching plugins that fill posts later. - if ( $q['no_found_rows'] || ( is_array( $this->posts ) && ! $this->posts ) ) { + if ( empty( $q ) || $q['no_found_rows'] || ( is_array( $this->posts ) && ! $this->posts ) ) { $this->found_posts = 0; return; } diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 966b799848149..2b55e78f22ccf 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -848,6 +848,19 @@ public function test_found_posts_should_be_lazily_loaded() { $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again ); } + /** + * @ticket 47280 + */ + public function test_found_posts_are_correct_for_empty_query() { + self::factory()->post->create_many( 12 ); + + $q = new WP_Query(); + + $this->assertSame( 0, $q->post_count ); + $this->assertSame( 0, $q->found_posts ); + $this->assertEquals( 0, $q->max_num_pages ); + } + /** * @ticket 47280 */ From a3c968de72c17fe3c4dbfdd5670514240fa5f9a8 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 12:19:25 +0100 Subject: [PATCH 29/71] Add tests for queries with no limits or paging. --- tests/phpunit/tests/post/query.php | 38 +++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 2b55e78f22ccf..d866fa445ba57 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -858,7 +858,7 @@ public function test_found_posts_are_correct_for_empty_query() { $this->assertSame( 0, $q->post_count ); $this->assertSame( 0, $q->found_posts ); - $this->assertEquals( 0, $q->max_num_pages ); + $this->assertSame( 0, $q->max_num_pages ); } /** @@ -878,6 +878,42 @@ public function test_found_posts_are_correct_for_basic_query() { $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } + /** + * @ticket 47280 + */ + public function test_found_posts_are_correct_for_query_with_no_limit() { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'posts_per_page' => -1, + ) + ); + + $this->assertSame( 5, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + // You would expect this to be 1 but historically it's 0 for posts without paging + $this->assertSame( 0, $q->max_num_pages, self::get_count_message( $q ) ); + } + + /** + * @ticket 47280 + */ + public function test_found_posts_are_correct_for_query_with_no_paging() { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'nopaging' => true, + ) + ); + + $this->assertSame( 5, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + // You would expect this to be 1 but historically it's 0 for posts without paging + $this->assertSame( 0, $q->max_num_pages, self::get_count_message( $q ) ); + } + /** * @ticket 47280 */ From d74745b69055fb7d59d6adb54c2d1f2482499c03 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 14:34:26 +0100 Subject: [PATCH 30/71] Run all the relevant tests with each of the available `fields` args. --- tests/phpunit/tests/post/query.php | 74 ++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index d866fa445ba57..354cba124f35f 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -761,10 +761,14 @@ public function test_found_posts_should_be_integer_even_if_found_posts_filter_re /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_property_is_present_with_no_found_rows_false() { + public function test_found_posts_property_is_present_with_no_found_rows_false( $fields ) { $q = new WP_Query( array( + 'fields' => $fields, 'post_type' => 'post', 'no_found_rows' => false, ) @@ -781,10 +785,14 @@ public function test_found_posts_property_is_present_with_no_found_rows_false() /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_property_is_present_with_no_found_rows_true() { + public function test_found_posts_property_is_present_with_no_found_rows_true( $fields ) { $q = new WP_Query( array( + 'fields' => $fields, 'post_type' => 'post', 'no_found_rows' => true, ) @@ -801,8 +809,11 @@ public function test_found_posts_property_is_present_with_no_found_rows_true() { /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_should_be_lazily_loaded() { + public function test_found_posts_should_be_lazily_loaded( $fields ) { global $wpdb; self::factory()->post->create_many( 5 ); @@ -811,6 +822,7 @@ public function test_found_posts_should_be_lazily_loaded() { $q = new WP_Query( array( + 'fields' => $fields, 'posts_per_page' => 2, 'update_post_term_cache' => false, 'update_post_meta_cache' => false, @@ -863,12 +875,16 @@ public function test_found_posts_are_correct_for_empty_query() { /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_are_correct_for_basic_query() { + public function test_found_posts_are_correct_for_basic_query( $fields ) { self::factory()->post->create_many( 5 ); $q = new WP_Query( array( + 'fields' => $fields, 'posts_per_page' => 2, ) ); @@ -880,12 +896,16 @@ public function test_found_posts_are_correct_for_basic_query() { /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_are_correct_for_query_with_no_limit() { + public function test_found_posts_are_correct_for_query_with_no_limit( $fields ) { self::factory()->post->create_many( 5 ); $q = new WP_Query( array( + 'fields' => $fields, 'posts_per_page' => -1, ) ); @@ -898,12 +918,16 @@ public function test_found_posts_are_correct_for_query_with_no_limit() { /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_are_correct_for_query_with_no_paging() { + public function test_found_posts_are_correct_for_query_with_no_paging( $fields ) { self::factory()->post->create_many( 5 ); $q = new WP_Query( array( + 'fields' => $fields, 'nopaging' => true, ) ); @@ -916,12 +940,16 @@ public function test_found_posts_are_correct_for_query_with_no_paging() { /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_are_correct_for_paged_query() { + public function test_found_posts_are_correct_for_paged_query( $fields ) { self::factory()->post->create_many( 5 ); $q = new WP_Query( array( + 'fields' => $fields, 'posts_per_page' => 2, 'paged' => 3, ) @@ -934,8 +962,11 @@ public function test_found_posts_are_correct_for_paged_query() { /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_are_correct_for_author_queries() { + public function test_found_posts_are_correct_for_author_queries( $fields ) { $author = self::factory()->user->create(); self::factory()->post->create_many( 5 ); self::factory()->post->create_many( @@ -947,6 +978,7 @@ public function test_found_posts_are_correct_for_author_queries() { $q = new WP_Query( array( + 'fields' => $fields, 'posts_per_page' => 2, 'author' => $author, ) @@ -959,8 +991,11 @@ public function test_found_posts_are_correct_for_author_queries() { /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_are_correct_for_tax_queries() { + public function test_found_posts_are_correct_for_tax_queries( $fields ) { $term = self::factory()->term->create_and_get(); self::factory()->post->create_many( 5 ); $ids = self::factory()->post->create_many( 5 ); @@ -971,6 +1006,7 @@ public function test_found_posts_are_correct_for_tax_queries() { $q = new WP_Query( array( + 'fields' => $fields, 'posts_per_page' => 2, 'tag' => $term->slug, ) @@ -983,8 +1019,11 @@ public function test_found_posts_are_correct_for_tax_queries() { /** * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields */ - public function test_found_posts_are_correct_for_meta_queries() { + public function test_found_posts_are_correct_for_meta_queries( $fields ) { self::factory()->post->create_many( 5 ); $ids = self::factory()->post->create_many( 5 ); @@ -994,6 +1033,7 @@ public function test_found_posts_are_correct_for_meta_queries() { $q = new WP_Query( array( + 'fields' => $fields, 'posts_per_page' => 2, 'meta_key' => 'my_meta', ) @@ -1004,6 +1044,20 @@ public function test_found_posts_are_correct_for_meta_queries() { $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } + public function dataFields() { + return array( + 'posts' => array( + '', + ), + 'ids' => array( + 'ids', + ), + 'parents' => array( + 'id=>parent', + ), + ); + } + protected static function get_count_message( WP_Query $query ) { return sprintf( "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", From 22095a1eb80ad8e1c3fa3541b755e6b2945e2621 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 15:16:26 +0100 Subject: [PATCH 31/71] Support PHP and JSON serialization. --- src/wp-includes/class-wp-query.php | 28 ++++++++++++- tests/phpunit/tests/post/query.php | 66 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index c1d5fb2d3f2be..f5f7c2ddd3bca 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -15,7 +15,7 @@ * @since 1.5.0 * @since 4.5.0 Removed the `$comments_popup` property. */ -class WP_Query { +class WP_Query implements JsonSerializable { /** * Query vars set by the user. @@ -3734,6 +3734,32 @@ public function __call( $name, $arguments ) { return false; } + /** + * Controls how the object is represented during PHP serialization. + * + * @since x.x.x + * + * @return array The properties of the object as an associative array. + */ + public function __serialize() { + $this->set_found_posts(); + + return get_object_vars( $this ); + } + + /** + * Controls how the object is represented during JSON serialization. + * + * @since x.x.x + * + * @return array The properties of the object as an associative array. + */ + public function jsonSerialize() { + $this->set_found_posts(); + + return get_object_vars( $this ); + } + /** * Is the query for an existing archive page? * diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 354cba124f35f..578fb8420220b 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -807,6 +807,72 @@ public function test_found_posts_property_is_present_with_no_found_rows_true( $f $this->assertNotNull( $q->max_num_pages ); } + /** + * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields + */ + public function test_found_posts_property_is_present_in_php_serialized_query( $fields ) { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'post_type' => 'post', + ) + ); + + $serialized = serialize( $q ); + + // Create more matching posts to simulate unserialization occuring at a later date. + self::factory()->post->create_many( 5 ); + + $unserialized = unserialize( $serialized ); + + $this->assertTrue( property_exists( $unserialized, 'found_posts' ) ); + $this->assertTrue( isset( $unserialized->found_posts ) ); + $this->assertSame( 5, $unserialized->found_posts ); + + $this->assertTrue( property_exists( $unserialized, 'max_num_pages' ) ); + $this->assertTrue( isset( $unserialized->max_num_pages ) ); + $this->assertEquals( 3, $unserialized->max_num_pages ); + } + + /** + * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields + */ + public function test_found_posts_property_is_present_in_json_serialized_query( $fields ) { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'post_type' => 'post', + ) + ); + + $serialized = json_encode( $q ); + + // Create more matching posts to simulate unserialization occuring at a later date. + self::factory()->post->create_many( 5 ); + + $unserialized = json_decode( $serialized ); + + $this->assertTrue( property_exists( $unserialized, 'found_posts' ) ); + $this->assertTrue( isset( $unserialized->found_posts ) ); + $this->assertSame( 5, $unserialized->found_posts ); + + $this->assertTrue( property_exists( $unserialized, 'max_num_pages' ) ); + $this->assertTrue( isset( $unserialized->max_num_pages ) ); + $this->assertEquals( 3, $unserialized->max_num_pages ); + } + /** * @ticket 47280 * @dataProvider dataFields From 64e6363f91c008812ba5a6af721cc3891c3e8b41 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 15:17:56 +0100 Subject: [PATCH 32/71] Be more specific about this guard condition. --- src/wp-includes/class-wp-query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index f5f7c2ddd3bca..1d3e7eb0b9ed3 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3342,7 +3342,7 @@ private function set_found_posts() { // Bail if posts is an empty array. Continue if posts is an empty string, // null, or false to accommodate caching plugins that fill posts later. - if ( empty( $q ) || $q['no_found_rows'] || ( is_array( $this->posts ) && ! $this->posts ) ) { + if ( ! isset( $q['no_found_rows'] ) || $q['no_found_rows'] || ( is_array( $this->posts ) && ! $this->posts ) ) { $this->found_posts = 0; return; } From 940890938c5e65526d87593ed77a2784439628d8 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 15:19:38 +0100 Subject: [PATCH 33/71] Coding standards. --- tests/phpunit/tests/post/query.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 578fb8420220b..cb4f3cd9bf88d 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1112,10 +1112,10 @@ public function test_found_posts_are_correct_for_meta_queries( $fields ) { public function dataFields() { return array( - 'posts' => array( + 'posts' => array( '', ), - 'ids' => array( + 'ids' => array( 'ids', ), 'parents' => array( From 92059b3126e89e9575185ead060b7dca022ea7dd Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 15:59:53 +0100 Subject: [PATCH 34/71] Implement serialization in a way that's compatible with PHP 5.6 to 8.1. --- src/wp-includes/class-wp-query.php | 43 +++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 1d3e7eb0b9ed3..8e32d1ba46a25 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -15,7 +15,7 @@ * @since 1.5.0 * @since 4.5.0 Removed the `$comments_popup` property. */ -class WP_Query implements JsonSerializable { +class WP_Query implements JsonSerializable, Serializable { /** * Query vars set by the user. @@ -3739,6 +3739,19 @@ public function __call( $name, $arguments ) { * * @since x.x.x * + * @return string The PHP serialized representation of the object. + */ + public function serialize() { + return serialize( $this->__serialize() ); + } + + /** + * Controls how the object is represented during PHP serialization. + * + * Used by PHP >= 7.4. + * + * @since x.x.x + * * @return array The properties of the object as an associative array. */ public function __serialize() { @@ -3747,6 +3760,34 @@ public function __serialize() { return get_object_vars( $this ); } + /** + * Controls how the object is reconstructed from a PHP serialized representation. + * + * @since x.x.x + * + * @param string $data The PHP serialized representation of the object. + * @return void + */ + public function unserialize( $data ) { + $this->__unserialize( unserialize( $data ) ); + } + + /** + * Controls how the object is reconstructed from a PHP serialized representation. + * + * Used by PHP >= 7.4. + * + * @since x.x.x + * + * @param array $data The associative array representation of the object. + * @return void + */ + public function __unserialize( $data ) { + foreach ( $data as $key => $value ) { + $this->$key = $value; + } + } + /** * Controls how the object is represented during JSON serialization. * From 262ad2ebe9f83c45c03fd0f84221c5001149a55b Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 19:36:51 +0100 Subject: [PATCH 35/71] Ignore PHPCS warnings about the magic serialization methods. --- src/wp-includes/class-wp-query.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 8e32d1ba46a25..7259cf9ebfc57 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3754,7 +3754,7 @@ public function serialize() { * * @return array The properties of the object as an associative array. */ - public function __serialize() { + public function __serialize() { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound $this->set_found_posts(); return get_object_vars( $this ); @@ -3782,7 +3782,7 @@ public function unserialize( $data ) { * @param array $data The associative array representation of the object. * @return void */ - public function __unserialize( $data ) { + public function __unserialize( $data ) { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound foreach ( $data as $key => $value ) { $this->$key = $value; } From 70316e1d5b2ed1aba340f1ba40e16213b681013d Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 22:08:07 +0100 Subject: [PATCH 36/71] Add a test for a filter on `posts_request` that uses `SQL_CALC_FOUND_ROWS`. --- tests/phpunit/tests/post/query.php | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index cb4f3cd9bf88d..18e7eea7533bc 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1110,6 +1110,46 @@ public function test_found_posts_are_correct_for_meta_queries( $fields ) { $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } + /** + * @ticket 47280 + */ + public function test_posts_are_counted_with_select_found_rows_when_query_includes_sql_calc_found_rows() { + // Create five published posts. + self::factory()->post->create_many( 5 ); + // Create ten draft posts. + self::factory()->post->create_many( + 10, + array( + 'post_status' => 'draft', + ) + ); + + add_filter( 'posts_request', function( $request ) { + global $wpdb; + + return " + SELECT SQL_CALC_FOUND_ROWS {$wpdb->posts}.ID + FROM {$wpdb->posts} + WHERE 1=1 + AND {$wpdb->posts}.post_type = 'post' + AND {$wpdb->posts}.post_status = 'draft' + ORDER BY {$wpdb->posts}.post_date + DESC LIMIT 0, 2 + "; + } ); + + $q = new WP_Query( + array( + 'posts_per_page' => 2, + ) + ); + + $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 10, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 5, $q->max_num_pages, self::get_count_message( $q ) ); + } + + public function dataFields() { return array( 'posts' => array( From cc927eeae526d0702c222e8666ba12e2c056cdc8 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 22:15:27 +0100 Subject: [PATCH 37/71] Add back compat in case anything uses the `posts_request` filter and reinstates the `SQL_CALC_FOUND_ROWS` modifier. --- src/wp-includes/class-wp-query.php | 40 ++++++++++++++++++++++++++---- tests/phpunit/tests/post/query.php | 1 + 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 7259cf9ebfc57..e23c9cd7e2601 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -188,6 +188,15 @@ class WP_Query implements JsonSerializable, Serializable { */ private $max_num_pages = 0; + /** + * Undocumented variable + * + * @since x.x.x + * + * @var bool + */ + private $use_calc_found_rows = false; + /** * The number of comment pages. * @@ -3024,12 +3033,17 @@ public function get_posts() { * Filters the completed SQL query before sending. * * @since 2.0.0 - * @since x.x.x This query now no longer contains a `SQL_CALC_FOUND_ROWS` modifier. + * @since x.x.x This query no longer contains a `SQL_CALC_FOUND_ROWS` modifier by default. * * @param string $request The complete SQL query. * @param WP_Query $query The WP_Query instance (passed by reference). */ $this->request = apply_filters_ref_array( 'posts_request', array( $this->request, &$this ) ); + + if ( false !== strpos( $this->request, 'SQL_CALC_FOUND_ROWS' ) ) { + $this->use_calc_found_rows = true; + $this->request_count = 'SELECT FOUND_ROWS()'; + } } /** @@ -3058,7 +3072,7 @@ public function get_posts() { /** @var int[] */ $this->posts = array_map( 'intval', $this->posts ); $this->post_count = count( $this->posts ); - $this->limits = $limits; + $this->set_limits( $limits ); return $this->posts; } @@ -3069,7 +3083,7 @@ public function get_posts() { } $this->post_count = count( $this->posts ); - $this->limits = $limits; + $this->set_limits( $limits ); /** @var int[] */ $r = array(); @@ -3120,14 +3134,15 @@ public function get_posts() { if ( $ids ) { $this->posts = $ids; - $this->limits = $limits; + $this->set_limits( $limits ); + _prime_post_caches( $ids, $q['update_post_term_cache'], $q['update_post_meta_cache'] ); } else { $this->posts = array(); } } else { $this->posts = $wpdb->get_results( $this->request ); - $this->limits = $limits; + $this->set_limits( $limits ); } } @@ -3388,6 +3403,21 @@ private function set_found_posts() { } } + /** + * Undocumented function + * + * @since x.x.x + * + * @param string $limits ... + */ + private function set_limits( $limits ) { + $this->limits = $limits; + + if ( $this->use_calc_found_rows ) { + $this->set_found_posts(); + } + } + /** * Set up the next post and iterate current post index. * diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 18e7eea7533bc..caac02492e114 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1144,6 +1144,7 @@ public function test_posts_are_counted_with_select_found_rows_when_query_include ) ); + // These results should now reflect the results for draft posts, as set by the filter $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); $this->assertSame( 10, $q->found_posts, self::get_count_message( $q ) ); $this->assertEquals( 5, $q->max_num_pages, self::get_count_message( $q ) ); From d27629bf7bf14047e1ee358621f24fe79ed4455e Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 22:52:10 +0100 Subject: [PATCH 38/71] Trigger a deprecated notice when a filter on `posts_request` returns a query containing `SQL_CALC_FOUND_ROWS`. --- src/wp-includes/class-wp-query.php | 2 ++ tests/phpunit/tests/post/query.php | 1 + 2 files changed, 3 insertions(+) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index e23c9cd7e2601..42d8a4a1220e0 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3041,6 +3041,8 @@ public function get_posts() { $this->request = apply_filters_ref_array( 'posts_request', array( $this->request, &$this ) ); if ( false !== strpos( $this->request, 'SQL_CALC_FOUND_ROWS' ) ) { + _deprecated_argument( 'The posts_request filter', 'x.x.x', '...' ); + $this->use_calc_found_rows = true; $this->request_count = 'SELECT FOUND_ROWS()'; } diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index caac02492e114..0548dcd76257e 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1112,6 +1112,7 @@ public function test_found_posts_are_correct_for_meta_queries( $fields ) { /** * @ticket 47280 + * @expectedDeprecated The posts_request filter */ public function test_posts_are_counted_with_select_found_rows_when_query_includes_sql_calc_found_rows() { // Create five published posts. From 492caf523149bd69420cd58bb6ddc0111332dd37 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 12 Jan 2022 22:53:35 +0100 Subject: [PATCH 39/71] Boop. --- tests/phpunit/tests/post/query.php | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 0548dcd76257e..8e782f8ed41d2 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1125,19 +1125,22 @@ public function test_posts_are_counted_with_select_found_rows_when_query_include ) ); - add_filter( 'posts_request', function( $request ) { - global $wpdb; - - return " - SELECT SQL_CALC_FOUND_ROWS {$wpdb->posts}.ID - FROM {$wpdb->posts} - WHERE 1=1 - AND {$wpdb->posts}.post_type = 'post' - AND {$wpdb->posts}.post_status = 'draft' - ORDER BY {$wpdb->posts}.post_date - DESC LIMIT 0, 2 - "; - } ); + add_filter( + 'posts_request', + function( $request ) { + global $wpdb; + + return " + SELECT SQL_CALC_FOUND_ROWS {$wpdb->posts}.ID + FROM {$wpdb->posts} + WHERE 1=1 + AND {$wpdb->posts}.post_type = 'post' + AND {$wpdb->posts}.post_status = 'draft' + ORDER BY {$wpdb->posts}.post_date + DESC LIMIT 0, 2 + "; + } + ); $q = new WP_Query( array( From 1c3df82a64059ea187a872ca9861313c0154f90c Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 19 Jan 2022 23:17:53 +0100 Subject: [PATCH 40/71] Add an explicit assertion for `FOUND_ROWS()`. --- tests/phpunit/tests/post/query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 8e782f8ed41d2..7a84c6f9cf9b7 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1149,12 +1149,12 @@ function( $request ) { ); // These results should now reflect the results for draft posts, as set by the filter + $this->assertStringContainsString( 'SELECT FOUND_ROWS()', $q->request_count ); $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); $this->assertSame( 10, $q->found_posts, self::get_count_message( $q ) ); $this->assertEquals( 5, $q->max_num_pages, self::get_count_message( $q ) ); } - public function dataFields() { return array( 'posts' => array( From 759e8d85a88494b0ec49c37ef6d1fbb3adf09009 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 20 Jan 2022 12:17:33 +0100 Subject: [PATCH 41/71] Add a test for switching sites between the query and fetching the post count. --- tests/phpunit/tests/post/query.php | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 7a84c6f9cf9b7..baa98cb056601 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1110,6 +1110,41 @@ public function test_found_posts_are_correct_for_meta_queries( $fields ) { $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } + /** + * @ticket 47280 + * @group ms-required + * @dataProvider dataFields + * + * @param string $fields + */ + public function test_found_posts_are_correct_when_switching_between_sites( $fields ) { + $blog = self::factory()->blog->create(); + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + ) + ); + + // Switch to another site. + switch_to_blog( $blog ); + + // Count the posts from the original site. This works because the SQL query + // and its table names has already been formed during the original query. + $post_count = $q->post_count; + $found_posts = $q->found_posts; + $max_num_pages = $q->max_num_pages; + + // Switch back. + restore_current_blog(); + + $this->assertSame( 2, $post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 3, $max_num_pages, self::get_count_message( $q ) ); + } + /** * @ticket 47280 * @expectedDeprecated The posts_request filter From ceec5ff15f067fbbe33b4508d3c44825bf94b37f Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 20 Jan 2022 12:48:36 +0100 Subject: [PATCH 42/71] Coding standards. --- tests/phpunit/tests/post/query.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index baa98cb056601..feaa22e0c8606 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1133,8 +1133,8 @@ public function test_found_posts_are_correct_when_switching_between_sites( $fiel // Count the posts from the original site. This works because the SQL query // and its table names has already been formed during the original query. - $post_count = $q->post_count; - $found_posts = $q->found_posts; + $post_count = $q->post_count; + $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; // Switch back. From 26174be996cd46f1e99788b1dccfce6adaf921a8 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 20 Jan 2022 12:48:51 +0100 Subject: [PATCH 43/71] Add more tests for `no_found_rows`. --- tests/phpunit/tests/post/query.php | 68 ++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index feaa22e0c8606..a38e25308a1e4 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -840,6 +840,40 @@ public function test_found_posts_property_is_present_in_php_serialized_query( $f $this->assertEquals( 3, $unserialized->max_num_pages ); } + /** + * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields + */ + public function test_found_posts_property_is_present_in_php_serialized_query_with_no_found_rows( $fields ) { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'post_type' => 'post', + 'no_found_rows' => true, + ) + ); + + $serialized = serialize( $q ); + + // Create more matching posts to simulate unserialization occuring at a later date. + self::factory()->post->create_many( 5 ); + + $unserialized = unserialize( $serialized ); + + $this->assertTrue( property_exists( $unserialized, 'found_posts' ) ); + $this->assertTrue( isset( $unserialized->found_posts ) ); + $this->assertSame( 0, $unserialized->found_posts ); + + $this->assertTrue( property_exists( $unserialized, 'max_num_pages' ) ); + $this->assertTrue( isset( $unserialized->max_num_pages ) ); + $this->assertSame( 0, $unserialized->max_num_pages ); + } + /** * @ticket 47280 * @dataProvider dataFields @@ -873,6 +907,40 @@ public function test_found_posts_property_is_present_in_json_serialized_query( $ $this->assertEquals( 3, $unserialized->max_num_pages ); } + /** + * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields + */ + public function test_found_posts_property_is_present_in_json_serialized_query_with_no_found_rows( $fields ) { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'post_type' => 'post', + 'no_found_rows' => true, + ) + ); + + $serialized = json_encode( $q ); + + // Create more matching posts to simulate unserialization occuring at a later date. + self::factory()->post->create_many( 5 ); + + $unserialized = json_decode( $serialized ); + + $this->assertTrue( property_exists( $unserialized, 'found_posts' ) ); + $this->assertTrue( isset( $unserialized->found_posts ) ); + $this->assertSame( 0, $unserialized->found_posts ); + + $this->assertTrue( property_exists( $unserialized, 'max_num_pages' ) ); + $this->assertTrue( isset( $unserialized->max_num_pages ) ); + $this->assertSame( 0, $unserialized->max_num_pages ); + } + /** * @ticket 47280 * @dataProvider dataFields From 241f805ccc741d898ef46e1008ccad40bd9d0273 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 28 Jan 2022 23:30:52 +0100 Subject: [PATCH 44/71] Allow the `found_posts` and `max_num_pages` properties to be set externally. --- src/wp-includes/class-wp-query.php | 37 +++++++++++++++++++++++------- tests/phpunit/tests/post/query.php | 30 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 42d8a4a1220e0..7bf646920a6c4 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -529,18 +529,18 @@ private function init_query_flags() { * @since 1.5.0 */ public function init() { - unset( $this->posts ); - unset( $this->query ); + $this->posts = null; + $this->query = null; $this->query_vars = array(); - unset( $this->queried_object ); - unset( $this->queried_object_id ); + $this->queried_object = null; + $this->queried_object_id = null; $this->post_count = 0; $this->current_post = -1; $this->in_the_loop = false; - unset( $this->request ); - unset( $this->post ); - unset( $this->comments ); - unset( $this->comment ); + $this->request = null; + $this->post = null; + $this->comments = null; + $this->comment = null; $this->comment_count = 0; $this->current_comment = -1; $this->found_posts = null; @@ -3732,6 +3732,27 @@ public function __get( $name ) { } } + /** + * Allows some private properties to be set. + * + * @since x.x.x + * + * @param string $name Name of property to set. + * @param mixed $value Value to set. + */ + public function __set( $name, $value ) { + if ( 'found_posts' === $name || 'max_num_pages' === $name ) { + $this->$name = $value; + return; + } + + // This allows third party code to set dynamic properties. + if ( ! property_exists( $this, $name ) ) { + $this->$name = $value; + return; + } + } + /** * Make private properties checkable for backward compatibility and lazy loading. * diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index a38e25308a1e4..808363185a886 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -807,6 +807,36 @@ public function test_found_posts_property_is_present_with_no_found_rows_true( $f $this->assertNotNull( $q->max_num_pages ); } + /** + * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields + */ + public function test_found_posts_property_can_be_set_externally( $fields ) { + add_filter( + 'posts_request', + function( $request, $query ) { + $query->found_posts = 123; + $query->max_num_pages = 456; + + return $request; + }, + 10, + 2 + ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'post_type' => 'post', + ) + ); + + $this->assertSame( 123, $q->found_posts ); + $this->assertEquals( 456, $q->max_num_pages ); + } + /** * @ticket 47280 * @dataProvider dataFields From 1ab6b3aa39a26f6952db489ec1b5fb9306c5443b Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 29 Jan 2022 19:27:48 +0100 Subject: [PATCH 45/71] Rename this for clarity. --- src/wp-includes/class-wp-query.php | 8 ++++---- tests/phpunit/tests/post/query.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 7bf646920a6c4..ae9ff02e0a010 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -89,7 +89,7 @@ class WP_Query implements JsonSerializable, Serializable { * @since xxx * @var string */ - public $request_count; + public $count_request; /** * Array of post objects or post IDs. @@ -3026,7 +3026,7 @@ public function get_posts() { $old_request = "SELECT $distinct $fields FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby $orderby $limits"; $this->request = $old_request; - $this->request_count = "SELECT COUNT($distinct {$wpdb->posts}.ID) FROM {$wpdb->posts} $join WHERE 1=1 $where"; + $this->count_request = "SELECT COUNT($distinct {$wpdb->posts}.ID) FROM {$wpdb->posts} $join WHERE 1=1 $where"; if ( ! $q['suppress_filters'] ) { /** @@ -3044,7 +3044,7 @@ public function get_posts() { _deprecated_argument( 'The posts_request filter', 'x.x.x', '...' ); $this->use_calc_found_rows = true; - $this->request_count = 'SELECT FOUND_ROWS()'; + $this->count_request = 'SELECT FOUND_ROWS()'; } } @@ -3374,7 +3374,7 @@ private function set_found_posts() { * @param string $found_posts_query The query to run to find the found posts. * @param WP_Query $query The WP_Query instance (passed by reference). */ - $found_posts_query = apply_filters_ref_array( 'found_posts_query', array( $this->request_count, &$this ) ); + $found_posts_query = apply_filters_ref_array( 'found_posts_query', array( $this->count_request, &$this ) ); $this->found_posts = (int) $wpdb->get_var( $found_posts_query ); } else { diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 808363185a886..432b9e99e523b 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1282,7 +1282,7 @@ function( $request ) { ); // These results should now reflect the results for draft posts, as set by the filter - $this->assertStringContainsString( 'SELECT FOUND_ROWS()', $q->request_count ); + $this->assertStringContainsString( 'SELECT FOUND_ROWS()', $q->count_request ); $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); $this->assertSame( 10, $q->found_posts, self::get_count_message( $q ) ); $this->assertEquals( 5, $q->max_num_pages, self::get_count_message( $q ) ); @@ -1306,7 +1306,7 @@ protected static function get_count_message( WP_Query $query ) { return sprintf( "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", $query->request, - $query->request_count + $query->count_request ); } From 3a676788966505db759e03d0f7c988e8e5a7a3d7 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 30 Jan 2022 15:15:42 +0100 Subject: [PATCH 46/71] Apply some basic formatting to SQL queries during a test failure. --- tests/phpunit/tests/post/query.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 432b9e99e523b..7a147ba245a47 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1305,8 +1305,22 @@ public function dataFields() { protected static function get_count_message( WP_Query $query ) { return sprintf( "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", - $query->request, - $query->count_request + self::format_sql( $query->request ), + self::format_sql( $query->count_request ) + ); + } + + /** + * Applies some basic formatting to an SQL query to make it more readable during a test failure. + * + * @param string $sql + * @return string + */ + protected static function format_sql( $sql ) { + return preg_replace( + '# (FROM|INNER JOIN|LEFT JOIN|ON|WHERE|AND|GROUP BY|ORDER BY|LIMIT) #', + "\n\$1 ", + $sql ); } From dd323a754fee1094d0248b2d20a102bddf626111 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 30 Jan 2022 17:04:07 +0100 Subject: [PATCH 47/71] Add a test for queries that trigger the `post_status_join` branch. --- tests/phpunit/tests/post/query.php | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 7a147ba245a47..7ae0ec9fec5d1 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1153,6 +1153,48 @@ public function test_found_posts_are_correct_for_author_queries( $fields ) { $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } + /** + * A query for the following triggers an additional LEFT JOIN on `wp_posts`: + * + * - Custom taxonomy query + * - `post_status` specified + * - `post_type` not specified + * + * @ticket 47280 + * @dataProvider dataFields + * + * @param string $fields + */ + public function test_found_posts_are_correct_for_query_that_performs_post_status_join( $fields ) { + $taxonomy = 'post_status_join_tax'; + register_taxonomy( $taxonomy, 'post' ); + $term = self::factory()->term->create_and_get( + array( + 'taxonomy' => $taxonomy, + ), + ); + self::factory()->post->create_many( 5 ); + $ids = self::factory()->post->create_many( 5 ); + + foreach ( $ids as $id ) { + wp_set_post_terms( $id, $term->slug, $taxonomy ); + } + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'taxonomy' => $taxonomy, + 'term' => $term->slug, + 'post_status' => 'publish', + ) + ); + + $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); + } + /** * @ticket 47280 * @dataProvider dataFields From 656a371ef37898bb0c94f53887b40c32f4c12640 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 30 Jan 2022 17:15:50 +0100 Subject: [PATCH 48/71] Syntax correction. --- tests/phpunit/tests/post/query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 7ae0ec9fec5d1..c61305757aa2b 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1171,7 +1171,7 @@ public function test_found_posts_are_correct_for_query_that_performs_post_status $term = self::factory()->term->create_and_get( array( 'taxonomy' => $taxonomy, - ), + ) ); self::factory()->post->create_many( 5 ); $ids = self::factory()->post->create_many( 5 ); From 0a7325d80bf23d033d9845bc734eebde526df250 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 28 Mar 2022 22:55:37 +0200 Subject: [PATCH 49/71] Re-remove `SQL_CALC_FOUND_ROWS`. --- src/wp-includes/class-wp-query.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 70d2f4c68bfe5..39c74e54a39bd 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3024,13 +3024,8 @@ public function get_posts() { $orderby = 'ORDER BY ' . $orderby; } - $found_rows = ''; - if ( ! $q['no_found_rows'] && ! empty( $limits ) ) { - $found_rows = 'SQL_CALC_FOUND_ROWS'; - } - $old_request = " - SELECT $found_rows $distinct $fields + SELECT $distinct $fields FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby @@ -3133,7 +3128,7 @@ public function get_posts() { // First get the IDs and then fill in the objects. $this->request = " - SELECT $found_rows $distinct {$wpdb->posts}.ID + SELECT $distinct {$wpdb->posts}.ID FROM {$wpdb->posts} $join WHERE 1=1 $where $groupby From 32e9bd305756148ca580cdecea3e96005f969d98 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 28 Mar 2022 22:04:32 +0100 Subject: [PATCH 50/71] Apply suggestions from code review Co-authored-by: Colin Stewart <79332690+costdev@users.noreply.github.com> --- src/wp-includes/class-wp-user-query.php | 6 +++--- tests/phpunit/tests/post/query.php | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/wp-includes/class-wp-user-query.php b/src/wp-includes/class-wp-user-query.php index 89e122235d7c2..42ba79c07387a 100644 --- a/src/wp-includes/class-wp-user-query.php +++ b/src/wp-includes/class-wp-user-query.php @@ -784,9 +784,9 @@ public function query() { /** * Return a total count of users. - * Historically this ran SELECT FOUND_ROWS() after issuing the main query, - * but since SQL_CALC_FOUND_ROWS is now deprecated from MySQL, it instead repeats - * the same query with COUNT(*) instead of $this->query_fields. + * Historically this ran `SELECT FOUND_ROWS()` after issuing the main query, + * but since `SQL_CALC_FOUND_ROWS` is now deprecated from MySQL, it instead repeats + * the same query with `COUNT(*)` instead of `$this->query_fields`. * * @since 3.2.0 * @since 5.1.0 Added the `$this` parameter. diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index c61305757aa2b..9990d07ab89f0 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1001,25 +1001,25 @@ public function test_found_posts_should_be_lazily_loaded( $fields ) { $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; - // Count the queries after fetching found posts + // Count the queries after fetching found posts. $after_found_posts = ( $wpdb->num_queries - $start ); // Repeat $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; - // Count the queries after fetching found posts a second time + // Count the queries after fetching found posts a second time. $after_found_posts_again = ( $wpdb->num_queries - $start ); - // Ensure the posts were not initially counted + // Ensure the posts were not initially counted. $this->assertSame( 1, $before_found_posts ); - // Ensure the counts are correct + // Ensure the counts are correct. $this->assertSame( 2, $post_count ); $this->assertSame( 5, $found_posts ); $this->assertEquals( 3, $max_num_pages ); - // Ensure subsequent counts only triggered one query + // Ensure subsequent counts only triggered one query. $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts ); $this->assertSame( ( $before_found_posts + 1 ), $after_found_posts_again ); } @@ -1076,7 +1076,7 @@ public function test_found_posts_are_correct_for_query_with_no_limit( $fields ) $this->assertSame( 5, $q->post_count, self::get_count_message( $q ) ); $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); - // You would expect this to be 1 but historically it's 0 for posts without paging + // You would expect this to be 1 but historically it's 0 for posts without paging. $this->assertSame( 0, $q->max_num_pages, self::get_count_message( $q ) ); } @@ -1098,7 +1098,7 @@ public function test_found_posts_are_correct_for_query_with_no_paging( $fields ) $this->assertSame( 5, $q->post_count, self::get_count_message( $q ) ); $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); - // You would expect this to be 1 but historically it's 0 for posts without paging + // You would expect this to be 1 but historically it's 0 for posts without paging. $this->assertSame( 0, $q->max_num_pages, self::get_count_message( $q ) ); } @@ -1323,7 +1323,7 @@ function( $request ) { ) ); - // These results should now reflect the results for draft posts, as set by the filter + // These results should now reflect the results for draft posts, as set by the filter. $this->assertStringContainsString( 'SELECT FOUND_ROWS()', $q->count_request ); $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); $this->assertSame( 10, $q->found_posts, self::get_count_message( $q ) ); @@ -1355,8 +1355,8 @@ protected static function get_count_message( WP_Query $query ) { /** * Applies some basic formatting to an SQL query to make it more readable during a test failure. * - * @param string $sql - * @return string + * @param string $sql The SQL query to be formatted. + * @return string The formatted SQL query. */ protected static function format_sql( $sql ) { return preg_replace( From f24cb977f41055c503f960e922ab0850dd3e74e9 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 28 Mar 2022 23:07:00 +0200 Subject: [PATCH 51/71] Rename the fields data provider. --- tests/phpunit/tests/post/query.php | 36 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 9990d07ab89f0..129e12856f4aa 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -761,7 +761,7 @@ public function test_found_posts_should_be_integer_even_if_found_posts_filter_re /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -785,7 +785,7 @@ public function test_found_posts_property_is_present_with_no_found_rows_false( $ /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -809,7 +809,7 @@ public function test_found_posts_property_is_present_with_no_found_rows_true( $f /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -839,7 +839,7 @@ function( $request, $query ) { /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -872,7 +872,7 @@ public function test_found_posts_property_is_present_in_php_serialized_query( $f /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -906,7 +906,7 @@ public function test_found_posts_property_is_present_in_php_serialized_query_wit /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -939,7 +939,7 @@ public function test_found_posts_property_is_present_in_json_serialized_query( $ /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -973,7 +973,7 @@ public function test_found_posts_property_is_present_in_json_serialized_query_wi /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1039,7 +1039,7 @@ public function test_found_posts_are_correct_for_empty_query() { /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1060,7 +1060,7 @@ public function test_found_posts_are_correct_for_basic_query( $fields ) { /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1082,7 +1082,7 @@ public function test_found_posts_are_correct_for_query_with_no_limit( $fields ) /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1104,7 +1104,7 @@ public function test_found_posts_are_correct_for_query_with_no_paging( $fields ) /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1126,7 +1126,7 @@ public function test_found_posts_are_correct_for_paged_query( $fields ) { /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1161,7 +1161,7 @@ public function test_found_posts_are_correct_for_author_queries( $fields ) { * - `post_type` not specified * * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1197,7 +1197,7 @@ public function test_found_posts_are_correct_for_query_that_performs_post_status /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1225,7 +1225,7 @@ public function test_found_posts_are_correct_for_tax_queries( $fields ) { /** * @ticket 47280 - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1253,7 +1253,7 @@ public function test_found_posts_are_correct_for_meta_queries( $fields ) { /** * @ticket 47280 * @group ms-required - * @dataProvider dataFields + * @dataProvider data_fields * * @param string $fields */ @@ -1330,7 +1330,7 @@ function( $request ) { $this->assertEquals( 5, $q->max_num_pages, self::get_count_message( $q ) ); } - public function dataFields() { + public function data_fields() { return array( 'posts' => array( '', From 49b195ae57f48b293df1d29eceb753947c6bb725 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 28 Mar 2022 23:08:35 +0200 Subject: [PATCH 52/71] Use static closures. --- tests/phpunit/tests/post/query.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 129e12856f4aa..b08600f221b42 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -816,7 +816,7 @@ public function test_found_posts_property_is_present_with_no_found_rows_true( $f public function test_found_posts_property_can_be_set_externally( $fields ) { add_filter( 'posts_request', - function( $request, $query ) { + static function( $request, $query ) { $query->found_posts = 123; $query->max_num_pages = 456; @@ -1302,7 +1302,7 @@ public function test_posts_are_counted_with_select_found_rows_when_query_include add_filter( 'posts_request', - function( $request ) { + static function( $request ) { global $wpdb; return " From 85cba24b708fb7d97841e06cc1503e1a44ee9420 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 28 Mar 2022 23:12:44 +0200 Subject: [PATCH 53/71] Docs! --- tests/phpunit/tests/post/query.php | 45 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index b08600f221b42..c7d3a704ca9a5 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -763,7 +763,7 @@ public function test_found_posts_should_be_integer_even_if_found_posts_filter_re * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_property_is_present_with_no_found_rows_false( $fields ) { $q = new WP_Query( @@ -787,7 +787,7 @@ public function test_found_posts_property_is_present_with_no_found_rows_false( $ * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_property_is_present_with_no_found_rows_true( $fields ) { $q = new WP_Query( @@ -811,7 +811,7 @@ public function test_found_posts_property_is_present_with_no_found_rows_true( $f * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_property_can_be_set_externally( $fields ) { add_filter( @@ -841,7 +841,7 @@ static function( $request, $query ) { * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_property_is_present_in_php_serialized_query( $fields ) { self::factory()->post->create_many( 5 ); @@ -874,7 +874,7 @@ public function test_found_posts_property_is_present_in_php_serialized_query( $f * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_property_is_present_in_php_serialized_query_with_no_found_rows( $fields ) { self::factory()->post->create_many( 5 ); @@ -908,7 +908,7 @@ public function test_found_posts_property_is_present_in_php_serialized_query_wit * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_property_is_present_in_json_serialized_query( $fields ) { self::factory()->post->create_many( 5 ); @@ -941,7 +941,7 @@ public function test_found_posts_property_is_present_in_json_serialized_query( $ * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_property_is_present_in_json_serialized_query_with_no_found_rows( $fields ) { self::factory()->post->create_many( 5 ); @@ -975,7 +975,7 @@ public function test_found_posts_property_is_present_in_json_serialized_query_wi * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_should_be_lazily_loaded( $fields ) { global $wpdb; @@ -1041,7 +1041,7 @@ public function test_found_posts_are_correct_for_empty_query() { * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_are_correct_for_basic_query( $fields ) { self::factory()->post->create_many( 5 ); @@ -1062,7 +1062,7 @@ public function test_found_posts_are_correct_for_basic_query( $fields ) { * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_are_correct_for_query_with_no_limit( $fields ) { self::factory()->post->create_many( 5 ); @@ -1084,7 +1084,7 @@ public function test_found_posts_are_correct_for_query_with_no_limit( $fields ) * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_are_correct_for_query_with_no_paging( $fields ) { self::factory()->post->create_many( 5 ); @@ -1106,7 +1106,7 @@ public function test_found_posts_are_correct_for_query_with_no_paging( $fields ) * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_are_correct_for_paged_query( $fields ) { self::factory()->post->create_many( 5 ); @@ -1128,7 +1128,7 @@ public function test_found_posts_are_correct_for_paged_query( $fields ) { * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_are_correct_for_author_queries( $fields ) { $author = self::factory()->user->create(); @@ -1163,7 +1163,7 @@ public function test_found_posts_are_correct_for_author_queries( $fields ) { * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_are_correct_for_query_that_performs_post_status_join( $fields ) { $taxonomy = 'post_status_join_tax'; @@ -1199,7 +1199,7 @@ public function test_found_posts_are_correct_for_query_that_performs_post_status * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_are_correct_for_tax_queries( $fields ) { $term = self::factory()->term->create_and_get(); @@ -1227,7 +1227,7 @@ public function test_found_posts_are_correct_for_tax_queries( $fields ) { * @ticket 47280 * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_are_correct_for_meta_queries( $fields ) { self::factory()->post->create_many( 5 ); @@ -1255,7 +1255,7 @@ public function test_found_posts_are_correct_for_meta_queries( $fields ) { * @group ms-required * @dataProvider data_fields * - * @param string $fields + * @param string $fields Value of the `fields` argument for `WP_Query`. */ public function test_found_posts_are_correct_when_switching_between_sites( $fields ) { $blog = self::factory()->blog->create(); @@ -1330,6 +1330,11 @@ static function( $request ) { $this->assertEquals( 5, $q->max_num_pages, self::get_count_message( $q ) ); } + /** + * Data provider for tests which need to run once for each possible value of the fields argument. + * + * @return array Test data. + */ public function data_fields() { return array( 'posts' => array( @@ -1344,6 +1349,12 @@ public function data_fields() { ); } + /** + * Helper method which returns a readable representation of the SQL queries performed. + * + * @param WP_Query $query The current query instance. + * @return string The formatted message. + */ protected static function get_count_message( WP_Query $query ) { return sprintf( "Request SQL:\n\n%s\n\nCount SQL:\n\n%s\n", From 6ed6430422af0e1b05ffe1dde485adcd9ba2fa18 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 30 Mar 2022 00:30:41 +0200 Subject: [PATCH 54/71] Add a test for multiple meta queries. --- tests/phpunit/tests/post/query.php | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index c7d3a704ca9a5..1c5b673e42476 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1250,6 +1250,41 @@ public function test_found_posts_are_correct_for_meta_queries( $fields ) { $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } + /** + * @ticket 47280 + * @dataProvider data_fields + * + * @param string $fields Value of the `fields` argument for `WP_Query`. + */ + public function test_found_posts_are_correct_for_multiple_meta_queries( $fields ) { + self::factory()->post->create_many( 5 ); + $ids = self::factory()->post->create_many( 5 ); + + foreach ( $ids as $id ) { + add_post_meta( $id, 'field_1', 'foo' ); + add_post_meta( $id, 'field_2', 'bar' ); + } + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'meta_query' => array( + array( + 'key' => 'field_1', + ), + array( + 'key' => 'field_2', + ), + ), + ) + ); + + $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); + } + /** * @ticket 47280 * @group ms-required From 2e2ccf7e0bf2a96a32fce05c04e8f5b19ca8bdc5 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 30 Mar 2022 00:42:01 +0200 Subject: [PATCH 55/71] Formatting. --- src/wp-includes/class-wp-query.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 39c74e54a39bd..14339413c6444 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3034,7 +3034,11 @@ public function get_posts() { "; $this->request = $old_request; - $this->count_request = "SELECT COUNT($distinct {$wpdb->posts}.ID) FROM {$wpdb->posts} $join WHERE 1=1 $where"; + $this->count_request = " + SELECT COUNT($distinct {$wpdb->posts}.ID) + FROM {$wpdb->posts} $join + WHERE 1=1 $where + "; if ( ! $q['suppress_filters'] ) { /** From b78e97bedd728348f3bb7bac0fb74ecbed0a0785 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 2 Apr 2022 12:05:01 +0200 Subject: [PATCH 56/71] Implement the deprecated argument message for the `posts_request` filter. --- src/wp-includes/class-wp-query.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 14339413c6444..4443785aebeb8 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -86,7 +86,7 @@ class WP_Query implements JsonSerializable, Serializable { /** * Get post database count query * - * @since xxx + * @since x.x.x * @var string */ public $count_request; @@ -3053,7 +3053,16 @@ public function get_posts() { $this->request = apply_filters_ref_array( 'posts_request', array( $this->request, &$this ) ); if ( false !== strpos( $this->request, 'SQL_CALC_FOUND_ROWS' ) ) { - _deprecated_argument( 'The posts_request filter', 'x.x.x', '...' ); + _deprecated_argument( + 'The posts_request filter', + 'x.x.x', + sprintf( + /* translators: 1: SQL query modifier 2: SQL query */ + __( 'The %1$s query modifier should no longer be added to queries because results are no longer counted with %2$s by default.' ), + 'SQL_CALC_FOUND_ROWS', + 'SELECT FOUND_ROWS()' + ) + ); $this->use_calc_found_rows = true; $this->count_request = 'SELECT FOUND_ROWS()'; From 36bc0a6c79da95a7b9058791b6fe191a96ee4d5d Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 2 Apr 2022 12:17:39 +0200 Subject: [PATCH 57/71] More docs. --- src/wp-includes/class-wp-query.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 4443785aebeb8..cffe4ed103d58 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -474,7 +474,7 @@ class WP_Query implements JsonSerializable, Serializable { private $stopwords; /** - * Undocumented variable + * The LIMIT clause of the query, used only during counting of results. * * @since x.x.x * @var string @@ -3402,7 +3402,10 @@ private function set_found_posts() { * Filters the query to run for retrieving the found posts. * * @since 2.1.0 - * @since x.x.x This query was changed... + * @since x.x.x This query was changed from `SELECT FOUND_ROWS()` to a more + * efficient `COUNT` query which only runs when the `found_posts` + * or `max_num_pages` properties are first accessed. This allows + * for lazy population of the result count. * * @param string $found_posts_query The query to run to find the found posts. * @param WP_Query $query The WP_Query instance (passed by reference). @@ -3426,7 +3429,9 @@ private function set_found_posts() { * Filters the number of found posts for the query. * * @since 2.1.0 - * @since x.x.x This filter now only runs after the value is lazily loaded... + * @since x.x.x By default this filter now only runs when the `found_posts` or + * `max_num_pages` properties are first accessed. This allows for + * lazy population of the result count. * * @param int $found_posts The number of posts found. * @param WP_Query $query The WP_Query instance (passed by reference). @@ -3439,11 +3444,11 @@ private function set_found_posts() { } /** - * Undocumented function + * Stores the LIMIT clause of the query for later use. * * @since x.x.x * - * @param string $limits ... + * @param string $limits The LIMIT clause of the query. */ private function set_limits( $limits ) { $this->limits = $limits; From 146dc6b72175d47fb714186b63b0db0f53ce575a Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 2 Apr 2022 12:46:15 +0100 Subject: [PATCH 58/71] Add a test that covers no results. --- tests/phpunit/tests/post/query.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 1c5b673e42476..1214912874536 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1037,6 +1037,25 @@ public function test_found_posts_are_correct_for_empty_query() { $this->assertSame( 0, $q->max_num_pages ); } + /** + * @ticket 47280 + * @dataProvider data_fields + * + * @param string $fields Value of the `fields` argument for `WP_Query`. + */ + public function test_found_posts_are_correct_for_query_with_no_results( $fields ) { + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_status' => 'draft', + ) + ); + + $this->assertSame( 0, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 0, $q->found_posts, self::get_count_message( $q ) ); + $this->assertSame( 0, $q->max_num_pages, self::get_count_message( $q ) ); + } + /** * @ticket 47280 * @dataProvider data_fields From 6ff250e55cab0c5401cfbc98afeb96c576a52592 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 2 Apr 2022 12:59:18 +0100 Subject: [PATCH 59/71] Add a test that covers the value of the count properties when `no_found_rows` is true. --- tests/phpunit/tests/post/query.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 1214912874536..d8bcf87b70e1e 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1121,6 +1121,27 @@ public function test_found_posts_are_correct_for_query_with_no_paging( $fields ) $this->assertSame( 0, $q->max_num_pages, self::get_count_message( $q ) ); } + /** + * @ticket 47280 + * @dataProvider data_fields + * + * @param string $fields Value of the `fields` argument for `WP_Query`. + */ + public function test_found_posts_are_correct_for_query_with_no_found_rows( $fields ) { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'no_found_rows' => true, + ) + ); + + $this->assertSame( 5, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 0, $q->found_posts, self::get_count_message( $q ) ); + $this->assertSame( 0, $q->max_num_pages, self::get_count_message( $q ) ); + } + /** * @ticket 47280 * @dataProvider data_fields From 46e7466dff1863be55170e2c677f5b342f27908d Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 2 Apr 2022 13:11:38 +0100 Subject: [PATCH 60/71] Better naming. --- tests/phpunit/tests/post/query.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index d8bcf87b70e1e..867fe4107d9a5 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -843,7 +843,7 @@ static function( $request, $query ) { * * @param string $fields Value of the `fields` argument for `WP_Query`. */ - public function test_found_posts_property_is_present_in_php_serialized_query( $fields ) { + public function test_found_posts_property_is_present_when_calculated_after_php_unserialization( $fields ) { self::factory()->post->create_many( 5 ); $q = new WP_Query( @@ -876,7 +876,7 @@ public function test_found_posts_property_is_present_in_php_serialized_query( $f * * @param string $fields Value of the `fields` argument for `WP_Query`. */ - public function test_found_posts_property_is_present_in_php_serialized_query_with_no_found_rows( $fields ) { + public function test_found_posts_property_is_present_when_calculated_after_php_unserialization_with_no_found_rows( $fields ) { self::factory()->post->create_many( 5 ); $q = new WP_Query( @@ -910,7 +910,7 @@ public function test_found_posts_property_is_present_in_php_serialized_query_wit * * @param string $fields Value of the `fields` argument for `WP_Query`. */ - public function test_found_posts_property_is_present_in_json_serialized_query( $fields ) { + public function test_found_posts_property_is_present_when_calculated_after_json_unserialization( $fields ) { self::factory()->post->create_many( 5 ); $q = new WP_Query( @@ -943,7 +943,7 @@ public function test_found_posts_property_is_present_in_json_serialized_query( $ * * @param string $fields Value of the `fields` argument for `WP_Query`. */ - public function test_found_posts_property_is_present_in_json_serialized_query_with_no_found_rows( $fields ) { + public function test_found_posts_property_is_present_when_calculated_after_json_unserialization_with_no_found_rows( $fields ) { self::factory()->post->create_many( 5 ); $q = new WP_Query( From 212e629b954ef8b41518ec7437e81c9d0817ded5 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 2 Apr 2022 13:30:25 +0100 Subject: [PATCH 61/71] Add tests that cover fetching found rows before serialization. --- tests/phpunit/tests/post/query.php | 158 +++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 867fe4107d9a5..d933de194dceb 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -971,6 +971,164 @@ public function test_found_posts_property_is_present_when_calculated_after_json_ $this->assertSame( 0, $unserialized->max_num_pages ); } + /** + * @ticket 47280 + * @dataProvider data_fields + * + * @param string $fields Value of the `fields` argument for `WP_Query`. + */ + public function test_found_posts_property_is_present_when_calculated_before_php_unserialization( $fields ) { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'post_type' => 'post', + ) + ); + + // Fetch found_posts and max_num_pages here so the values are populated when serialized. + $found_posts = $q->found_posts; + $max_num_pages = $q->max_num_pages; + + $serialized = serialize( $q ); + + // Create more matching posts to simulate unserialization occuring at a later date. + self::factory()->post->create_many( 5 ); + + $unserialized = unserialize( $serialized ); + + $this->assertSame( 5, $found_posts ); + $this->assertTrue( property_exists( $unserialized, 'found_posts' ) ); + $this->assertTrue( isset( $unserialized->found_posts ) ); + $this->assertSame( 5, $unserialized->found_posts ); + + $this->assertEquals( 3, $max_num_pages ); + $this->assertTrue( property_exists( $unserialized, 'max_num_pages' ) ); + $this->assertTrue( isset( $unserialized->max_num_pages ) ); + $this->assertEquals( 3, $unserialized->max_num_pages ); + } + + /** + * @ticket 47280 + * @dataProvider data_fields + * + * @param string $fields Value of the `fields` argument for `WP_Query`. + */ + public function test_found_posts_property_is_present_when_calculated_before_php_unserialization_with_no_found_rows( $fields ) { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'post_type' => 'post', + 'no_found_rows' => true, + ) + ); + + // Fetch found_posts and max_num_pages here so the values are populated when serialized. + $found_posts = $q->found_posts; + $max_num_pages = $q->max_num_pages; + + $serialized = serialize( $q ); + + // Create more matching posts to simulate unserialization occuring at a later date. + self::factory()->post->create_many( 5 ); + + $unserialized = unserialize( $serialized ); + + $this->assertSame( 0, $found_posts ); + $this->assertTrue( property_exists( $unserialized, 'found_posts' ) ); + $this->assertTrue( isset( $unserialized->found_posts ) ); + $this->assertSame( 0, $unserialized->found_posts ); + + $this->assertSame( 0, $max_num_pages ); + $this->assertTrue( property_exists( $unserialized, 'max_num_pages' ) ); + $this->assertTrue( isset( $unserialized->max_num_pages ) ); + $this->assertSame( 0, $unserialized->max_num_pages ); + } + + /** + * @ticket 47280 + * @dataProvider data_fields + * + * @param string $fields Value of the `fields` argument for `WP_Query`. + */ + public function test_found_posts_property_is_present_when_calculated_before_json_unserialization( $fields ) { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'post_type' => 'post', + ) + ); + + // Fetch found_posts and max_num_pages here so the values are populated when serialized. + $found_posts = $q->found_posts; + $max_num_pages = $q->max_num_pages; + + $serialized = json_encode( $q ); + + // Create more matching posts to simulate unserialization occuring at a later date. + self::factory()->post->create_many( 5 ); + + $unserialized = json_decode( $serialized ); + + $this->assertSame( 5, $found_posts ); + $this->assertTrue( property_exists( $unserialized, 'found_posts' ) ); + $this->assertTrue( isset( $unserialized->found_posts ) ); + $this->assertSame( 5, $unserialized->found_posts ); + + $this->assertEquals( 3, $max_num_pages ); + $this->assertTrue( property_exists( $unserialized, 'max_num_pages' ) ); + $this->assertTrue( isset( $unserialized->max_num_pages ) ); + $this->assertEquals( 3, $unserialized->max_num_pages ); + } + + /** + * @ticket 47280 + * @dataProvider data_fields + * + * @param string $fields Value of the `fields` argument for `WP_Query`. + */ + public function test_found_posts_property_is_present_when_calculated_before_json_unserialization_with_no_found_rows( $fields ) { + self::factory()->post->create_many( 5 ); + + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'post_type' => 'post', + 'no_found_rows' => true, + ) + ); + + // Fetch found_posts and max_num_pages here so the values are populated when serialized. + $found_posts = $q->found_posts; + $max_num_pages = $q->max_num_pages; + + $serialized = json_encode( $q ); + + // Create more matching posts to simulate unserialization occuring at a later date. + self::factory()->post->create_many( 5 ); + + $unserialized = json_decode( $serialized ); + + $this->assertSame( 0, $found_posts ); + $this->assertTrue( property_exists( $unserialized, 'found_posts' ) ); + $this->assertTrue( isset( $unserialized->found_posts ) ); + $this->assertSame( 0, $unserialized->found_posts ); + + $this->assertSame( 0, $max_num_pages ); + $this->assertTrue( property_exists( $unserialized, 'max_num_pages' ) ); + $this->assertTrue( isset( $unserialized->max_num_pages ) ); + $this->assertSame( 0, $unserialized->max_num_pages ); + } + /** * @ticket 47280 * @dataProvider data_fields From b4e0e5ae62a6813e6f1e1177af63dbe68322bbf2 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 2 Apr 2022 13:33:39 +0100 Subject: [PATCH 62/71] =?UTF-8?q?=C2=AF\=5F(=E3=83=84)=5F/=C2=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/phpunit/tests/post/query.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index d933de194dceb..503e76b4ff186 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -989,7 +989,7 @@ public function test_found_posts_property_is_present_when_calculated_before_php_ ); // Fetch found_posts and max_num_pages here so the values are populated when serialized. - $found_posts = $q->found_posts; + $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; $serialized = serialize( $q ); @@ -1029,7 +1029,7 @@ public function test_found_posts_property_is_present_when_calculated_before_php_ ); // Fetch found_posts and max_num_pages here so the values are populated when serialized. - $found_posts = $q->found_posts; + $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; $serialized = serialize( $q ); @@ -1068,7 +1068,7 @@ public function test_found_posts_property_is_present_when_calculated_before_json ); // Fetch found_posts and max_num_pages here so the values are populated when serialized. - $found_posts = $q->found_posts; + $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; $serialized = json_encode( $q ); @@ -1108,7 +1108,7 @@ public function test_found_posts_property_is_present_when_calculated_before_json ); // Fetch found_posts and max_num_pages here so the values are populated when serialized. - $found_posts = $q->found_posts; + $found_posts = $q->found_posts; $max_num_pages = $q->max_num_pages; $serialized = json_encode( $q ); From df1db9d6095f85f58393296a913ee8bcb0b73415 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 2 Apr 2022 20:54:54 +0100 Subject: [PATCH 63/71] Revert changes in other query classes for now to keep the `WP_Query` changes clean. --- src/wp-includes/class-wp-comment-query.php | 4 ++-- src/wp-includes/class-wp-network-query.php | 4 ++-- src/wp-includes/class-wp-site-query.php | 4 ++-- src/wp-includes/class-wp-user-query.php | 13 +++++++------ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/class-wp-comment-query.php b/src/wp-includes/class-wp-comment-query.php index 11aba2f63e277..bf3db0ecf35b0 100644 --- a/src/wp-includes/class-wp-comment-query.php +++ b/src/wp-includes/class-wp-comment-query.php @@ -183,7 +183,7 @@ public function __call( $name, $arguments ) { * When used with `$offset`, `$offset` takes precedence. Default 1. * @type int $offset Number of comments to offset the query. Used to build * LIMIT clause. Default 0. - * @type bool $no_found_rows Whether to disable the query to count found rows. + * @type bool $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query. * Default: true. * @type string|array $orderby Comment status or array of statuses. To use 'meta_value' * or 'meta_value_num', `$meta_key` must also be defined. @@ -952,7 +952,7 @@ protected function get_comment_ids() { $found_rows = ''; if ( ! $this->query_vars['no_found_rows'] ) { - $found_rows = 'SQL_CALC_FOUND_ROWS'; // @TODO + $found_rows = 'SQL_CALC_FOUND_ROWS'; } $this->sql_clauses['select'] = "SELECT $found_rows $fields"; diff --git a/src/wp-includes/class-wp-network-query.php b/src/wp-includes/class-wp-network-query.php index edad5f20bbf25..429143976ca04 100644 --- a/src/wp-includes/class-wp-network-query.php +++ b/src/wp-includes/class-wp-network-query.php @@ -98,7 +98,7 @@ class WP_Network_Query { * @type int $number Maximum number of networks to retrieve. Default empty (no limit). * @type int $offset Number of networks to offset the query. Used to build LIMIT clause. * Default 0. - * @type bool $no_found_rows Whether to disable the query to count found rows. Default true. + * @type bool $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query. Default true. * @type string|array $orderby Network status or array of statuses. Accepts 'id', 'domain', 'path', * 'domain_length', 'path_length' and 'network__in'. Also accepts false, * an empty array, or 'none' to disable `ORDER BY` clause. Default 'id'. @@ -471,7 +471,7 @@ protected function get_network_ids() { $found_rows = ''; if ( ! $this->query_vars['no_found_rows'] ) { - $found_rows = 'SQL_CALC_FOUND_ROWS'; // @TODO + $found_rows = 'SQL_CALC_FOUND_ROWS'; } $this->sql_clauses['select'] = "SELECT $found_rows $fields"; diff --git a/src/wp-includes/class-wp-site-query.php b/src/wp-includes/class-wp-site-query.php index bf7b184362111..1a4df65d99dd4 100644 --- a/src/wp-includes/class-wp-site-query.php +++ b/src/wp-includes/class-wp-site-query.php @@ -127,7 +127,7 @@ class WP_Site_Query { * @type int $number Maximum number of sites to retrieve. Default 100. * @type int $offset Number of sites to offset the query. Used to build LIMIT clause. * Default 0. - * @type bool $no_found_rows Whether to disable the query to count found rows. Default true. + * @type bool $no_found_rows Whether to disable the `SQL_CALC_FOUND_ROWS` query. Default true. * @type string|array $orderby Site status or array of statuses. Accepts: * - 'id' * - 'domain' @@ -674,7 +674,7 @@ protected function get_site_ids() { $found_rows = ''; if ( ! $this->query_vars['no_found_rows'] ) { - $found_rows = 'SQL_CALC_FOUND_ROWS'; // @TODO + $found_rows = 'SQL_CALC_FOUND_ROWS'; } $this->sql_clauses['select'] = "SELECT $found_rows $fields"; diff --git a/src/wp-includes/class-wp-user-query.php b/src/wp-includes/class-wp-user-query.php index 42ba79c07387a..a757e38e20969 100644 --- a/src/wp-includes/class-wp-user-query.php +++ b/src/wp-includes/class-wp-user-query.php @@ -289,6 +289,10 @@ public function prepare_query( $query = array() ) { $this->query_fields = "$wpdb->users.ID"; } + if ( isset( $qv['count_total'] ) && $qv['count_total'] ) { + $this->query_fields = 'SQL_CALC_FOUND_ROWS ' . $this->query_fields; + } + $this->query_from = "FROM $wpdb->users"; $this->query_where = 'WHERE 1=1'; @@ -781,12 +785,8 @@ public function query() { } if ( isset( $qv['count_total'] ) && $qv['count_total'] ) { - /** - * Return a total count of users. - * Historically this ran `SELECT FOUND_ROWS()` after issuing the main query, - * but since `SQL_CALC_FOUND_ROWS` is now deprecated from MySQL, it instead repeats - * the same query with `COUNT(*)` instead of `$this->query_fields`. + * Filters SELECT FOUND_ROWS() query for the current WP_User_Query instance. * * @since 3.2.0 * @since 5.1.0 Added the `$this` parameter. @@ -796,7 +796,8 @@ public function query() { * @param string $sql The SELECT FOUND_ROWS() query for the current WP_User_Query. * @param WP_User_Query $query The current WP_User_Query instance. */ - $found_users_query = apply_filters( 'found_users_query', "SELECT COUNT(*) $this->query_from $this->query_where", $this ); + $found_users_query = apply_filters( 'found_users_query', 'SELECT FOUND_ROWS()', $this ); + $this->total_users = (int) $wpdb->get_var( $found_users_query ); } } From e7623ed0e74bb5cd8af79c87bb1c0872fc0d4539 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 2 Apr 2022 20:54:59 +0100 Subject: [PATCH 64/71] More docs. --- src/wp-includes/class-wp-query.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index cffe4ed103d58..032ab2801de04 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -189,7 +189,9 @@ class WP_Query implements JsonSerializable, Serializable { private $max_num_pages = 0; /** - * Undocumented variable + * Whether the SQL query to count the results should use `SQL_CALC_FOUND_ROWS`. + * + * Used only as a fallback if a filter on the main query reinstates `SELECT FOUND_ROWS()`. * * @since x.x.x * From 3041fde071824a08fde675f6e203d25719e02632 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 23 Jun 2022 21:33:26 +0100 Subject: [PATCH 65/71] More tidying up of SQL in test failure messages. --- tests/phpunit/tests/post/query.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 503e76b4ff186..3d1d4f0751966 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1603,11 +1603,14 @@ protected static function get_count_message( WP_Query $query ) { * @return string The formatted SQL query. */ protected static function format_sql( $sql ) { - return preg_replace( + $sql = preg_replace( '# (FROM|INNER JOIN|LEFT JOIN|ON|WHERE|AND|GROUP BY|ORDER BY|LIMIT) #', "\n\$1 ", $sql ); + $sql = preg_replace( '#^\t+#m', '', $sql ); + + return $sql; } } From 5ad1aff534ad0f31727ef2622b577ef50b94ce92 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 3 Jul 2022 01:45:14 +0100 Subject: [PATCH 66/71] This can be null too. --- src/wp-includes/class-wp-query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index a5c1b24747bda..0ee6e7a831401 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -174,7 +174,7 @@ class WP_Query implements JsonSerializable, Serializable { * @since 2.1.0 * @since x.x.x This value is now lazily loaded only when it's needed. * - * @var int + * @var int|null */ private $found_posts = null; From 47f5c6fd79cb13bc2f7348f65e9e16d2d6c1784b Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 14 Aug 2022 01:56:47 +0100 Subject: [PATCH 67/71] Always count distinct post IDs. This ensures the count for meta queries with an `OR` relationship is accurate. --- src/wp-includes/class-wp-query.php | 2 +- tests/phpunit/tests/post/query.php | 41 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 74bab1a194317..5eed0ac25bb41 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3068,7 +3068,7 @@ public function get_posts() { $this->request = $old_request; $this->count_request = " - SELECT COUNT($distinct {$wpdb->posts}.ID) + SELECT COUNT(DISTINCT {$wpdb->posts}.ID) FROM {$wpdb->posts} $join WHERE 1=1 $where "; diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 3d1d4f0751966..97bb91109a5f5 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1483,6 +1483,47 @@ public function test_found_posts_are_correct_for_multiple_meta_queries( $fields $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } + /** + * @ticket 47280 + * @dataProvider data_fields + * + * @param string $fields Value of the `fields` argument for `WP_Query`. + */ + public function test_found_posts_are_correct_for_OR_meta_queries( $fields ) { + self::factory()->post->create_many( 5 ); + $ids = self::factory()->post->create_many( 5 ); + + // Add a mixture of meta values so all 5 posts match the meta query. + add_post_meta( $ids[0], 'field_1', 'foo' ); + add_post_meta( $ids[0], 'field_2', 'bar' ); + add_post_meta( $ids[1], 'field_1', 'foo' ); + add_post_meta( $ids[2], 'field_1', 'foo' ); + add_post_meta( $ids[3], 'field_2', 'bar' ); + add_post_meta( $ids[4], 'field_2', 'bar' ); + + // This query results in a `GROUP BY wp_posts.ID` clause, which means the + // count query must count DISTINCT post IDs to eliminate duplicate posts. + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => 'field_1', + ), + array( + 'key' => 'field_2', + ), + ), + ) + ); + + $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); + $this->assertSame( 5, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); + } + /** * @ticket 47280 * @group ms-required From b9bddc4dd5073235e13709f4ff816fdc5802de39 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 14 Aug 2022 02:16:16 +0100 Subject: [PATCH 68/71] Account for a custom `GROUP BY` field or fields. --- src/wp-includes/class-wp-query.php | 5 +++- tests/phpunit/tests/post/query.php | 47 +++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 5eed0ac25bb41..8290c578d758a 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3050,7 +3050,10 @@ public function get_posts() { $limits = isset( $clauses['limits'] ) ? $clauses['limits'] : ''; } + $count_field = "{$wpdb->posts}.ID"; + if ( ! empty( $groupby ) ) { + $count_field = $groupby; $groupby = 'GROUP BY ' . $groupby; } if ( ! empty( $orderby ) ) { @@ -3068,7 +3071,7 @@ public function get_posts() { $this->request = $old_request; $this->count_request = " - SELECT COUNT(DISTINCT {$wpdb->posts}.ID) + SELECT COUNT(DISTINCT {$count_field}) FROM {$wpdb->posts} $join WHERE 1=1 $where "; diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 97bb91109a5f5..3a6782550a6d0 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1502,7 +1502,7 @@ public function test_found_posts_are_correct_for_OR_meta_queries( $fields ) { add_post_meta( $ids[4], 'field_2', 'bar' ); // This query results in a `GROUP BY wp_posts.ID` clause, which means the - // count query must count DISTINCT post IDs to eliminate duplicate posts. + // count query must count `DISTINCT wp_posts.ID` to eliminate duplicates. $q = new WP_Query( array( 'fields' => $fields, @@ -1524,6 +1524,51 @@ public function test_found_posts_are_correct_for_OR_meta_queries( $fields ) { $this->assertEquals( 3, $q->max_num_pages, self::get_count_message( $q ) ); } + /** + * @ticket 47280 + * @dataProvider data_fields + * + * @param string $fields Value of the `fields` argument for `WP_Query`. + */ + public function test_found_posts_are_correct_for_group_by_queries( $fields ) { + $user1 = self::factory()->user->create(); + $user2 = self::factory()->user->create(); + + self::factory()->post->create_many( + 5, + array( + 'post_author' => $user1, + ) + ); + self::factory()->post->create_many( + 5, + array( + 'post_author' => $user2, + ) + ); + + /** + * Adds a GROUP BY clause to the query. + */ + add_filter( 'posts_groupby_request', function() { + return "{$GLOBALS['wpdb']->posts}.post_author"; + } ); + + // This query results in a `GROUP BY wp_posts.post_author` clause, which means the + // count query must count `DISTINCT wp_posts.post_author` to eliminate duplicates. + $q = new WP_Query( + array( + 'fields' => $fields, + 'posts_per_page' => 2, + ) + ); + + $this->assertSame( 2, $q->post_count, self::get_count_message( $q ) ); + // There is a total of two distinct authors in the results. + $this->assertSame( 2, $q->found_posts, self::get_count_message( $q ) ); + $this->assertEquals( 1, $q->max_num_pages, self::get_count_message( $q ) ); + } + /** * @ticket 47280 * @group ms-required From abe2176f01a5b88c8561bc6fb7024bc9edb6a3f4 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 14 Aug 2022 02:19:03 +0100 Subject: [PATCH 69/71] Coding standards. --- src/wp-includes/class-wp-query.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 8290c578d758a..1bb3dca12bfbc 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -3054,6 +3054,7 @@ public function get_posts() { if ( ! empty( $groupby ) ) { $count_field = $groupby; + $groupby = 'GROUP BY ' . $groupby; } if ( ! empty( $orderby ) ) { From bbedca0c3a6dc61995320b568029aa0dbf1cbf00 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 14 Aug 2022 02:26:04 +0100 Subject: [PATCH 70/71] My coding is not up to standards. --- tests/phpunit/tests/post/query.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 3a6782550a6d0..4f748ac03920c 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1550,9 +1550,12 @@ public function test_found_posts_are_correct_for_group_by_queries( $fields ) { /** * Adds a GROUP BY clause to the query. */ - add_filter( 'posts_groupby_request', function() { - return "{$GLOBALS['wpdb']->posts}.post_author"; - } ); + add_filter( + 'posts_groupby_request', + function() { + return "{$GLOBALS['wpdb']->posts}.post_author"; + } + ); // This query results in a `GROUP BY wp_posts.post_author` clause, which means the // count query must count `DISTINCT wp_posts.post_author` to eliminate duplicates. From d537eabea2878787acfa9f5d63c4f83e1730ee67 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 18 Jan 2023 11:18:25 +0000 Subject: [PATCH 71/71] Avoid a deprecated error in PHP 8.1. --- src/wp-includes/class-wp-query.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index b970eb461a42f..b1d75838682c9 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -4099,6 +4099,7 @@ public function __unserialize( $data ) { // phpcs:ignore PHPCompatibility.Functi * * @return array The properties of the object as an associative array. */ + #[ReturnTypeWillChange] public function jsonSerialize() { $this->set_found_posts();