fix(2meet-data-optimizer): route a meta query only when every registered clause is routable
- Leave the query to WordPress on OR, nested groups with registered keys, several post types, orderby on a named routed clause, and compare/type pairs whose flat-column semantics differ from native (untyped numeric ranges, CHAR on decimals, NUMERIC on text) - Wrap LIKE values as %esc_like%; hook pre_get_posts at priority 1000 so plugin clauses (HivePress) are seen - Verify: wp eval-file tests/live/run.php router (7/7); qbench Q04/Q06/Q08/Q14/Q16 == native on dev36 - PR: PR-060
This commit is contained in:
@@ -39,6 +39,7 @@ Exit code 0 when every test passed, 1 otherwise. Output per test:
|
||||
| `shadow_write` | writes in shadow_read reach native meta (user, term, HivePress listing) |
|
||||
| `serialized` | array meta values are stored serialized in flat tables, never "Array" |
|
||||
| `resync` | dual_write / shadow_read: flat follows native after every write (4 entities × 2 modes: update, same value, delete, refused writes, stale flat, key rename by meta_id) |
|
||||
| `router` | a routed WP_Query returns the same ids, in order, as native (OR, several post types, untyped numeric compares, named orderby, LIKE) |
|
||||
| `zero_side_effect` | zone tables that were never created are never queried on save / delete |
|
||||
|
||||
## Tools (`tools/`)
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
<?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).
|
||||
*
|
||||
* 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}
|
||||
*/
|
||||
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 {
|
||||
foreach ( array( 'pre_get_posts', 'posts_join', 'posts_where', 'posts_groupby', 'posts_orderby', 'posts_request', 'found_posts_query' ) as $hook ) {
|
||||
$priority = has_filter( $hook, array( $router, $hook ) );
|
||||
if ( false !== $priority ) {
|
||||
remove_filter( $hook, array( $router, $hook ), $priority );
|
||||
}
|
||||
}
|
||||
}
|
||||
tmdo_live_assert_same( '', $wpdb->last_error, 'SQL error' );
|
||||
return array(
|
||||
'ids' => array_map( 'intval', $q->posts ),
|
||||
'routed' => ! empty( $q->get( 'wpdo_post_clauses' ) ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function tmdo_live_router_assert_native( array $args, array $ids, ?bool $expect_routed = null ): void {
|
||||
$native = tmdo_live_router_query( $args, $ids, false );
|
||||
$routed = tmdo_live_router_query( $args, $ids, true );
|
||||
tmdo_live_assert_same( $native['ids'], $routed['ids'], 'routed ids (in order) vs native' );
|
||||
if ( null !== $expect_routed ) {
|
||||
tmdo_live_assert_same( $expect_routed, $routed['routed'], 'query routed to the flat table' );
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
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 )
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
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 () {
|
||||
TMDO_Live_Fixtures::field( 'post', '_sku', 'product' );
|
||||
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
||||
$p = tmdo_live_router_posts(
|
||||
'product',
|
||||
array(
|
||||
'P1' => array( '_sku' => 'tmdo-live-B1-1' ),
|
||||
'P2' => array( '_sku' => 'tmdo-live-B1-10' ),
|
||||
'P3' => array( '_sku' => 'tmdo-live-B2-1' ),
|
||||
)
|
||||
);
|
||||
foreach ( array( 'B1-1', 'B1_1' ) as $needle ) {
|
||||
tmdo_live_router_assert_native(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'meta_query' => array( array( 'key' => '_sku', 'value' => $needle, 'compare' => 'LIKE' ) ),
|
||||
),
|
||||
array_values( $p ),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user