>, remaining: array} Extracted and leftover clauses. */ public static function extract_hot_clauses( array $raw_meta_query, array $post_types ): array { $registry = TMDO_Schema_Registry::instance(); $hot_clauses = array(); $remaining = array(); foreach ( $raw_meta_query as $k => $clause ) { if ( 'relation' === $k || ! is_array( $clause ) || ! isset( $clause['key'] ) ) { $remaining[ $k ] = $clause; continue; } $matched = false; foreach ( $post_types as $pt ) { $field = $registry->get_field( $pt, $clause['key'] ); if ( $field && 'hot' === $field['zone'] && TMDO_Routing_Predicate::should_query_from_zone( $pt, 'hot' ) ) { $hot_clauses[ $pt ][] = array( 'column' => $field['column'], 'value' => $clause['value'] ?? '', 'compare' => strtoupper( trim( $clause['compare'] ?? '=' ) ), 'type' => strtoupper( trim( $clause['type'] ?? 'CHAR' ) ), ); $matched = true; break; } } if ( ! $matched ) { $remaining[ $k ] = $clause; } } return array( 'hot_clauses' => $hot_clauses, 'remaining' => $remaining, ); } /** * Detect hot-table clauses in meta_query and stash them for later hooks. * * @param \WP_Query $query The WP_Query object. * @return void */ public function pre_get_posts( \WP_Query $query ): void { if ( is_admin() && ! wp_doing_ajax() ) { return; } $post_types = (array) $query->get( 'post_type' ); if ( empty( $post_types ) ) { return; } $raw_meta_query = (array) $query->get( 'meta_query' ); if ( empty( $raw_meta_query ) ) { return; } $result = self::extract_hot_clauses( $raw_meta_query, $post_types ); if ( empty( $result['hot_clauses'] ) ) { return; } $remaining = $result['remaining']; if ( isset( $raw_meta_query['relation'] ) && ! isset( $remaining['relation'] ) ) { $remaining['relation'] = $raw_meta_query['relation']; } $query->set( 'meta_query', $remaining ); $query->set( self::QUERY_VAR, $result['hot_clauses'] ); } /** * LEFT JOIN hot tables for each post type with extracted clauses. * * @param string $join Current JOIN SQL. * @param \WP_Query $query The WP_Query object. * @return string Modified JOIN SQL. */ public function posts_join( ?string $join, \WP_Query $query ): string { $join = (string) ( $join ?? '' ); $hot_clauses = $query->get( self::QUERY_VAR ); if ( empty( $hot_clauses ) || ! is_array( $hot_clauses ) ) { return $join; } global $wpdb; foreach ( $hot_clauses as $post_type => $clauses ) { $alias = $this->table_alias( $post_type ); if ( false !== strpos( $join, "`{$alias}`" ) ) { continue; } $table = TMDO_Zone_Hot::table( $post_type ); $join .= " LEFT JOIN `{$table}` AS `{$alias}`" . " ON (`{$wpdb->posts}`.`ID` = `{$alias}`.`post_id`)"; } return $join; } /** * Append WHERE conditions for hot table columns. * * @param string $where Current WHERE SQL. * @param \WP_Query $query The WP_Query object. * @return string Modified WHERE SQL. */ public function posts_where( ?string $where, \WP_Query $query ): string { $where = (string) ( $where ?? '' ); $hot_clauses = $query->get( self::QUERY_VAR ); if ( empty( $hot_clauses ) || ! is_array( $hot_clauses ) ) { return $where; } foreach ( $hot_clauses as $post_type => $clauses ) { $alias = $this->table_alias( $post_type ); foreach ( $clauses as $clause ) { $col = '`' . $alias . '`.`' . esc_sql( $clause['column'] ) . '`'; $compare = $this->sanitize_compare( $clause['compare'] ); $type = $clause['type']; $value = $clause['value']; $where .= $this->build_condition( $col, $compare, $type, $value ); } } return $where; } /** * Ensure GROUP BY to prevent duplicate rows from JOINs. * * @param string $groupby Current GROUP BY SQL. * @param \WP_Query $query The WP_Query object. * @return string Modified GROUP BY SQL. */ public function posts_groupby( ?string $groupby, \WP_Query $query ): string { $groupby = (string) ( $groupby ?? '' ); $hot_clauses = $query->get( self::QUERY_VAR ); if ( empty( $hot_clauses ) || ! is_array( $hot_clauses ) ) { return $groupby; } global $wpdb; if ( '' === trim( $groupby ) ) { $groupby = "`{$wpdb->posts}`.`ID`"; } return $groupby; } // ── Private helpers ─────────────────────────────────────────────────── /** * Generate a unique alias for the hot table JOIN. * * @param string $post_type Post type slug. * @return string SQL table alias. */ private function table_alias( string $post_type ): string { return 'wpdo_hot_' . sanitize_key( $post_type ); } /** * Build a single WHERE condition with prepared values. * * @param string $col Column reference (escaped). * @param string $compare Comparison operator. * @param string $type Meta type for placeholder selection. * @param mixed $value Value(s) to compare against. * @return string SQL WHERE condition fragment. */ private function build_condition( string $col, string $compare, string $type, $value ): string { global $wpdb; $ph = $this->placeholder( $type ); switch ( $compare ) { case 'IN': case 'NOT IN': $vals = array_values( (array) $value ); if ( empty( $vals ) ) { return ' AND 1=0'; } $phs = implode( ',', array_fill( 0, count( $vals ), $ph ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare return $wpdb->prepare( " AND {$col} {$compare} ({$phs})", ...$vals ); case 'BETWEEN': case 'NOT BETWEEN': $vals = array_values( (array) $value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $col/$compare/$ph are sanitized; $ph is a literal placeholder string. return $wpdb->prepare( " AND {$col} {$compare} {$ph} AND {$ph}", $vals[0], $vals[1] ?? $vals[0] ); case 'EXISTS': return " AND {$col} IS NOT NULL"; case 'NOT EXISTS': return " AND {$col} IS NULL"; default: // =, !=, >, >=, <, <=, LIKE, NOT LIKE // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $col/$compare/$ph are sanitized; $ph is a literal placeholder string. return $wpdb->prepare( " AND {$col} {$compare} {$ph}", $value ); } } /** * Returns the SQL placeholder string for a given meta type. * * @param string $type Meta type (NUMERIC, SIGNED, etc. or CHAR). * @return string SQL placeholder (%d or %s). */ private function placeholder( string $type ): string { $int_types = array( 'NUMERIC', 'SIGNED', 'UNSIGNED', 'INTEGER' ); return in_array( $type, $int_types, true ) ? '%d' : '%s'; } /** * Sanitizes a comparison operator against an allowed list. * * @param string $compare Comparison operator string. * @return string Sanitized comparison operator, defaults to '='. */ private function sanitize_compare( string $compare ): string { $allowed = array( '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'EXISTS', 'NOT EXISTS', ); return in_array( $compare, $allowed, true ) ? $compare : '='; } }