diff --git a/includes/query/class-tmdo-post-query-router.php b/includes/query/class-tmdo-post-query-router.php index acdb148..b05e5cb 100644 --- a/includes/query/class-tmdo-post-query-router.php +++ b/includes/query/class-tmdo-post-query-router.php @@ -433,8 +433,11 @@ class TMDO_Post_Query_Router { global $wpdb; list( $count_sql, $excluded_sql ) = array_pad( (array) $count_sql, 2, '' ); - $key = md5( $count_sql ) . ':' . wp_cache_get_last_changed( 'posts' ); - $total = wp_cache_get( $key, self::COUNT_CACHE_GROUP ); + // Keyword searches carry wpdb's placeholder escape for "%", which is new + // in every request: without it the key would never match again. + $stable = method_exists( $wpdb, 'remove_placeholder_escape' ) ? $wpdb->remove_placeholder_escape( $count_sql ) : $count_sql; + $key = md5( $stable ) . ':' . wp_cache_get_last_changed( 'posts' ); + $total = wp_cache_get( $key, self::COUNT_CACHE_GROUP ); if ( false === $total ) { $total = (int) $wpdb->get_var( $count_sql ); wp_cache_set( $key, $total, self::COUNT_CACHE_GROUP, HOUR_IN_SECONDS ); diff --git a/tests/integration/PostQueryRouterFoundRowsTest.php b/tests/integration/PostQueryRouterFoundRowsTest.php index a374e91..c593b65 100644 --- a/tests/integration/PostQueryRouterFoundRowsTest.php +++ b/tests/integration/PostQueryRouterFoundRowsTest.php @@ -157,6 +157,49 @@ class PostQueryRouterFoundRowsTest extends TestCase { $this->assertSame( '0', $count( '2' ), 'recounted' ); } + /** + * Keyword searches carry wpdb's placeholder escape for "%", which changes + * with every request: the cache key uses the SQL without it, or no search + * with a keyword ever hits the cached count. + */ + public function test_the_cached_count_survives_a_new_placeholder_escape(): void { + global $wpdb; + $real = $wpdb; + $wpdb = new class( $real ) { + public int $counts = 0; + + public function __construct( private object $db ) {} + + public function remove_placeholder_escape( string $sql ): string { + return (string) preg_replace( '/\{[0-9a-f]{8}\}/', '%', $sql ); + } + + public function __call( string $name, array $args ) { + if ( 'get_var' === $name && str_contains( (string) $args[0], 'COUNT(*)' ) ) { + ++$this->counts; + } + return $this->db->$name( ...$args ); + } + + public function __get( string $name ) { + return $this->db->$name; + } + }; + try { + foreach ( array( '{1a2b3c4d}', '{9f8e7d6c}' ) as $escape ) { // Two requests. + list( $router, $query, $request ) = $this->routed(); + $request = str_replace( ' GROUP BY', " AND '{$escape}' = '{$escape}' GROUP BY", $request ); + $router->posts_request( $request, $query ); + $this->assertSame( '2', $real->get_var( $router->found_posts_query( 'SELECT FOUND_ROWS()', $query ) ) ); + } + $counts = $wpdb->counts; + } finally { + $wpdb = $real; + } + + $this->assertSame( 1, $counts, 'counted once' ); + } + public function test_flat_writes_and_deletes_move_posts_last_changed(): void { $GLOBALS['_wp_current_filter'] = 'update_post_metadata'; $before = wp_cache_get_last_changed( 'posts' );