Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81324903a6 | |||
| 3e96357459 | |||
| 80343ec282 | |||
| 33addd1e49 | |||
| bed3f906da | |||
| 7ad65eab95 |
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: 2meet Data Optimizer
|
||||
* Plugin URI: https://2meet.io/2meet-data-optimizer
|
||||
* Description: 通用 WordPress 反 EAV 引擎:將 postmeta / usermeta / termmeta / commentmeta 自動分流至四象限扁平表(Hot / Warm / Cold / Archive),大幅提升搜尋與篩選效能。從 wp-data-optimizer v2.16.0 提煉的純核心,整合層交給 11 個 AddOn。
|
||||
* Version: 1.4.0
|
||||
* Version: 1.4.1
|
||||
* Requires at least: 6.0
|
||||
* Tested up to: 6.9.4
|
||||
* Requires PHP: 8.1
|
||||
@@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
}
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────
|
||||
define( 'TMDO_VERSION', '1.4.0' );
|
||||
define( 'TMDO_VERSION', '1.4.1' );
|
||||
define( 'TMDO_DB_VERSION', '2.1.0' );
|
||||
define( 'TMDO_PATH', plugin_dir_path( __FILE__ ) );
|
||||
define( 'TMDO_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
@@ -7,6 +7,37 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
## [1.4.1] — 2026-09-28 — 2.4 驗收修正
|
||||
|
||||
**性質**:修正。無 schema 變更。來源:2.4 全面驗收的 security review 與 correctness review。
|
||||
|
||||
### Fixed
|
||||
|
||||
- **meta_key 的排序不再被清掉**:查詢帶 `meta_key`,且 orderby 在多個排序(陣列或
|
||||
`'meta_value date'`)中仍有 `meta_value`、`meta_value_num` 或該 key 名稱時,1.4.0 會把
|
||||
meta_key 當 EXISTS 路由並清空,WordPress 因此丟掉價格排序。現在只在 orderby 完全沒有引用它時
|
||||
才路由;`meta_compare_key` 不是 `=`(例如 LIKE 會比對到其他 key)時維持原生;排序形狀的
|
||||
group 只有在 orderby 沒有引用它、`meta_value(_num)` 或它的 key 時才當成未使用而略過(沒有
|
||||
meta_key 時它是 WordPress 的主要 clause)。(PR-130)
|
||||
- **ORDER BY 只改寫單獨的 `posts.ID`**:flat-first 與 copy route 共用 `ties_on_post_id()`;
|
||||
外掛加上的 `FIELD(wp_posts.ID, …)`、`(wp_posts.ID IN (…))` 等運算式保持原樣。1.4.0 會在
|
||||
運算式內插入方向,SQL 語法錯誤、頁面沒有結果。正則失敗(PCRE 回溯或 JIT 上限)時回傳原
|
||||
SQL,不再變成空字串。(PR-131)
|
||||
- 呼叫端以 `new WP_Query( array( … ) )` 預設的 router 內部 query var(`wpdo_post_clauses`、
|
||||
`wpdo_post_sort`、`wpdo_post_copy_route`、`wpdo_post_count_sql`)一律清除;copy route 的
|
||||
排序方向改用白名單。無法從網址觸發。(PR-132)
|
||||
- 新文章建立的空 flat 列一併寫入 `post_status` / `post_date` 副本(`wp_insert_post` 在
|
||||
`transition_post_status` 之後才執行,原本留下 NULL,會讓整個 group 關閉 copy route);
|
||||
`wp tmdo post-status-sync` 完成後遞增路由計數版本(dry run 不遞增)。(PR-133)
|
||||
|
||||
### 已知、未修改
|
||||
|
||||
- 1.4.0 的 flat-first 排序同值時依 ID 與排序同方向(原生為 ID DESC),集合與總數相同(已接受)。
|
||||
- 若日後有 group 註冊名為 `post_status` / `post_date` 的欄位,遷移引擎寫副本時 `$data` 與
|
||||
`$formats` 會錯位;目前沒有這樣的 group。
|
||||
|
||||
---
|
||||
|
||||
## [1.4.0] — 2026-09-28 — 百萬級 listing 的路由排序與日期查詢(hardening 2.4)
|
||||
|
||||
**性質**:效能 + 修正。無 schema 變更;post status/date 副本的索引改為三欄(見「升級後」)。
|
||||
|
||||
@@ -163,6 +163,10 @@ final class TMDO_Post_Status_Sync {
|
||||
}
|
||||
self::set_state( $group, 'ready' );
|
||||
}
|
||||
// Routed counts cached while the copies were stale must go.
|
||||
if ( ! $dry_run && class_exists( 'TMDO_Post_Query_Router' ) ) {
|
||||
TMDO_Post_Query_Router::bump_count_version();
|
||||
}
|
||||
return $report;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,9 +125,17 @@ class TMDO_Post_Query_Router {
|
||||
global $wpdb;
|
||||
foreach ( TMDO_Post_Fields::groups_for_post_type( $post->post_type ) as $group ) {
|
||||
$table = TMDO_Schema_Manager::get_table_name( 'post', $group );
|
||||
if ( TMDO_Schema_Manager::table_exists( $table ) ) {
|
||||
$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `{$table}` (`post_id`) VALUES (%d)", (int) $post_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
if ( ! TMDO_Schema_Manager::table_exists( $table ) ) {
|
||||
continue;
|
||||
}
|
||||
// With the status / date copies: this runs after transition_post_status,
|
||||
// and one NULL copy turns the copy route off for the whole group.
|
||||
$copies = class_exists( 'TMDO_Post_Status_Sync' ) ? TMDO_Post_Status_Sync::insert_columns( (int) $post_id, $group ) : array();
|
||||
if ( $copies ) {
|
||||
$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `{$table}` (`post_id`, `post_status`, `post_date`) VALUES (%d, %s, %s)", (int) $post_id, $copies['post_status'], $copies['post_date'] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
continue;
|
||||
}
|
||||
$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `{$table}` (`post_id`) VALUES (%d)", (int) $post_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +146,11 @@ class TMDO_Post_Query_Router {
|
||||
* @return void
|
||||
*/
|
||||
public function pre_get_posts( \WP_Query $query ): void {
|
||||
// The router's query vars are its own: drop any a caller passed in.
|
||||
foreach ( array( self::QUERY_VAR, self::SORT_VAR, self::COPY_VAR, self::COUNT_VAR ) as $var ) {
|
||||
$query->set( $var, '' );
|
||||
}
|
||||
|
||||
// Mode gate: only run when post mode is reads_from_flat.
|
||||
if ( ! self::is_router_active() ) {
|
||||
return;
|
||||
@@ -172,8 +185,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 +208,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 +238,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 ) {
|
||||
@@ -380,7 +402,7 @@ class TMDO_Post_Query_Router {
|
||||
if ( empty( $copy ) ) {
|
||||
return $orderby;
|
||||
}
|
||||
return '`' . self::table_alias( $copy['post_type'], $copy['group'] ) . '`.`post_date` ' . $copy['order'];
|
||||
return '`' . self::table_alias( $copy['post_type'], $copy['group'] ) . '`.`post_date` ' . ( 'ASC' === $copy['order'] ? 'ASC' : 'DESC' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -769,22 +791,58 @@ class TMDO_Post_Query_Router {
|
||||
if ( empty( $copy ) ) {
|
||||
return $request;
|
||||
}
|
||||
return self::ties_on_post_id( self::without_group_by( $request ), self::table_alias( $copy['post_type'], $copy['group'] ), '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* ORDER BY terms that are posts.ID alone (with or without a direction)
|
||||
* read as the equal flat post_id — in $direction, or each in its own when
|
||||
* $direction is '' — and only once. posts.ID inside an expression a plugin
|
||||
* added (FIELD(), IN …) is left alone: a direction inserted there breaks
|
||||
* the SQL.
|
||||
*
|
||||
* @param string $request SELECT statement.
|
||||
* @param string $alias Flat table alias.
|
||||
* @param string $direction 'ASC', 'DESC' or '' (keep each term's own).
|
||||
* @return string
|
||||
*/
|
||||
private static function ties_on_post_id( string $request, string $alias, string $direction ): string {
|
||||
global $wpdb;
|
||||
$tie = '`' . self::table_alias( $copy['post_type'], $copy['group'] ) . '`.`post_id` ';
|
||||
return (string) preg_replace_callback(
|
||||
'/\bORDER BY\b(?!.*\bORDER BY\b).*$/s',
|
||||
static function ( $m ) use ( $wpdb, $tie ) {
|
||||
$order = (string) preg_replace_callback(
|
||||
'/`?' . preg_quote( $wpdb->posts, '/' ) . '`?\.`?ID`?(?!\w)(?:\s+(ASC|DESC)\b)?/i',
|
||||
static fn( $id ) => $tie . ( 'DESC' === strtoupper( $id[1] ?? '' ) ? 'DESC' : 'ASC' ),
|
||||
$m[0]
|
||||
);
|
||||
return (string) preg_replace( '/(,\s*' . preg_quote( $tie, '/' ) . '(ASC|DESC))(\s*,\s*' . preg_quote( $tie, '/' ) . '\2)+/', '$1', $order );
|
||||
},
|
||||
self::without_group_by( $request )
|
||||
$tie = "`{$alias}`.`post_id` ";
|
||||
$id = '`?' . preg_quote( $wpdb->posts, '/' ) . '`?\.`?ID`?';
|
||||
return self::replaced(
|
||||
preg_replace_callback(
|
||||
'/\bORDER BY\b(?!.*\bORDER BY\b).*$/s',
|
||||
static function ( $m ) use ( $id, $tie, $direction ) {
|
||||
$order = self::replaced(
|
||||
preg_replace_callback(
|
||||
'/(?<=ORDER BY |,)(\s*)' . $id . '(?:\s+(ASC|DESC)\b)?(?=\s*(?:,|\bLIMIT\b|$))/i',
|
||||
static fn( $t ) => $t[1] . $tie . ( '' !== $direction ? $direction : ( 'DESC' === strtoupper( $t[2] ?? '' ) ? 'DESC' : 'ASC' ) ),
|
||||
$m[0]
|
||||
),
|
||||
$m[0]
|
||||
);
|
||||
return self::replaced( preg_replace( '/(,\s*' . preg_quote( $tie, '/' ) . '(ASC|DESC))(\s*,\s*' . preg_quote( $tie, '/' ) . '\2)+/', '$1', $order ), $order );
|
||||
},
|
||||
$request
|
||||
),
|
||||
$request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A preg_* result, or the text it was given when PCRE failed (backtrack
|
||||
* or JIT stack limit on a very long request): the query stays unrouted
|
||||
* in shape rather than empty.
|
||||
*
|
||||
* @param string|null $result preg_* return value.
|
||||
* @param string $fallback Input text.
|
||||
* @return string
|
||||
*/
|
||||
private static function replaced( ?string $result, string $fallback ): string {
|
||||
return $result ?? $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* WordPress groups by posts.ID for any tax or meta query. With a flat row
|
||||
* (one per post) as the only join there is nothing to collapse, and the
|
||||
@@ -799,7 +857,7 @@ class TMDO_Post_Query_Router {
|
||||
if ( 1 !== preg_match_all( '/\bJOIN\b/i', $request ) ) {
|
||||
return $request;
|
||||
}
|
||||
return (string) preg_replace( '/\s+GROUP BY\s+`?' . preg_quote( $wpdb->posts, '/' ) . '`?\.`?ID`?(?=\s)/', ' ', $request, 1 );
|
||||
return self::replaced( preg_replace( '/\s+GROUP BY\s+`?' . preg_quote( $wpdb->posts, '/' ) . '`?\.`?ID`?(?=\s)/', ' ', $request, 1 ), $request );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -873,16 +931,9 @@ class TMDO_Post_Query_Router {
|
||||
// post_id, once, in the sort's direction: a (column, post_id) index then
|
||||
// returns rows in order with no filesort. Ties are ordered by ID the same
|
||||
// way as the column (natively ID DESC); pagination stays stable.
|
||||
$tie = "`{$alias}`.`post_id` " . ( 'ASC' === strtoupper( (string) $sort['order'] ) ? 'ASC' : 'DESC' );
|
||||
$flat_first = preg_replace_callback(
|
||||
'/\bORDER BY\b(?!.*\bORDER BY\b).*$/s',
|
||||
static function ( $m ) use ( $wpdb, $tie ) {
|
||||
$order = (string) preg_replace( '/`?' . preg_quote( $wpdb->posts, '/' ) . '`?\.`?ID`?(?!\w)(\s+(ASC|DESC)\b)?/i', $tie, $m[0] );
|
||||
return (string) preg_replace( '/(,\s*' . preg_quote( $tie, '/' ) . ')(\s*,\s*' . preg_quote( $tie, '/' ) . ')+/', '$1', $order );
|
||||
},
|
||||
(string) $flat_first
|
||||
);
|
||||
return (string) preg_replace( '/^\s*SELECT\s/', 'SELECT STRAIGHT_JOIN ', (string) $flat_first, 1 );
|
||||
$direction = 'ASC' === strtoupper( (string) $sort['order'] ) ? 'ASC' : 'DESC';
|
||||
$flat_first = self::ties_on_post_id( $flat_first, $alias, $direction );
|
||||
return self::replaced( preg_replace( '/^\s*SELECT\s/', 'SELECT STRAIGHT_JOIN ', $flat_first, 1 ), $request );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -892,7 +943,7 @@ class TMDO_Post_Query_Router {
|
||||
* @return string
|
||||
*/
|
||||
private static function without_found_rows( string $request ): string {
|
||||
return (string) preg_replace( '/\bSQL_CALC_FOUND_ROWS\s+/', '', $request, 1 );
|
||||
return self::replaced( preg_replace( '/\bSQL_CALC_FOUND_ROWS\s+/', '', $request, 1 ), $request );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-1
@@ -4,7 +4,7 @@ Tags: performance, database, postmeta, optimization, hivepress
|
||||
Requires at least: 6.0
|
||||
Tested up to: 6.9
|
||||
Requires PHP: 8.1
|
||||
Stable tag: 1.4.0
|
||||
Stable tag: 1.4.1
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
@@ -107,6 +107,12 @@ The Classifier tab analyzes your existing `wp_postmeta` data (access frequency,
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.4.1 =
|
||||
* A query ordered by its meta_key among several orderings (or by the key's name) keeps meta_key native; meta_compare_key other than '=' is not routed; a sort group stays when orderby may use it as the primary clause
|
||||
* Only standalone posts.ID terms in ORDER BY become flat post_id tie-breaks (FIELD()/IN expressions from other plugins are left alone); PCRE failures keep the original SQL
|
||||
* Router query vars preset by callers are dropped
|
||||
* New posts' flat rows get their status/date copies; post-status-sync invalidates cached routed counts
|
||||
|
||||
= 1.4.0 =
|
||||
* Flat-first sorts read the ID tie-break from the flat post_id, so a declared (column, post_id) index returns rows in order; ties now follow the sort direction
|
||||
* Meta clauses without a value route as EXISTS; HivePress's featured-listing lookup on every search page uses the flat table
|
||||
|
||||
@@ -100,6 +100,55 @@ class PostQueryRouterCopyTest extends TestCase {
|
||||
$this->assertStringEndsWith( 'ORDER BY ' . self::ALIAS . '.`post_date` DESC LIMIT 0, 10', $this->request( '', '' ) );
|
||||
}
|
||||
|
||||
public function test_copy_route_leaves_posts_id_inside_expressions_alone(): void {
|
||||
global $wpdb;
|
||||
$sql = $this->request( '', ', FIELD({posts}.ID,5,9) DESC, ({posts}.ID IN (3,4)) DESC, {posts}.ID DESC' );
|
||||
|
||||
$this->assertStringEndsWith( "`post_date` DESC, FIELD({$wpdb->posts}.ID,5,9) DESC, ({$wpdb->posts}.ID IN (3,4)) DESC, " . self::ALIAS . '.`post_id` DESC LIMIT 0, 10', $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* The router's query vars are its own: values a caller passed in
|
||||
* (new WP_Query( array( 'wpdo_post_copy_route' => … ) )) are dropped
|
||||
* (2.4 security review L2).
|
||||
*/
|
||||
public function test_query_vars_passed_by_a_caller_are_dropped(): void {
|
||||
$query = new WP_Query();
|
||||
foreach ( array(
|
||||
'post_type' => 'rc_item',
|
||||
'orderby' => 'title',
|
||||
'wpdo_post_copy_route' => array( 'post_type' => 'rc_item', 'group' => 'rc_core', 'statuses' => array( 'publish' ), 'order' => 'DESC, (SELECT 1)' ),
|
||||
'wpdo_post_sort' => array( 'post_type' => 'rc_item', 'group' => 'rc_core', 'column' => 'rc_price', 'order' => 'ASC' ),
|
||||
'wpdo_post_clauses' => array( 'rc_item' => array() ),
|
||||
'wpdo_post_count_sql' => 'SELECT 1',
|
||||
) as $k => $v ) {
|
||||
$query->set( $k, $v );
|
||||
}
|
||||
$router = new TMDO_Post_Query_Router();
|
||||
$router->pre_get_posts( $query );
|
||||
|
||||
foreach ( array( 'wpdo_post_copy_route', 'wpdo_post_sort', 'wpdo_post_clauses', 'wpdo_post_count_sql' ) as $var ) {
|
||||
$this->assertEmpty( $query->get( $var ), $var );
|
||||
}
|
||||
$this->assertSame( 'wp_posts.post_title ASC', $router->posts_orderby( 'wp_posts.post_title ASC', $query ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* wp_insert_post runs after transition_post_status: a new post's empty flat
|
||||
* row must carry the copies itself, or one NULL turns the copy route off for
|
||||
* the whole group (2.4 correctness review).
|
||||
*/
|
||||
public function test_a_new_post_gets_its_flat_row_with_the_copies(): void {
|
||||
global $wpdb;
|
||||
$post = new WP_Post( (object) array( 'ID' => 7, 'post_type' => 'rc_item', 'post_status' => 'draft', 'post_date' => '2026-02-02 00:00:00' ) );
|
||||
$GLOBALS['_wp_posts'][7] = $post;
|
||||
|
||||
( new TMDO_Post_Query_Router() )->add_flat_rows( 7, $post, false );
|
||||
|
||||
$this->assertSame( array( 'post_status' => 'draft', 'post_date' => '2026-02-02 00:00:00' ), $wpdb->get_row( 'SELECT post_status, post_date FROM `' . self::TABLE . '` WHERE post_id = 7', ARRAY_A ) );
|
||||
unset( $GLOBALS['_wp_posts'][7] );
|
||||
}
|
||||
|
||||
public function test_copy_route_keeps_group_by_with_another_join(): void {
|
||||
global $wpdb;
|
||||
$sql = $this->request( " LEFT JOIN wp_term_relationships ON ({$wpdb->posts}.ID = wp_term_relationships.object_id)" );
|
||||
|
||||
@@ -272,6 +272,25 @@ class PostQueryRouterFoundRowsTest extends TestCase {
|
||||
$this->assertSame( array( '2', '3' ), $wpdb->get_col( $sql ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Only standalone posts.ID terms are tie-breaks: inside an expression a
|
||||
* plugin added (pinning posts with FIELD() or IN) the direction must not be
|
||||
* inserted, or the SQL breaks (2.4 correctness review).
|
||||
*/
|
||||
public function test_flat_first_leaves_posts_id_inside_expressions_alone(): void {
|
||||
global $wpdb;
|
||||
list( $router, $query, $request ) = $this->sorted();
|
||||
$pinned = str_replace( ' LIMIT 0, 10', ", FIELD({$wpdb->posts}.ID,3,2) DESC, ({$wpdb->posts}.ID IN (2)) DESC LIMIT 0, 10", $request );
|
||||
|
||||
$sql = $router->posts_request( $pinned, $query );
|
||||
|
||||
$this->assertStringStartsWith( 'SELECT STRAIGHT_JOIN ', $sql );
|
||||
$this->assertStringContainsString( "FIELD({$wpdb->posts}.ID,3,2) DESC, ({$wpdb->posts}.ID IN (2)) DESC LIMIT", $sql );
|
||||
$wpdb->last_error = '';
|
||||
$this->assertSame( $wpdb->get_col( str_replace( 'SQL_CALC_FOUND_ROWS ', '', $pinned ) ), $wpdb->get_col( $sql ) );
|
||||
$this->assertSame( '', $wpdb->last_error );
|
||||
}
|
||||
|
||||
public function test_date_ordered_filtered_request_keeps_wordpress_join_order(): void {
|
||||
list( $router, $query, $request ) = $this->routed();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -132,6 +132,16 @@ class PostStatusSyncTest extends TestCase {
|
||||
$this->assertSame( 2, $report['ps_a']['mismatched'] );
|
||||
}
|
||||
|
||||
/** Routed counts cached while copies were stale are dropped by a sync (2.4 correctness review). */
|
||||
public function test_a_sync_invalidates_cached_routed_counts_but_a_dry_run_does_not(): void {
|
||||
$GLOBALS['_wp_last_changed'][ TMDO_Post_Query_Router::COUNT_VERSION_GROUP ] = 'before';
|
||||
TMDO_Post_Status_Sync::run( true );
|
||||
$this->assertSame( 'before', $GLOBALS['_wp_last_changed'][ TMDO_Post_Query_Router::COUNT_VERSION_GROUP ] );
|
||||
|
||||
TMDO_Post_Status_Sync::run();
|
||||
$this->assertNotSame( 'before', $GLOBALS['_wp_last_changed'][ TMDO_Post_Query_Router::COUNT_VERSION_GROUP ] );
|
||||
}
|
||||
|
||||
public function test_copies_use_the_posts_collation_so_they_compare_with_posts(): void {
|
||||
global $wpdb;
|
||||
TMDO_Post_Status_Sync::run();
|
||||
|
||||
@@ -354,6 +354,19 @@ if ( ! function_exists( 'get_term_meta' ) ) { function get_term_meta( int $tid,
|
||||
if ( ! function_exists( 'update_term_meta' ) ) { function update_term_meta( int $tid, string $key, $value, $prev = '' ): bool { $GLOBALS['_wp_termmeta'][ $tid ][ $key ] = $value; return true; } }
|
||||
if ( ! function_exists( 'get_comment_meta' ) ) { function get_comment_meta( int $cid, string $key = '', bool $single = false ) { return $GLOBALS['_wp_commentmeta'][ $cid ][ $key ] ?? ( $single ? '' : [] ); } }
|
||||
if ( ! function_exists( 'update_comment_meta' ) ) { function update_comment_meta( int $cid, string $key, $value, $prev = '' ): bool { $GLOBALS['_wp_commentmeta'][ $cid ][ $key ] = $value; return true; } }
|
||||
if ( ! class_exists( 'WP_Post' ) ) {
|
||||
final class WP_Post {
|
||||
public $ID = 0;
|
||||
public $post_type = 'post';
|
||||
public $post_status = 'publish';
|
||||
public $post_date = '0000-00-00 00:00:00';
|
||||
public function __construct( $post ) {
|
||||
foreach ( get_object_vars( $post ) as $k => $v ) {
|
||||
$this->$k = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( ! function_exists( 'get_post' ) ) { function get_post( $post_id ) { return $GLOBALS['_wp_posts'][ (int) $post_id ] ?? null; } }
|
||||
if ( ! function_exists( 'get_post_type' ) ) { function get_post_type( $post_id ) { return $GLOBALS['_wp_post_types'][ (int) $post_id ] ?? false; } }
|
||||
if ( ! function_exists( 'get_post_status' ) ) { function get_post_status( $post_id ) { return $GLOBALS['_wp_post_status'][ (int) $post_id ] ?? 'publish'; } }
|
||||
|
||||
Reference in New Issue
Block a user