diff --git a/includes/query/class-tmdo-post-query-router.php b/includes/query/class-tmdo-post-query-router.php index db3d49e..922277a 100644 --- a/includes/query/class-tmdo-post-query-router.php +++ b/includes/query/class-tmdo-post-query-router.php @@ -172,8 +172,11 @@ class TMDO_Post_Query_Router { // meta_key + orderby meta_value(_num): native INNER JOINs the key // (EXISTS) and orders by meta_value+0 (NUMERIC) or meta_value. - $meta_key = $query->get( 'meta_key' ); - $take_key = is_string( $meta_key ) && in_array( $sort_name, array( 'meta_value', 'meta_value_num' ), true ) + // A key comparison other than equality (meta_compare_key LIKE …) matches + // other keys too: not a test on one flat column. + $meta_key = $query->get( 'meta_key' ); + $key_exact = in_array( (string) $query->get( 'meta_compare_key' ), array( '', '=' ), true ); + $take_key = $key_exact && is_string( $meta_key ) && in_array( $sort_name, array( 'meta_value', 'meta_value_num' ), true ) && null !== self::field_for( array( 'key' => $meta_key ), $pt ); if ( $take_key ) { $field = self::field_for( array( 'key' => $meta_key ), $pt ); @@ -192,11 +195,13 @@ class TMDO_Post_Query_Router { $routed[ $pt ][] = $route; $sort = self::sort_for( $pt, $route, $direction ); } - // meta_key alone (no meta_value, not a meta_value sort): WP_Query adds a - // clause that tests the key exists — HivePress's featured-listing lookup. - $exists_key = ! $take_key && is_string( $meta_key ) && '' !== $meta_key + // meta_key alone (no meta_value, not ordered by): WP_Query adds a clause + // that tests the key exists — HivePress's featured-listing lookup. Any + // orderby naming meta_value, meta_value_num or the key sorts by that + // clause, which clearing meta_key would drop. + $exists_key = ! $take_key && $key_exact && is_string( $meta_key ) && '' !== $meta_key && '' === (string) $query->get( 'meta_value' ) && '' === (string) $query->get( 'meta_compare' ) - && ! in_array( $sort_name, array( 'meta_value', 'meta_value_num' ), true ) + && ! array_intersect( $named, array( 'meta_value', 'meta_value_num', $meta_key ) ) && null !== self::field_for( array( 'key' => $meta_key ), $pt ); if ( $exists_key ) { $route = self::route_clause( @@ -220,8 +225,12 @@ class TMDO_Post_Query_Router { if ( ! isset( $clause['key'] ) ) { // Sort shape: matches every post, only orders by the key. $name = 'AND' === $relation ? self::sort_shape( $clause ) : ''; - if ( '' !== $name && ! in_array( $name, $named, true ) && self::field_for( $clause[ $name ], $pt ) ) { - continue; // Matches every post and orders nothing here (a copy of a search's meta query). + // Matches every post and orders nothing here (a copy of a search's meta + // query) — unless orderby names it, or meta_value(_num) / its key, which + // sort by it when it is WordPress's primary (first) clause. + if ( '' !== $name && ! array_intersect( $named, array( $name, 'meta_value', 'meta_value_num', (string) ( $clause[ $name ]['key'] ?? '' ) ) ) + && self::field_for( $clause[ $name ], $pt ) ) { + continue; } $field = '' !== $name && $name === $sort_name ? self::field_for( $clause[ $name ], $pt ) : null; if ( $field ) { diff --git a/tests/integration/PostQueryRouterTest.php b/tests/integration/PostQueryRouterTest.php index 7286a68..91a5829 100644 --- a/tests/integration/PostQueryRouterTest.php +++ b/tests/integration/PostQueryRouterTest.php @@ -471,6 +471,61 @@ class PostQueryRouterTest extends TestCase { $this->assertStringContainsString( 'BETWEEN', $where ); } + /** + * WordPress orders by the primary meta clause (meta_key) for meta_value, + * meta_value_num or the key's name anywhere in orderby; routing the key and + * clearing meta_key would drop that ordering (2.4 correctness review). + */ + public function test_meta_key_ordered_by_among_several_orderings_stays_native(): void { + foreach ( array( + array( 'meta_value_num' => 'ASC', 'title' => 'ASC' ), + 'meta_value date', + '_stock', + ) as $orderby ) { + $this->set_post_mode( 'aeav_only' ); + $query = $this->make_query( array( 'post_type' => 'product', 'meta_key' => '_stock', 'orderby' => $orderby ) ); + ( new WPDO_Post_Query_Router() )->pre_get_posts( $query ); + + $label = is_array( $orderby ) ? implode( ',', array_keys( $orderby ) ) : $orderby; + $this->assertSame( '_stock', $query->get( 'meta_key' ), $label ); + $this->assertSame( '', $query->get( 'wpdo_post_clauses' ), $label ); + } + } + + /** meta_compare_key LIKE matches other keys too: not a test on one flat column. */ + public function test_meta_key_with_a_key_comparison_stays_native(): void { + foreach ( array( 'rand', 'meta_value_num' ) as $orderby ) { + $this->set_post_mode( 'aeav_only' ); + $query = $this->make_query( array( 'post_type' => 'product', 'meta_key' => '_stock', 'meta_compare_key' => 'LIKE', 'orderby' => $orderby ) ); + ( new WPDO_Post_Query_Router() )->pre_get_posts( $query ); + + $this->assertSame( '_stock', $query->get( 'meta_key' ), $orderby ); + $this->assertSame( '', $query->get( 'wpdo_post_clauses' ), $orderby ); + } + } + + /** + * Without meta_key the first clause is WordPress's primary one: orderby + * meta_value_num sorts by the sort group's key, so the group stays. + */ + public function test_sort_group_ordered_as_the_primary_clause_is_not_dropped(): void { + $this->assert_not_routed( + array( + 'post_type' => 'product', + 'orderby' => 'meta_value_num', + 'meta_query' => array( + array( + 'relation' => 'OR', + 'price__order' => array( 'key' => '_price', 'compare' => 'NOT EXISTS', 'type' => 'DECIMAL' ), + array( 'key' => '_price', 'compare' => 'EXISTS', 'type' => 'DECIMAL' ), + ), + array( 'key' => '_stock', 'value' => 1, 'compare' => '>=', 'type' => 'NUMERIC' ), + ), + ), + 'primary sort group' + ); + } + public function test_meta_key_with_a_value_is_left_alone(): void { $this->set_post_mode( 'aeav_only' ); $router = new WPDO_Post_Query_Router();