84f6e32913
- A sort-only search (HivePress _sort, no routed filter) LEFT JOINs the flat table to keep posts without a row; it now drives from the flat table (STRAIGHT_JOIN, INNER JOIN) when every post of the type has a flat row. The router gives a new post of a type with its own groups an empty row (wp_insert_post); coverage is one anti-join per group, cached 15 min and per post-mode change (forget_coverage()). - A flat-first search drops the GROUP BY posts.ID WordPress adds for any tax query when the flat row is the only join (WooCommerce's product_visibility NOT IN on searches is a subquery); a joining tax query keeps it. - Verify: live router suite (5 new cases, RED before); dev36 rating__desc page, ids identical: native ~3.3 s, 2.1 routing ~1.5–1.9 s, now ~0.51–0.85 s (main query 440 → 46 ms, COUNT 400 → 160 ms). p50 ≤ 300 ms is still not met: page rendering and HivePress's own get_posts() in pre_get_posts (suppress_filters, ~100 ms) remain. - PR: PR-079
559 lines
20 KiB
PHP
559 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Suite: a routed WP_Query returns exactly what the native meta_query returns.
|
|
*
|
|
* Bugs (dev36 qbench, 2026-09-26): the post router pulled registered clauses
|
|
* out of any meta_query and ANDed them, so OR relations (Q04, Q16), queries on
|
|
* several post types (Q08), untyped comparisons on numeric columns (Q14,
|
|
* native compares strings) and orderby on a named clause (Q15) returned other
|
|
* posts than WordPress would; LIKE compared without wildcards (Q06). A key
|
|
* of a second group (wp_core _thumbnail_id, Q05) was never joined, and every
|
|
* routed query carried a GROUP BY although flat rows are one per post.
|
|
*
|
|
* Every case runs the same query twice on fixture posts — router hooks off,
|
|
* then on with post mode shadow_read — and compares the ids in order.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Run a WP_Query restricted to $ids, optionally through the post router.
|
|
*
|
|
* @param array<string, mixed> $args Query args (meta_query, post_type, orderby …).
|
|
* @param int[] $ids Fixture posts to search within.
|
|
* @param bool $routed Hook a TMDO_Post_Query_Router for this query.
|
|
* @return array{ids:int[], routed:bool, sql:string}
|
|
*/
|
|
function tmdo_live_router_query( array $args, array $ids, bool $routed ): array {
|
|
global $wpdb;
|
|
$router = new TMDO_Post_Query_Router();
|
|
if ( $routed ) {
|
|
$router->register_hooks();
|
|
}
|
|
try {
|
|
$wpdb->last_error = '';
|
|
$q = new WP_Query(
|
|
$args + array(
|
|
'post__in' => $ids,
|
|
'post_status' => 'any',
|
|
'posts_per_page' => -1,
|
|
'orderby' => 'ID',
|
|
'order' => 'ASC',
|
|
'fields' => 'ids',
|
|
'no_found_rows' => true,
|
|
'cache_results' => false,
|
|
'suppress_filters' => false,
|
|
)
|
|
);
|
|
} finally {
|
|
tmdo_live_router_unhook( $router );
|
|
}
|
|
tmdo_live_assert_same( '', $wpdb->last_error, 'SQL error' );
|
|
return array(
|
|
'ids' => array_map( 'intval', $q->posts ),
|
|
'routed' => ! empty( $q->get( 'wpdo_post_clauses' ) ) || ! empty( $q->get( 'wpdo_post_sort' ) ),
|
|
'sql' => (string) $q->request,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Remove every hook TMDO_Post_Query_Router::register_hooks() added.
|
|
*
|
|
* @param TMDO_Post_Query_Router $router Router instance.
|
|
*/
|
|
function tmdo_live_router_unhook( TMDO_Post_Query_Router $router ): void {
|
|
foreach ( array( 'pre_get_posts', 'posts_join', 'posts_where', 'posts_orderby', 'posts_request', 'split_the_query', 'found_posts_query' ) as $method ) {
|
|
foreach ( 'posts_request' === $method ? array( 'posts_request', 'posts_request_ids' ) : array( $method ) as $hook ) {
|
|
$priority = has_filter( $hook, array( $router, $method ) );
|
|
if ( false !== $priority ) {
|
|
remove_filter( $hook, array( $router, $method ), $priority );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create posts of $post_type with the given meta, in post mode shadow_read
|
|
* (so the Hook Bus keeps the flat rows in step).
|
|
*
|
|
* @param string $post_type Post type.
|
|
* @param array<string, array<string, mixed>> $posts Label => meta.
|
|
* @return array<string, int> Label => post id.
|
|
*/
|
|
function tmdo_live_router_posts( string $post_type, array $posts ): array {
|
|
$ids = array();
|
|
foreach ( $posts as $label => $meta ) {
|
|
$ids[ $label ] = TMDO_Live_Fixtures::post( $post_type );
|
|
foreach ( $meta as $key => $value ) {
|
|
update_post_meta( $ids[ $label ], $key, $value );
|
|
}
|
|
}
|
|
return $ids;
|
|
}
|
|
|
|
/**
|
|
* Listing fixtures shared by the hp_listing cases.
|
|
*
|
|
* @return array<string, int>
|
|
*/
|
|
function tmdo_live_router_listings(): array {
|
|
foreach ( array( 'hp_price', 'hp_verified', 'hp_featured', 'hp_hourly_rate' ) as $key ) {
|
|
TMDO_Live_Fixtures::field( 'post', $key, 'hp_listing' );
|
|
}
|
|
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
|
return tmdo_live_router_posts(
|
|
'hp_listing',
|
|
array(
|
|
'L1' => array( 'hp_price' => '10000', 'hp_featured' => '1', 'hp_verified' => '0', 'hp_hourly_rate' => '40' ),
|
|
'L2' => array( 'hp_price' => '9500', 'hp_verified' => '1', 'hp_hourly_rate' => '30' ),
|
|
'L3' => array( 'hp_price' => '500', 'hp_featured' => '0', 'hp_hourly_rate' => '45' ),
|
|
'L4' => array( 'hp_price' => '20000', 'tmdo_live_unregistered' => '0' ),
|
|
'L5' => array(),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Assert the routed query equals native; optionally that it was routed at all.
|
|
*
|
|
* @param array<string, mixed> $args Query args.
|
|
* @param int[] $ids Fixture ids.
|
|
* @param bool|null $expect_routed true / false to assert, null to ignore.
|
|
* @param bool $ordered Compare order too (false when the order has ties).
|
|
*/
|
|
function tmdo_live_router_assert_native( array $args, array $ids, ?bool $expect_routed = null, bool $ordered = true ): void {
|
|
$native = tmdo_live_router_query( $args, $ids, false );
|
|
$routed = tmdo_live_router_query( $args, $ids, true );
|
|
if ( ! $ordered ) {
|
|
sort( $native['ids'] );
|
|
sort( $routed['ids'] );
|
|
}
|
|
tmdo_live_assert_same( $native['ids'], $routed['ids'], 'routed ids vs native' );
|
|
if ( null !== $expect_routed ) {
|
|
tmdo_live_assert_same( $expect_routed, $routed['routed'], 'query routed to the flat table' );
|
|
}
|
|
if ( $routed['routed'] ) {
|
|
tmdo_live_assert_true( false === stripos( $routed['sql'], 'GROUP BY' ), 'routed query adds no GROUP BY: ' . $routed['sql'] );
|
|
}
|
|
}
|
|
|
|
TMDO_Live::test(
|
|
'Q04 OR across registered keys returns the native posts',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'hp_listing',
|
|
'meta_query' => array(
|
|
'relation' => 'OR',
|
|
array( 'key' => 'hp_featured', 'value' => '1' ),
|
|
array( 'key' => 'hp_verified', 'value' => '1' ),
|
|
),
|
|
),
|
|
array_values( $l )
|
|
);
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'Q16 OR mixing a registered and an unregistered key returns the native posts',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'hp_listing',
|
|
'meta_query' => array(
|
|
'relation' => 'OR',
|
|
array( 'key' => 'hp_price', 'value' => 1000, 'compare' => '<', 'type' => 'NUMERIC' ),
|
|
array( 'key' => 'tmdo_live_unregistered', 'value' => '0' ),
|
|
),
|
|
),
|
|
array_values( $l )
|
|
);
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'Q14 untyped comparison on a numeric column keeps string semantics',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'hp_listing',
|
|
'meta_query' => array( array( 'key' => 'hp_price', 'value' => '9000', 'compare' => '>' ) ),
|
|
),
|
|
array_values( $l )
|
|
);
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'Q15 orderby on a named clause keeps the native order, routed',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'hp_listing',
|
|
'meta_query' => array(
|
|
'rate' => array( 'key' => 'hp_hourly_rate', 'value' => 50, 'compare' => '<', 'type' => 'NUMERIC' ),
|
|
'price' => array( 'key' => 'hp_price', 'compare' => 'EXISTS', 'type' => 'NUMERIC' ),
|
|
),
|
|
'orderby' => 'price',
|
|
'order' => 'ASC',
|
|
),
|
|
array_values( $l ),
|
|
true
|
|
);
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'Q08 several post types return the native posts',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
TMDO_Live_Fixtures::field( 'post', 'hp_budget', 'hp_request' );
|
|
$r = tmdo_live_router_posts( 'hp_request', array( 'R1' => array( 'hp_budget' => '90000' ) ) );
|
|
update_post_meta( $l['L2'], 'hp_budget', '85000' );
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => array( 'hp_listing', 'hp_request' ),
|
|
'meta_query' => array( array( 'key' => 'hp_budget', 'value' => 80000, 'compare' => '>=', 'type' => 'NUMERIC' ) ),
|
|
),
|
|
array_merge( array_values( $l ), array_values( $r ) )
|
|
);
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'typed AND on registered keys is routed and returns the native posts',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'hp_listing',
|
|
'meta_query' => array(
|
|
array( 'key' => 'hp_price', 'value' => array( 9000, 15000 ), 'compare' => 'BETWEEN', 'type' => 'NUMERIC' ),
|
|
array( 'key' => 'hp_verified', 'value' => '1' ),
|
|
),
|
|
),
|
|
array_values( $l ),
|
|
true
|
|
);
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'Q06 LIKE matches substrings and escapes wildcards, routed',
|
|
static function () {
|
|
// A text key of wp_core (WooCommerce's _sku left the flat tables in 1.2.0).
|
|
TMDO_Live_Fixtures::field( 'post', '_wp_page_template' );
|
|
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
|
$p = tmdo_live_router_posts(
|
|
'post',
|
|
array(
|
|
'P1' => array( '_wp_page_template' => 'tmdo-live-B1-1' ),
|
|
'P2' => array( '_wp_page_template' => 'tmdo-live-B1-10' ),
|
|
'P3' => array( '_wp_page_template' => 'tmdo-live-B2-1' ),
|
|
)
|
|
);
|
|
foreach ( array( 'B1-1', 'B1_1' ) as $needle ) {
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'post',
|
|
'meta_query' => array( array( 'key' => '_wp_page_template', 'value' => $needle, 'compare' => 'LIKE' ) ),
|
|
),
|
|
array_values( $p ),
|
|
true
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'Q05 keys of two groups are both joined, routed',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
TMDO_Live_Fixtures::field( 'post', '_thumbnail_id', 'hp_listing' );
|
|
update_post_meta( $l['L1'], '_thumbnail_id', '123' );
|
|
update_post_meta( $l['L3'], '_thumbnail_id', '124' );
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'hp_listing',
|
|
'meta_query' => array(
|
|
array( 'key' => 'hp_hourly_rate', 'value' => 42, 'compare' => '>=', 'type' => 'NUMERIC' ),
|
|
array( 'key' => '_thumbnail_id', 'compare' => 'EXISTS' ),
|
|
),
|
|
),
|
|
array_values( $l ),
|
|
true
|
|
);
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'NOT EXISTS with explicit statuses and date order keeps posts without a flat row',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'hp_listing',
|
|
'post_status' => array( 'draft' ),
|
|
'orderby' => 'date',
|
|
'order' => 'DESC',
|
|
'meta_query' => array( array( 'key' => 'hp_featured', 'compare' => 'NOT EXISTS' ) ),
|
|
),
|
|
array_values( $l ),
|
|
true,
|
|
false
|
|
);
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'HivePress sort shape (NOT EXISTS OR EXISTS, named clause) keeps the native order, routed',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
foreach ( array( 'ASC', 'DESC' ) as $order ) {
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'hp_listing',
|
|
'meta_query' => array(
|
|
array(
|
|
'relation' => 'OR',
|
|
'price__order' => array( 'key' => 'hp_price', 'type' => 'DECIMAL', 'compare' => 'NOT EXISTS' ),
|
|
array( 'key' => 'hp_price', 'type' => 'DECIMAL', 'compare' => 'EXISTS' ),
|
|
),
|
|
),
|
|
'orderby' => 'price__order',
|
|
'order' => $order,
|
|
),
|
|
array_values( $l ),
|
|
true
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'Q07 meta_key + meta_value_num keeps the native posts and order, routed',
|
|
static function () {
|
|
$l = tmdo_live_router_listings();
|
|
tmdo_live_router_assert_native(
|
|
array(
|
|
'post_type' => 'hp_listing',
|
|
'meta_key' => 'hp_price',
|
|
'orderby' => 'meta_value_num',
|
|
'order' => 'DESC',
|
|
),
|
|
array_values( $l ),
|
|
true
|
|
);
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'a routed full-row search keeps the split query (IDs first) and native found_posts',
|
|
static function () {
|
|
global $wpdb;
|
|
$l = tmdo_live_router_listings();
|
|
$args = array(
|
|
'post_type' => 'hp_listing',
|
|
'post__in' => array_values( $l ),
|
|
'post_status' => 'any',
|
|
'posts_per_page' => 2,
|
|
'orderby' => 'ID',
|
|
'order' => 'ASC',
|
|
'meta_query' => array( array( 'key' => 'hp_price', 'value' => 1000, 'compare' => '>=', 'type' => 'NUMERIC' ) ),
|
|
);
|
|
$native = new WP_Query( $args );
|
|
$router = new TMDO_Post_Query_Router();
|
|
$router->register_hooks();
|
|
try {
|
|
$routed = new WP_Query( $args );
|
|
} finally {
|
|
tmdo_live_router_unhook( $router );
|
|
}
|
|
|
|
tmdo_live_assert_same( wp_list_pluck( $native->posts, 'ID' ), wp_list_pluck( $routed->posts, 'ID' ), 'page of posts' );
|
|
tmdo_live_assert_same( $native->found_posts, $routed->found_posts, 'found_posts' );
|
|
tmdo_live_assert_true( false !== strpos( $routed->request, 'wpdoflat_' ), 'routed: ' . $routed->request );
|
|
tmdo_live_assert_true( 1 === preg_match( '/^\s*SELECT\s+' . preg_quote( $wpdb->posts, '/' ) . '\.ID\s/', $routed->request ), 'IDs first: ' . $routed->request );
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'a filtered search ordered by a flat column drives from the flat table and matches native',
|
|
static function () {
|
|
foreach ( array( 'hp_price', 'hp_hourly_rate' ) as $key ) {
|
|
TMDO_Live_Fixtures::field( 'post', $key, 'hp_listing' );
|
|
}
|
|
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
|
// A price band no seeded listing uses keeps the result to the fixtures.
|
|
tmdo_live_router_posts(
|
|
'hp_listing',
|
|
array(
|
|
'A' => array( 'hp_price' => '9876501', 'hp_hourly_rate' => '12' ),
|
|
'B' => array( 'hp_price' => '9876502', 'hp_hourly_rate' => '15' ),
|
|
'C' => array( 'hp_price' => '9876503' ),
|
|
)
|
|
);
|
|
$args = array(
|
|
'post_type' => 'hp_listing',
|
|
'post_status' => 'any',
|
|
'posts_per_page' => 10,
|
|
'fields' => 'ids',
|
|
'suppress_filters' => false,
|
|
'meta_query' => array( array( 'key' => 'hp_price', 'value' => array( 9876500, 9876510 ), 'compare' => 'BETWEEN', 'type' => 'NUMERIC' ) ),
|
|
'meta_key' => 'hp_price',
|
|
'orderby' => 'meta_value_num',
|
|
'order' => 'DESC',
|
|
);
|
|
$native = new WP_Query( $args );
|
|
$router = new TMDO_Post_Query_Router();
|
|
$router->register_hooks();
|
|
try {
|
|
$routed = new WP_Query( $args );
|
|
} finally {
|
|
tmdo_live_router_unhook( $router );
|
|
}
|
|
|
|
tmdo_live_assert_same( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), 'ids' );
|
|
tmdo_live_assert_same( $native->found_posts, $routed->found_posts, 'found_posts' );
|
|
tmdo_live_assert_true( 0 === strpos( ltrim( $routed->request ), 'SELECT STRAIGHT_JOIN' ), 'flat-first: ' . $routed->request );
|
|
}
|
|
);
|
|
|
|
/**
|
|
* HivePress-style sort-only search (rating__order), fields ids, first page.
|
|
*
|
|
* @param string $order ASC | DESC.
|
|
* @return array<string, mixed> WP_Query args.
|
|
*/
|
|
function tmdo_live_router_rating_sort( string $order ): array {
|
|
return array(
|
|
'post_type' => 'hp_listing',
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => 10,
|
|
'fields' => 'ids',
|
|
'suppress_filters' => false,
|
|
'meta_query' => array(
|
|
array(
|
|
'relation' => 'OR',
|
|
'rating__order' => array( 'key' => 'hp_rating', 'type' => 'DECIMAL', 'compare' => 'NOT EXISTS' ),
|
|
array( 'key' => 'hp_rating', 'type' => 'DECIMAL', 'compare' => 'EXISTS' ),
|
|
),
|
|
),
|
|
'orderby' => 'rating__order',
|
|
'order' => $order,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Native and routed runs of the same query.
|
|
*
|
|
* @param array<string, mixed> $args Query args.
|
|
* @return array{0:WP_Query, 1:WP_Query}
|
|
*/
|
|
function tmdo_live_router_native_and_routed( array $args ): array {
|
|
$native = new WP_Query( $args );
|
|
$router = new TMDO_Post_Query_Router();
|
|
$router->register_hooks();
|
|
try {
|
|
$routed = new WP_Query( $args );
|
|
} finally {
|
|
tmdo_live_router_unhook( $router );
|
|
}
|
|
return array( $native, $routed );
|
|
}
|
|
|
|
TMDO_Live::test(
|
|
'a sort-only search drives from the flat table when every post of the type has a flat row (2.2 PR-079)',
|
|
static function () {
|
|
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
|
|
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
|
TMDO_Post_Query_Router::forget_coverage();
|
|
foreach ( array( 'DESC', 'ASC' ) as $order ) {
|
|
list( $native, $routed ) = tmdo_live_router_native_and_routed( tmdo_live_router_rating_sort( $order ) );
|
|
tmdo_live_assert_same( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), "{$order} ids" );
|
|
tmdo_live_assert_same( $native->found_posts, $routed->found_posts, "{$order} found_posts" );
|
|
tmdo_live_assert_true( 0 === strpos( ltrim( $routed->request ), 'SELECT STRAIGHT_JOIN' ), "{$order} flat-first: " . $routed->request );
|
|
}
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'a sort-only search keeps the LEFT JOIN while a post of the type has no flat row (2.2 PR-079)',
|
|
static function () {
|
|
global $wpdb;
|
|
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
|
|
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
|
$bare = TMDO_Live_Fixtures::post( 'hp_listing' );
|
|
$wpdb->delete( TMDO_Schema_Manager::get_table_name( 'post', 'hp_listing_core' ), array( 'post_id' => $bare ) );
|
|
TMDO_Post_Query_Router::forget_coverage();
|
|
try {
|
|
list( $native, $routed ) = tmdo_live_router_native_and_routed( tmdo_live_router_rating_sort( 'DESC' ) );
|
|
} finally {
|
|
TMDO_Post_Query_Router::forget_coverage();
|
|
}
|
|
tmdo_live_assert_same( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), 'ids' );
|
|
tmdo_live_assert_true( false === strpos( $routed->request, 'STRAIGHT_JOIN' ), 'not flat-first: ' . $routed->request );
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'a post created while the router is active gets its empty flat row (2.2 PR-079)',
|
|
static function () {
|
|
global $wpdb;
|
|
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
|
|
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
|
$router = new TMDO_Post_Query_Router();
|
|
$router->register_hooks();
|
|
try {
|
|
$id = TMDO_Live_Fixtures::post( 'hp_listing' );
|
|
} finally {
|
|
tmdo_live_router_unhook( $router );
|
|
}
|
|
$table = TMDO_Schema_Manager::get_table_name( 'post', 'hp_listing_core' );
|
|
tmdo_live_assert_same( '1', $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE post_id = %d", $id ) ), 'flat row created' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'a flat-first search drops the GROUP BY WordPress adds for a subquery-only tax query (2.2 PR-079)',
|
|
static function () {
|
|
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
|
|
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
|
TMDO_Post_Query_Router::forget_coverage();
|
|
// Like WooCommerce's product_visibility exclusion on front-end searches: NOT IN
|
|
// is a subquery, yet WordPress groups by posts.ID for any tax query.
|
|
$args = tmdo_live_router_rating_sort( 'DESC' ) + array(
|
|
'tax_query' => array( array( 'taxonomy' => 'hp_listing_category', 'field' => 'term_id', 'terms' => array( 999999999 ), 'operator' => 'NOT IN' ) ),
|
|
);
|
|
list( $native, $routed ) = tmdo_live_router_native_and_routed( $args );
|
|
tmdo_live_assert_same( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), 'ids' );
|
|
tmdo_live_assert_true( 0 === strpos( ltrim( $routed->request ), 'SELECT STRAIGHT_JOIN' ), 'flat-first: ' . $routed->request );
|
|
tmdo_live_assert_true( false === stripos( $routed->request, 'GROUP BY' ), 'no GROUP BY: ' . $routed->request );
|
|
}
|
|
);
|
|
|
|
TMDO_Live::test(
|
|
'a flat-first search keeps the GROUP BY when a tax query joins term relationships (2.2 PR-079)',
|
|
static function () {
|
|
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
|
|
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
|
TMDO_Post_Query_Router::forget_coverage();
|
|
$terms = get_terms( array( 'taxonomy' => 'hp_listing_category', 'number' => 2, 'hide_empty' => true, 'fields' => 'ids' ) );
|
|
if ( count( (array) $terms ) < 2 ) {
|
|
throw new TMDO_Live_Skip( 'needs two used hp_listing_category terms' );
|
|
}
|
|
$args = tmdo_live_router_rating_sort( 'DESC' ) + array(
|
|
'tax_query' => array( array( 'taxonomy' => 'hp_listing_category', 'field' => 'term_id', 'terms' => $terms, 'operator' => 'IN' ) ),
|
|
);
|
|
list( $native, $routed ) = tmdo_live_router_native_and_routed( $args );
|
|
tmdo_live_assert_same( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), 'ids' );
|
|
tmdo_live_assert_true( false !== stripos( $routed->request, 'GROUP BY' ), 'GROUP BY kept: ' . $routed->request );
|
|
}
|
|
);
|