diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php
index a6b105ef2877c..b1d75838682c9 100644
--- a/src/wp-includes/class-wp-query.php
+++ b/src/wp-includes/class-wp-query.php
@@ -16,7 +16,7 @@
* @since 4.5.0 Removed the `$comments_popup` property.
*/
#[AllowDynamicProperties]
-class WP_Query {
+class WP_Query implements JsonSerializable, Serializable {
/**
* Query vars set by the user.
@@ -84,6 +84,14 @@ class WP_Query {
*/
public $request;
+ /**
+ * Get post database count query
+ *
+ * @since x.x.x
+ * @var string
+ */
+ public $count_request;
+
/**
* Array of post objects or post IDs.
*
@@ -165,17 +173,32 @@ class WP_Query {
* If limit clause was not used, equals $post_count.
*
* @since 2.1.0
- * @var int
+ * @since x.x.x This value is now lazily loaded only when it's needed.
+ *
+ * @var int|null
*/
- public $found_posts = 0;
+ private $found_posts = null;
/**
* 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;
+
+ /**
+ * 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
+ *
+ * @var bool
+ */
+ private $use_calc_found_rows = false;
/**
* The number of comment pages.
@@ -461,6 +484,14 @@ class WP_Query {
*/
private $stopwords;
+ /**
+ * The LIMIT clause of the query, used only during counting of results.
+ *
+ * @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' );
@@ -509,21 +540,21 @@ 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 = 0;
+ $this->found_posts = null;
$this->max_num_pages = 0;
$this->max_num_comment_pages = 0;
@@ -1975,7 +2006,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 {
@@ -3044,20 +3075,19 @@ 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 ) ) {
$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
@@ -3066,17 +3096,39 @@ public function get_posts() {
";
$this->request = $old_request;
+ $this->count_request = "
+ SELECT COUNT(DISTINCT {$count_field})
+ FROM {$wpdb->posts} $join
+ WHERE 1=1 $where
+ ";
if ( ! $q['suppress_filters'] ) {
/**
* Filters the completed SQL query before sending.
*
* @since 2.0.0
+ * @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' ) ) {
+ _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()';
+ }
}
/**
@@ -3173,7 +3225,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->set_limits( $limits );
if ( $q['cache_results'] && $id_query_is_cacheable ) {
$cache_value = array(
@@ -3194,7 +3246,7 @@ public function get_posts() {
}
$this->post_count = count( $this->posts );
- $this->set_found_posts( $q, $limits );
+ $this->set_limits( $limits );
/** @var int[] */
$post_parents = array();
@@ -3242,7 +3294,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
@@ -3254,6 +3306,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.
@@ -3264,14 +3317,14 @@ public function get_posts() {
if ( $post_ids ) {
$this->posts = $post_ids;
- $this->set_found_posts( $q, $limits );
+ $this->set_limits( $limits );
_prime_post_caches( $post_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->set_limits( $limits );
}
}
@@ -3502,18 +3555,24 @@ public function get_posts() {
* for the current query.
*
* @since 3.5.0
+ * @since x.x.x The `$q` and `$limits` parameters were removed.
*
* @global wpdb $wpdb WordPress database abstraction object.
- *
- * @param array $q Query variables.
- * @param string $limits LIMIT clauses of the query.
*/
- private function set_found_posts( $q, $limits ) {
+ private function set_found_posts() {
global $wpdb;
+ if ( null !== $this->found_posts ) {
+ return;
+ }
+
+ $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.
- if ( $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;
}
@@ -3522,11 +3581,15 @@ 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 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).
*/
- $found_posts_query = apply_filters_ref_array( 'found_posts_query', array( 'SELECT FOUND_ROWS()', &$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 {
@@ -3545,6 +3608,9 @@ private function set_found_posts( $q, $limits ) {
* Filters the number of found posts for the query.
*
* @since 2.1.0
+ * @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).
@@ -3556,6 +3622,21 @@ private function set_found_posts( $q, $limits ) {
}
}
+ /**
+ * Stores the LIMIT clause of the query for later use.
+ *
+ * @since x.x.x
+ *
+ * @param string $limits The LIMIT clause of the query.
+ */
+ 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.
*
@@ -3883,21 +3964,48 @@ 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;
}
}
/**
- * Make private properties checkable for backward compatibility.
+ * 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.
*
* @since 4.0.0
*
@@ -3905,6 +4013,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 );
}
@@ -3926,6 +4038,74 @@ public function __call( $name, $arguments ) {
return false;
}
+ /**
+ * Controls how the object is represented during PHP serialization.
+ *
+ * @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() { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound
+ $this->set_found_posts();
+
+ 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 ) { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound
+ foreach ( $data as $key => $value ) {
+ $this->$key = $value;
+ }
+ }
+
+ /**
+ * Controls how the object is represented during JSON serialization.
+ *
+ * @since x.x.x
+ *
+ * @return array The properties of the object as an associative array.
+ */
+ #[ReturnTypeWillChange]
+ 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 66bcdc26915eb..72557b29aafc4 100644
--- a/tests/phpunit/tests/post/query.php
+++ b/tests/phpunit/tests/post/query.php
@@ -777,4 +777,948 @@ public function test_found_posts_should_be_integer_even_if_found_posts_filter_re
$this->assertIsInt( $q->found_posts );
}
+
+ /**
+ * @ticket 47280
+ * @dataProvider data_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(
+ array(
+ 'fields' => $fields,
+ 'post_type' => 'post',
+ 'no_found_rows' => false,
+ )
+ );
+
+ $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->assertTrue( isset( $q->max_num_pages ) );
+ $this->assertNotNull( $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_property_is_present_with_no_found_rows_true( $fields ) {
+ $q = new WP_Query(
+ array(
+ 'fields' => $fields,
+ 'post_type' => 'post',
+ 'no_found_rows' => true,
+ )
+ );
+
+ $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->assertTrue( isset( $q->max_num_pages ) );
+ $this->assertNotNull( $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_property_can_be_set_externally( $fields ) {
+ add_filter(
+ 'posts_request',
+ static 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 data_fields
+ *
+ * @param string $fields Value of the `fields` argument for `WP_Query`.
+ */
+ public function test_found_posts_property_is_present_when_calculated_after_php_unserialization( $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 data_fields
+ *
+ * @param string $fields Value of the `fields` argument for `WP_Query`.
+ */
+ 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(
+ 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 data_fields
+ *
+ * @param string $fields Value of the `fields` argument for `WP_Query`.
+ */
+ public function test_found_posts_property_is_present_when_calculated_after_json_unserialization( $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 data_fields
+ *
+ * @param string $fields Value of the `fields` argument for `WP_Query`.
+ */
+ 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(
+ 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 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
+ *
+ * @param string $fields Value of the `fields` argument for `WP_Query`.
+ */
+ public function test_found_posts_should_be_lazily_loaded( $fields ) {
+ global $wpdb;
+
+ self::factory()->post->create_many( 5 );
+
+ $start = $wpdb->num_queries;
+
+ $q = new WP_Query(
+ array(
+ 'fields' => $fields,
+ 'posts_per_page' => 2,
+ 'update_post_term_cache' => false,
+ 'update_post_meta_cache' => false,
+ )
+ );
+
+ // Count the queries
+ $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;
+
+ // 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.
+ $after_found_posts_again = ( $wpdb->num_queries - $start );
+
+ // Ensure the posts were not initially counted.
+ $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 );
+
+ // 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
+ */
+ 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->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
+ *
+ * @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 );
+
+ $q = new WP_Query(
+ array(
+ 'fields' => $fields,
+ 'posts_per_page' => 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
+ * @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_limit( $fields ) {
+ self::factory()->post->create_many( 5 );
+
+ $q = new WP_Query(
+ array(
+ 'fields' => $fields,
+ '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
+ * @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_paging( $fields ) {
+ self::factory()->post->create_many( 5 );
+
+ $q = new WP_Query(
+ array(
+ 'fields' => $fields,
+ '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
+ * @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
+ *
+ * @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 );
+
+ $q = new WP_Query(
+ array(
+ 'fields' => $fields,
+ 'posts_per_page' => 2,
+ 'paged' => 3,
+ )
+ );
+
+ $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 ) );
+ }
+
+ /**
+ * @ticket 47280
+ * @dataProvider data_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();
+ self::factory()->post->create_many( 5 );
+ self::factory()->post->create_many(
+ 5,
+ array(
+ 'post_author' => $author,
+ )
+ );
+
+ $q = new WP_Query(
+ array(
+ 'fields' => $fields,
+ 'posts_per_page' => 2,
+ 'author' => $author,
+ )
+ );
+
+ $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 ) );
+ }
+
+ /**
+ * 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 data_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';
+ 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 data_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();
+ self::factory()->post->create_many( 5 );
+ $ids = self::factory()->post->create_many( 5 );
+
+ foreach ( $ids as $id ) {
+ wp_set_post_terms( $id, $term->slug );
+ }
+
+ $q = new WP_Query(
+ array(
+ 'fields' => $fields,
+ 'posts_per_page' => 2,
+ 'tag' => $term->slug,
+ )
+ );
+
+ $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 data_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 );
+ $ids = self::factory()->post->create_many( 5 );
+
+ foreach ( $ids as $id ) {
+ add_post_meta( $id, 'my_meta', 'foo' );
+ }
+
+ $q = new WP_Query(
+ array(
+ 'fields' => $fields,
+ 'posts_per_page' => 2,
+ 'meta_key' => 'my_meta',
+ )
+ );
+
+ $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 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
+ * @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 wp_posts.ID` to eliminate duplicates.
+ $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
+ * @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
+ * @dataProvider data_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();
+ 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
+ */
+ 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',
+ static 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,
+ )
+ );
+
+ // 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 ) );
+ $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(
+ '',
+ ),
+ 'ids' => array(
+ 'ids',
+ ),
+ 'parents' => array(
+ 'id=>parent',
+ ),
+ );
+ }
+
+ /**
+ * 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",
+ 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 The SQL query to be formatted.
+ * @return string The formatted SQL query.
+ */
+ protected static function format_sql( $sql ) {
+ $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;
+ }
+
}
diff --git a/tests/phpunit/tests/query/noFoundRows.php b/tests/phpunit/tests/query/noFoundRows.php
index 7b4f66a416030..39544de91358f 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(
@@ -11,9 +15,12 @@ public function test_no_found_rows_default() {
)
);
- $this->assertStringContainsString( 'SQL_CALC_FOUND_ROWS', $q->request );
+ $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $q->request );
}
+ /**
+ * @ticket 47280
+ */
public function test_no_found_rows_false() {
$q = new WP_Query(
array(
@@ -22,9 +29,12 @@ public function test_no_found_rows_false() {
)
);
- $this->assertStringContainsString( 'SQL_CALC_FOUND_ROWS', $q->request );
+ $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $q->request );
}
+ /**
+ * @ticket 47280
+ */
public function test_no_found_rows_0() {
$q = new WP_Query(
array(
@@ -33,9 +43,12 @@ public function test_no_found_rows_0() {
)
);
- $this->assertStringContainsString( 'SQL_CALC_FOUND_ROWS', $q->request );
+ $this->assertStringNotContainsString( 'SQL_CALC_FOUND_ROWS', $q->request );
}
+ /**
+ * @ticket 47280
+ */
public function test_no_found_rows_empty_string() {
$q = new WP_Query(
array(
@@ -44,9 +57,12 @@ public function test_no_found_rows_empty_string() {
)
);
- $this->assertStringContainsString( 'SQL_CALC_FOUND_ROWS', $q->request );
+ $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 = self::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 = self::factory()->post->create();