feat(2meet-data-optimizer): route meta orderby including the HivePress sort clause
- Sort shape (relation OR of NOT EXISTS + EXISTS on one key, named in orderby): drop it from meta_query, LEFT JOIN the flat table and order by its column, ties by ID - Named routed clause as the sole orderby, and meta_key + meta_value_num (meta_value on string columns) with IS NOT NULL; stay native when the column would not order like CAST(meta_value AS type), orderby has several keys, or meta_value filters - Verify: tests/live/run.php router (11/11); fe.sh price__asc / hourly_rate__desc / rating__desc ids == native - PR: PR-062
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Meta orderby on the flat table: the "sort by an attribute, keep posts
|
||||
* without it" shape (relation OR of NOT EXISTS + EXISTS on one key, named
|
||||
* clause in orderby — HivePress search sorting), orderby on a named routed
|
||||
* clause, and meta_key + meta_value(_num).
|
||||
*
|
||||
* Only when the flat column orders the way native CASTs meta_value;
|
||||
* otherwise the whole query stays native.
|
||||
*/
|
||||
class PostQueryRouterOrderTest extends TestCase {
|
||||
|
||||
private const ALIAS = '`wpdoflat_product_wc_product`';
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
WPDO_Entity_Registry::init();
|
||||
WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() );
|
||||
WPDO_Post_Fields::register_entity_fields();
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
$cache = new ReflectionProperty( WPDO_Mode_Manager::class, 'cache' );
|
||||
$cache->setAccessible( true );
|
||||
$cache->setValue( null, array( 'post' => 'aeav_only', 'user' => 'disabled', 'term' => 'disabled', 'comment' => 'disabled' ) );
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void {
|
||||
$cache = new ReflectionProperty( WPDO_Mode_Manager::class, 'cache' );
|
||||
$cache->setAccessible( true );
|
||||
$cache->setValue( null, null );
|
||||
WPDO_Entity_Registry::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $vars Query vars (post_type defaults to product).
|
||||
* @return array{query:WP_Query, join:string, where:string, orderby:string}
|
||||
*/
|
||||
private function route( array $vars ): array {
|
||||
$query = new WP_Query();
|
||||
foreach ( $vars + array( 'post_type' => 'product' ) as $k => $v ) {
|
||||
$query->set( $k, $v );
|
||||
}
|
||||
$router = new TMDO_Post_Query_Router();
|
||||
$router->pre_get_posts( $query );
|
||||
return array(
|
||||
'query' => $query,
|
||||
'join' => $router->posts_join( '', $query ),
|
||||
'where' => $router->posts_where( '', $query ),
|
||||
'orderby' => $router->posts_orderby( 'wp_posts.post_date DESC', $query ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type Clause type.
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
private function sort_shape( string $type ): array {
|
||||
return array(
|
||||
array(
|
||||
'relation' => 'OR',
|
||||
'price__order' => array( 'key' => '_price', 'type' => $type, 'compare' => 'NOT EXISTS' ),
|
||||
array( 'key' => '_price', 'type' => $type, 'compare' => 'EXISTS' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_sort_shape_left_joins_and_orders_by_the_flat_column_without_filtering(): void {
|
||||
$sql = $this->route( array( 'meta_query' => $this->sort_shape( 'DECIMAL' ), 'orderby' => 'price__order', 'order' => 'ASC' ) );
|
||||
|
||||
$this->assertSame( array(), $sql['query']->get( 'meta_query' ), 'sort group removed from the native meta_query' );
|
||||
$this->assertStringContainsString( 'LEFT JOIN `wp_itest_wpdo_post_wc_product` AS ' . self::ALIAS, $sql['join'] );
|
||||
$this->assertStringNotContainsString( '_price', $sql['where'], 'every post matches: no condition' );
|
||||
$this->assertStringStartsWith( self::ALIAS . '.`_price` ASC', $sql['orderby'] );
|
||||
}
|
||||
|
||||
public function test_sort_shape_with_char_type_on_a_numeric_column_stays_native(): void {
|
||||
$vars = array( 'meta_query' => $this->sort_shape( 'CHAR' ), 'orderby' => 'price__order', 'order' => 'ASC' );
|
||||
$sql = $this->route( $vars );
|
||||
|
||||
$this->assertSame( $vars['meta_query'], $sql['query']->get( 'meta_query' ) );
|
||||
$this->assertSame( 'wp_posts.post_date DESC', $sql['orderby'] );
|
||||
}
|
||||
|
||||
public function test_orderby_on_a_named_routed_clause_orders_by_its_column(): void {
|
||||
$sql = $this->route(
|
||||
array(
|
||||
'meta_query' => array( 'price' => array( 'key' => '_price', 'compare' => 'EXISTS', 'type' => 'NUMERIC' ) ),
|
||||
'orderby' => array( 'price' => 'DESC' ),
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertStringContainsString( self::ALIAS . '.`_price` IS NOT NULL', $sql['where'] );
|
||||
$this->assertStringStartsWith( self::ALIAS . '.`_price` DESC', $sql['orderby'] );
|
||||
}
|
||||
|
||||
public function test_orderby_naming_a_routed_clause_among_other_keys_stays_native(): void {
|
||||
$vars = array(
|
||||
'meta_query' => array( 'price' => array( 'key' => '_price', 'compare' => 'EXISTS', 'type' => 'NUMERIC' ) ),
|
||||
'orderby' => array( 'price' => 'DESC', 'title' => 'ASC' ),
|
||||
);
|
||||
$sql = $this->route( $vars );
|
||||
|
||||
$this->assertSame( $vars['meta_query'], $sql['query']->get( 'meta_query' ) );
|
||||
$this->assertSame( 'wp_posts.post_date DESC', $sql['orderby'] );
|
||||
}
|
||||
|
||||
public function test_meta_value_num_on_a_numeric_key_requires_the_key_and_orders_by_its_column(): void {
|
||||
$sql = $this->route( array( 'meta_key' => '_price', 'orderby' => 'meta_value_num', 'order' => 'DESC' ) );
|
||||
|
||||
$this->assertSame( '', $sql['query']->get( 'meta_key' ), 'primary meta clause taken over' );
|
||||
$this->assertStringContainsString( self::ALIAS . '.`_price` IS NOT NULL', $sql['where'], 'native INNER JOIN drops posts without the key' );
|
||||
$this->assertStringStartsWith( self::ALIAS . '.`_price` DESC', $sql['orderby'] );
|
||||
}
|
||||
|
||||
public function test_meta_value_on_a_string_key_is_routed(): void {
|
||||
$sql = $this->route( array( 'meta_key' => '_sku', 'orderby' => 'meta_value', 'order' => 'ASC' ) );
|
||||
|
||||
$this->assertStringStartsWith( self::ALIAS . '.`_sku` ASC', $sql['orderby'] );
|
||||
}
|
||||
|
||||
public function test_meta_value_on_a_numeric_key_stays_native(): void {
|
||||
$sql = $this->route( array( 'meta_key' => '_price', 'orderby' => 'meta_value', 'order' => 'ASC' ) );
|
||||
|
||||
$this->assertSame( '_price', $sql['query']->get( 'meta_key' ), 'native string order' );
|
||||
$this->assertSame( 'wp_posts.post_date DESC', $sql['orderby'] );
|
||||
}
|
||||
|
||||
public function test_meta_key_with_a_meta_value_filter_stays_native(): void {
|
||||
$sql = $this->route( array( 'meta_key' => '_price', 'meta_value' => '5', 'orderby' => 'meta_value_num' ) );
|
||||
|
||||
$this->assertSame( '_price', $sql['query']->get( 'meta_key' ) );
|
||||
$this->assertSame( 'wp_posts.post_date DESC', $sql['orderby'] );
|
||||
}
|
||||
}
|
||||
@@ -381,17 +381,6 @@ class PostQueryRouterTest extends TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
public function test_orderby_on_a_named_registered_clause_is_not_routed(): void {
|
||||
$this->assert_not_routed(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'meta_query' => array( 'price' => array( 'key' => '_price', 'compare' => 'EXISTS', 'type' => 'NUMERIC' ) ),
|
||||
'orderby' => array( 'price' => 'ASC' ),
|
||||
),
|
||||
'orderby names a routed clause'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_untyped_integer_equality_on_an_integer_column_is_routed(): void {
|
||||
$this->set_post_mode( 'aeav_only' );
|
||||
$query = $this->make_query(
|
||||
|
||||
Reference in New Issue
Block a user