Skip to content

Commit 74c24a3

Browse files
ellatrixclaude
andcommitted
Exclude non-cacheable meta keys from keyless meta_value queries
When a WP_Query uses meta_value without meta_key, the query matches across all meta keys. Non-cacheable meta keys must be excluded from these results, otherwise stale cached results would be served since cache invalidation is skipped for those keys. Adds a NOT IN clause to exclude registered non-cacheable keys when a meta query clause has a value but no key. Adds tests based on the cases raised in PR review. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 26abea9 commit 74c24a3

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,21 @@ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' )
776776
}
777777
}
778778

779+
// When querying by meta_value without a specific key, exclude
780+
// non-cacheable meta keys so their rows never appear in results.
781+
// This is necessary because cache invalidation is skipped for these
782+
// keys, so including them would produce stale cached query results.
783+
if ( 'post' === $this->object_type
784+
&& ! array_key_exists( 'key', $clause )
785+
&& array_key_exists( 'value', $clause )
786+
) {
787+
$excluded_keys = $this->get_non_cacheable_meta_keys();
788+
if ( ! empty( $excluded_keys ) ) {
789+
$placeholders = implode( ',', array_fill( 0, count( $excluded_keys ), '%s' ) );
790+
$sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key NOT IN ($placeholders)", $excluded_keys ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
791+
}
792+
}
793+
779794
// meta_value.
780795
if ( array_key_exists( 'value', $clause ) ) {
781796
$meta_value = $clause['value'];
@@ -895,6 +910,33 @@ protected function is_non_cacheable_meta_key( $meta_key ) {
895910
return false;
896911
}
897912

913+
/**
914+
* Returns all post meta keys registered with invalidates_query_cache false.
915+
*
916+
* @since 7.0.0
917+
*
918+
* @return string[] Array of non-cacheable meta keys.
919+
*/
920+
protected function get_non_cacheable_meta_keys() {
921+
global $wp_meta_keys;
922+
923+
$keys = array();
924+
925+
if ( ! is_array( $wp_meta_keys ) || ! isset( $wp_meta_keys['post'] ) ) {
926+
return $keys;
927+
}
928+
929+
foreach ( $wp_meta_keys['post'] as $registered_keys ) {
930+
foreach ( $registered_keys as $meta_key => $args ) {
931+
if ( isset( $args['invalidates_query_cache'] ) && ! $args['invalidates_query_cache'] ) {
932+
$keys[] = $meta_key;
933+
}
934+
}
935+
}
936+
937+
return array_unique( $keys );
938+
}
939+
898940
/**
899941
* Identifies an existing table alias that is compatible with the current
900942
* query clause.

tests/phpunit/tests/meta/invalidatesQueryCache.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,4 +373,60 @@ public function test_subtype_registration_does_not_skip_cache_for_different_post
373373
$after = wp_cache_get_last_changed( 'posts' );
374374
$this->assertNotSame( $before, $after, 'last_changed should still change when writing to a post type where the key is cacheable.' );
375375
}
376+
377+
/**
378+
* A WP_Query using meta_value without meta_key should not return posts
379+
* matched via a non-cacheable meta key.
380+
*/
381+
public function test_wp_query_meta_value_excludes_non_cacheable_keys() {
382+
register_post_meta(
383+
'post',
384+
'nocache_meta',
385+
array( 'invalidates_query_cache' => false )
386+
);
387+
388+
add_post_meta( self::$post_id, 'nocache_meta', 'nocache_value' );
389+
390+
$query = new WP_Query(
391+
array(
392+
'fields' => 'ids',
393+
'meta_value' => 'nocache_value',
394+
)
395+
);
396+
397+
$this->assertEmpty( $query->posts, 'WP_Query by meta_value should not match non-cacheable meta keys.' );
398+
}
399+
400+
/**
401+
* A WP_Query using meta_value without meta_key should not return stale
402+
* cached results after non-cacheable meta is deleted.
403+
*/
404+
public function test_wp_query_meta_value_no_stale_cache_after_delete() {
405+
register_post_meta(
406+
'post',
407+
'nocache_meta',
408+
array( 'invalidates_query_cache' => false )
409+
);
410+
411+
add_post_meta( self::$post_id, 'nocache_meta', 'nocache_value' );
412+
413+
// Run the query once to prime the cache.
414+
$query1 = new WP_Query(
415+
array(
416+
'fields' => 'ids',
417+
'meta_value' => 'nocache_value',
418+
)
419+
);
420+
421+
delete_post_meta( self::$post_id, 'nocache_meta' );
422+
423+
$query2 = new WP_Query(
424+
array(
425+
'fields' => 'ids',
426+
'meta_value' => 'nocache_value',
427+
)
428+
);
429+
430+
$this->assertEmpty( $query2->posts, 'WP_Query should not return stale cached results for non-cacheable meta.' );
431+
}
376432
}

0 commit comments

Comments
 (0)