Files
2meet-data-optimizer/includes/engine/class-tmdo-query-compiler.php
T
wpdev d36bb954d1 chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:06:36 +08:00

323 lines
8.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* TMDO_Query_Compiler - 跨實體查詢編譯器
*
* 將 wpdo_meta_query 編譯為高效 SQL
* - 單次 JOIN UAE 表(而非原生 meta_query 多次 JOIN wp_*meta
* - 型別正確的比較(避免 CAST 開銷)
* - 命中索引
*
* @package WP_Data_Optimizer
*/
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
defined( 'ABSPATH' ) || exit;
final class TMDO_Query_Compiler {
/** @var array 合法的 SQL 比較運算子 */
private const VALID_OPERATORS = array(
'=',
'!=',
'<>',
'>',
'>=',
'<',
'<=',
'LIKE',
'NOT LIKE',
'IN',
'NOT IN',
'BETWEEN',
'NOT BETWEEN',
'EXISTS',
'NOT EXISTS',
);
/**
* 注入 wpdo_meta_query 至 WP_Querypost 實體)
*
* @param WP_Query $query
* @param array $wpdo_meta_query 結構同 meta_query
*/
public static function inject_into_wp_query( WP_Query $query, array $wpdo_meta_query ): void {
$post_type = $query->get( 'post_type' );
if ( is_array( $post_type ) ) {
$post_type = $post_type[0] ?? 'post';
}
if ( empty( $post_type ) || $post_type === 'any' ) {
$post_type = 'post';
}
// 編譯 JOIN 與 WHERE
$compiled = self::compile( 'post', $wpdo_meta_query );
if ( empty( $compiled['joins'] ) && empty( $compiled['where'] ) ) {
return;
}
// 注入 posts_clauses filter
add_filter(
'posts_clauses',
function ( $clauses ) use ( $compiled ) {
global $wpdb;
if ( ! empty( $compiled['joins'] ) ) {
$clauses['join'] .= ' ' . implode( ' ', $compiled['joins'] );
}
if ( ! empty( $compiled['where'] ) ) {
$clauses['where'] .= ' AND (' . implode( ' AND ', $compiled['where'] ) . ')';
}
return $clauses;
},
10,
1
);
// wpdo_orderby 支援
$wpdo_orderby = $query->get( 'wpdo_orderby' );
if ( $wpdo_orderby ) {
$order_dir = strtoupper( $query->get( 'order' ) ?: 'DESC' );
if ( ! in_array( $order_dir, array( 'ASC', 'DESC' ), true ) ) {
$order_dir = 'DESC';
}
$wpdo_col = TMDO_Schema_Manager::sanitize_column_name( $wpdo_orderby );
// 找出該欄位所在的群組
$field_def = TMDO_Entity_Registry::get_field( 'post', $wpdo_orderby );
if ( $field_def ) {
$alias = 'wpdo_' . $field_def['group'];
add_filter(
'posts_orderby',
function ( $orderby ) use ( $alias, $wpdo_col, $order_dir ) {
return "`{$alias}`.`{$wpdo_col}` {$order_dir}";
},
10,
1
);
}
}
}
/**
* 注入至 WP_User_Query
*/
public static function inject_into_user_query( WP_User_Query $query, array $wpdo_meta_query ): void {
$compiled = self::compile( 'user', $wpdo_meta_query );
if ( empty( $compiled['joins'] ) && empty( $compiled['where'] ) ) {
return;
}
global $wpdb;
// 透過 reflection 存取 WP_User_Query 的 query_vars 來注入
$query_orderby = &$query->query_orderby;
$query_where = &$query->query_where;
$query_from = &$query->query_from;
if ( ! empty( $compiled['joins'] ) ) {
$query_from .= ' ' . implode( ' ', $compiled['joins'] );
}
if ( ! empty( $compiled['where'] ) ) {
$query_where .= ' AND (' . implode( ' AND ', $compiled['where'] ) . ')';
}
// wpdo_orderby
$wpdo_orderby = $query->get( 'wpdo_orderby' );
if ( $wpdo_orderby ) {
$field_def = TMDO_Entity_Registry::get_field( 'user', $wpdo_orderby );
if ( $field_def ) {
$order_dir = strtoupper( $query->get( 'order' ) ?: 'DESC' );
if ( ! in_array( $order_dir, array( 'ASC', 'DESC' ), true ) ) {
$order_dir = 'DESC';
}
$alias = 'wpdo_' . $field_def['group'];
$col = TMDO_Schema_Manager::sanitize_column_name( $wpdo_orderby );
$query_orderby = "ORDER BY `{$alias}`.`{$col}` {$order_dir}";
}
}
}
/**
* 注入至 get_terms() 的 clauses
*/
public static function inject_into_terms_clauses( array $clauses, array $wpdo_meta_query ): array {
$compiled = self::compile( 'term', $wpdo_meta_query );
if ( empty( $compiled['joins'] ) && empty( $compiled['where'] ) ) {
return $clauses;
}
if ( ! empty( $compiled['joins'] ) ) {
$clauses['join'] .= ' ' . implode( ' ', $compiled['joins'] );
}
if ( ! empty( $compiled['where'] ) ) {
$clauses['where'] .= ' AND (' . implode( ' AND ', $compiled['where'] ) . ')';
}
return $clauses;
}
/**
* 核心編譯邏輯
*
* @param string $entity_type
* @param array $meta_query 結構範例:
* [
* 'relation' => 'AND',
* [ 'key' => 'price', 'value' => [100, 500], 'compare' => 'BETWEEN' ],
* [ 'key' => 'stock', 'value' => 'instock' ],
* ]
* @return array{joins: array<string>, where: array<string>}
*/
public static function compile( string $entity_type, array $meta_query ): array {
$adapter = TMDO_Entity_Registry::get_adapter( $entity_type );
if ( ! $adapter ) {
return array(
'joins' => array(),
'where' => array(),
);
}
$relation = strtoupper( $meta_query['relation'] ?? 'AND' );
if ( ! in_array( $relation, array( 'AND', 'OR' ), true ) ) {
$relation = 'AND';
}
unset( $meta_query['relation'] );
$needed_groups = array(); // group → true
$where_clauses = array();
foreach ( $meta_query as $clause ) {
if ( ! is_array( $clause ) || empty( $clause['key'] ) ) {
continue;
}
$field_def = TMDO_Entity_Registry::get_field( $entity_type, $clause['key'] );
if ( ! $field_def ) {
// 非 UAE 管理的欄位,跳過(讓原生 meta_query 接手)
continue;
}
$group = $field_def['group'];
$needed_groups[ $group ] = true;
$compare = strtoupper( $clause['compare'] ?? '=' );
if ( ! in_array( $compare, self::VALID_OPERATORS, true ) ) {
$compare = '=';
}
$alias = 'wpdo_' . $group;
$col = TMDO_Schema_Manager::sanitize_column_name( $clause['key'] );
$value = $clause['value'] ?? null;
$where_clauses[] = self::build_comparison( $alias, $col, $compare, $value, $field_def );
}
// 建立 JOIN
$joins = array();
$prim_table = $adapter->get_primary_table();
$prim_id = $adapter->get_primary_id_column();
$id_col = $adapter->get_entity_id_column();
foreach ( array_keys( $needed_groups ) as $group ) {
$wpdo_table = TMDO_Schema_Manager::get_table_name( $entity_type, $group );
if ( ! TMDO_Schema_Manager::table_exists( $wpdo_table ) ) {
continue;
}
$alias = 'wpdo_' . $group;
$joins[] = "LEFT JOIN `{$wpdo_table}` `{$alias}` ON `{$prim_table}`.`{$prim_id}` = `{$alias}`.`{$id_col}`";
}
// 組合 where,依 relation 連接
$where_sql = array();
if ( ! empty( $where_clauses ) ) {
$where_sql[] = implode( " {$relation} ", $where_clauses );
}
return array(
'joins' => $joins,
'where' => $where_sql,
);
}
/**
* 產生單一比較子句
*/
private static function build_comparison(
string $alias,
string $col,
string $compare,
$value,
array $field_def
): string {
global $wpdb;
$col_ref = "`{$alias}`.`{$col}`";
switch ( $compare ) {
case 'EXISTS':
return "{$col_ref} IS NOT NULL";
case 'NOT EXISTS':
return "{$col_ref} IS NULL";
case 'BETWEEN':
case 'NOT BETWEEN':
if ( ! is_array( $value ) || count( $value ) !== 2 ) {
return '1=1';
}
$v1 = self::escape_value( $value[0], $field_def );
$v2 = self::escape_value( $value[1], $field_def );
return "{$col_ref} {$compare} {$v1} AND {$v2}";
case 'IN':
case 'NOT IN':
if ( ! is_array( $value ) ) {
$value = array( $value );
}
if ( empty( $value ) ) {
return $compare === 'IN' ? '1=0' : '1=1';
}
$escaped = array_map( fn( $v ) => self::escape_value( $v, $field_def ), $value );
return "{$col_ref} {$compare} (" . implode( ',', $escaped ) . ')';
case 'LIKE':
case 'NOT LIKE':
$like_val = '%' . $wpdb->esc_like( (string) $value ) . '%';
return "{$col_ref} {$compare} '" . esc_sql( $like_val ) . "'";
default:
$escaped = self::escape_value( $value, $field_def );
return "{$col_ref} {$compare} {$escaped}";
}
}
/**
* 型別安全地轉義值
*/
private static function escape_value( $value, array $field_def ): string {
$type = $field_def['type'] ?? 'text';
return match ( $type ) {
'integer', 'boolean' => (string) (int) $value,
'decimal' => (string) (float) $value,
default => "'" . esc_sql( (string) $value ) . "'",
};
}
}