Skip to content

Commit 6e3d8eb

Browse files
ellatrixclaude
andcommitted
Support per-post-type invalidates_query_cache behavior
Use get_post_type() in wp_cache_set_posts_last_changed to check the exact subtype when deciding whether to skip cache invalidation. Extract post types from the parent WP_Query context in WP_Meta_Query so that a key registered as non-cacheable for one post type is still allowed in queries targeting a different post type. Adds tests for per-subtype behavior: matching post type, different post type, array of post types, and cache invalidation with mismatched post type. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent dee022f commit 6e3d8eb

3 files changed

Lines changed: 141 additions & 7 deletions

File tree

src/wp-includes/class-wp-meta-query.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ class WP_Meta_Query {
103103
*/
104104
protected $object_type = '';
105105

106+
/**
107+
* Object subtypes for the query (e.g. post types).
108+
*
109+
* @since 7.0.0
110+
* @var string[]
111+
*/
112+
protected $object_subtypes = array();
113+
106114
/**
107115
* Constructor.
108116
*
@@ -374,6 +382,17 @@ public function get_sql( $type, $primary_table, $primary_id_column, $context = n
374382
$this->meta_id_column = sanitize_key( $type . '_id' );
375383
$this->object_type = $type;
376384

385+
// Extract object subtypes from the parent query context.
386+
$this->object_subtypes = array();
387+
if ( 'post' === $type && $context instanceof WP_Query && ! empty( $context->query_vars['post_type'] ) ) {
388+
$post_type = $context->query_vars['post_type'];
389+
if ( is_array( $post_type ) ) {
390+
$this->object_subtypes = $post_type;
391+
} elseif ( 'any' !== $post_type ) {
392+
$this->object_subtypes = array( $post_type );
393+
}
394+
}
395+
377396
$this->primary_table = $primary_table;
378397
$this->primary_id_column = $primary_id_column;
379398

@@ -546,7 +565,7 @@ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' )
546565
if ( $this->object_type
547566
&& isset( $clause['key'] )
548567
&& is_string( $clause['key'] )
549-
&& ! wp_meta_key_invalidates_query_cache( $this->object_type, $clause['key'] )
568+
&& $this->is_non_cacheable_meta_key( $clause['key'] )
550569
) {
551570
_doing_it_wrong(
552571
__METHOD__,
@@ -838,6 +857,33 @@ public function get_clauses() {
838857
return $this->clauses;
839858
}
840859

860+
/**
861+
* Checks whether a meta key is registered as non-query-cacheable.
862+
*
863+
* When object subtypes are known (e.g. post types from the parent WP_Query),
864+
* checks each subtype individually. If any subtype has the key registered as
865+
* non-cacheable, returns true. Falls back to searching across all subtypes
866+
* when no subtypes are available.
867+
*
868+
* @since 7.0.0
869+
*
870+
* @param string $meta_key Meta key to check.
871+
* @return bool True if the meta key is non-cacheable, false otherwise.
872+
*/
873+
protected function is_non_cacheable_meta_key( $meta_key ) {
874+
if ( ! empty( $this->object_subtypes ) ) {
875+
foreach ( $this->object_subtypes as $subtype ) {
876+
if ( ! wp_meta_key_invalidates_query_cache( $this->object_type, $meta_key, $subtype ) ) {
877+
return true;
878+
}
879+
}
880+
return false;
881+
}
882+
883+
// No subtypes available — fall back to checking all subtypes.
884+
return ! wp_meta_key_invalidates_query_cache( $this->object_type, $meta_key );
885+
}
886+
841887
/**
842888
* Identifies an existing table alias that is compatible with the current
843889
* query clause.

src/wp-includes/post.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8469,8 +8469,11 @@ function wp_add_trashed_suffix_to_post_name_for_post( $post ) {
84698469
* @param string $meta_key Optional. Meta key. Passed by meta action hooks.
84708470
*/
84718471
function wp_cache_set_posts_last_changed( $meta_id = 0, $object_id = 0, $meta_key = '' ) {
8472-
if ( $meta_key && ! wp_meta_key_invalidates_query_cache( 'post', $meta_key ) ) {
8473-
return;
8472+
if ( $meta_key ) {
8473+
$post_type = $object_id ? get_post_type( $object_id ) : '';
8474+
if ( ! wp_meta_key_invalidates_query_cache( 'post', $meta_key, $post_type ? $post_type : '' ) ) {
8475+
return;
8476+
}
84748477
}
84758478
wp_cache_set_last_changed( 'posts' );
84768479
}

tests/phpunit/tests/meta/invalidatesQueryCache.php

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static function wpTearDownAfterClass() {
2222
public function tear_down() {
2323
unregister_meta_key( 'post', 'nocache_meta' );
2424
unregister_meta_key( 'post', 'nocache_meta', 'post' );
25+
unregister_meta_key( 'post', 'nocache_meta', 'page' );
2526
unregister_meta_key( 'post', 'normal_meta' );
2627
parent::tear_down();
2728
}
@@ -216,17 +217,21 @@ public function test_subtype_registration_skips_cache_invalidation() {
216217
}
217218

218219
/**
219-
* A key registered for a specific post type should be refused in meta queries.
220+
* A key registered for a specific post type should be refused in meta queries
221+
* when the WP_Query targets that post type.
220222
*
221223
* @expectedIncorrectUsage WP_Meta_Query::get_sql_for_clause
222224
*/
223-
public function test_subtype_registration_refuses_meta_query() {
225+
public function test_subtype_registration_refuses_meta_query_for_matching_post_type() {
224226
register_post_meta(
225227
'post',
226228
'nocache_meta',
227229
array( 'invalidates_query_cache' => false )
228230
);
229231

232+
$query = new WP_Query();
233+
$query->query_vars['post_type'] = 'post';
234+
230235
$meta_query = new WP_Meta_Query(
231236
array(
232237
array(
@@ -236,8 +241,88 @@ public function test_subtype_registration_refuses_meta_query() {
236241
)
237242
);
238243

239-
$sql = $meta_query->get_sql( 'post', 'wp_posts', 'ID' );
244+
$sql = $meta_query->get_sql( 'post', 'wp_posts', 'ID', $query );
240245

241-
$this->assertStringNotContainsString( 'nocache_meta', $sql['where'], 'Non-cacheable meta key should not appear in WHERE clause.' );
246+
$this->assertStringNotContainsString( 'nocache_meta', $sql['where'], 'Non-cacheable meta key should not appear in WHERE clause for matching post type.' );
247+
}
248+
249+
/**
250+
* A key registered as non-cacheable for one post type should be allowed
251+
* in meta queries targeting a different post type.
252+
*/
253+
public function test_subtype_registration_allows_meta_query_for_different_post_type() {
254+
register_post_meta(
255+
'page',
256+
'nocache_meta',
257+
array( 'invalidates_query_cache' => false )
258+
);
259+
260+
$query = new WP_Query();
261+
$query->query_vars['post_type'] = 'post';
262+
263+
$meta_query = new WP_Meta_Query(
264+
array(
265+
array(
266+
'key' => 'nocache_meta',
267+
'value' => 'test',
268+
),
269+
)
270+
);
271+
272+
$sql = $meta_query->get_sql( 'post', 'wp_posts', 'ID', $query );
273+
274+
$this->assertStringContainsString( 'nocache_meta', $sql['where'], 'Meta key should be allowed for a post type where it is cacheable.' );
275+
}
276+
277+
/**
278+
* When querying multiple post types, if the key is non-cacheable on any
279+
* of them, the clause should be refused.
280+
*
281+
* @expectedIncorrectUsage WP_Meta_Query::get_sql_for_clause
282+
*/
283+
public function test_subtype_registration_refuses_meta_query_for_array_with_matching_post_type() {
284+
register_post_meta(
285+
'page',
286+
'nocache_meta',
287+
array( 'invalidates_query_cache' => false )
288+
);
289+
290+
$query = new WP_Query();
291+
$query->query_vars['post_type'] = array( 'post', 'page' );
292+
293+
$meta_query = new WP_Meta_Query(
294+
array(
295+
array(
296+
'key' => 'nocache_meta',
297+
'value' => 'test',
298+
),
299+
)
300+
);
301+
302+
$sql = $meta_query->get_sql( 'post', 'wp_posts', 'ID', $query );
303+
304+
$this->assertStringNotContainsString( 'nocache_meta', $sql['where'], 'Non-cacheable meta key should be refused when any queried post type has it as non-cacheable.' );
305+
}
306+
307+
/**
308+
* A key registered for a specific post type should not skip cache invalidation
309+
* when written to a different post type.
310+
*/
311+
public function test_subtype_registration_does_not_skip_cache_for_different_post_type() {
312+
register_post_meta(
313+
'page',
314+
'nocache_meta',
315+
array( 'invalidates_query_cache' => false )
316+
);
317+
318+
// self::$post_id is a 'post', not a 'page'.
319+
wp_cache_set_last_changed( 'posts' );
320+
$before = wp_cache_get_last_changed( 'posts' );
321+
322+
usleep( 1000 );
323+
add_post_meta( self::$post_id, 'nocache_meta', 'value1' );
324+
325+
$after = wp_cache_get_last_changed( 'posts' );
326+
$this->assertNotSame( $before, $after, 'last_changed should still change when writing to a post type where the key is cacheable.' );
242327
}
243328
}

0 commit comments

Comments
 (0)