run:R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
23.8 KB
2026-03-11 16:18:51
R W Run
7.8 KB
2026-03-11 16:18:52
R W Run
36.1 KB
2026-03-11 16:18:51
R W Run
11.9 KB
2026-03-11 16:18:52
R W Run
18.94 KB
2026-03-11 16:18:52
R W Run
7.35 KB
2026-03-11 16:18:52
R W Run
28.6 KB
2026-03-11 16:18:51
R W Run
316 By
2026-03-11 16:18:51
R W Run
12.9 KB
2026-03-11 16:18:51
R W Run
61.02 KB
2026-03-11 16:18:52
R W Run
15 KB
2026-03-11 16:18:51
R W Run
112.05 KB
2026-03-11 16:18:51
R W Run
12.47 KB
2026-03-11 16:18:51
R W Run
15.07 KB
2026-03-11 16:18:52
R W Run
9.84 KB
2026-03-11 16:18:52
R W Run
13.17 KB
2026-03-11 16:18:52
R W Run
33.83 KB
2026-03-11 16:18:51
R W Run
42.63 KB
2026-03-11 16:18:51
R W Run
55.71 KB
2026-03-11 16:18:52
R W Run
12.53 KB
2026-03-11 16:18:51
R W Run
2.55 KB
2026-03-11 16:18:52
R W Run
28.92 KB
2026-03-11 16:18:52
R W Run
539 By
2026-03-11 16:18:51
R W Run
367 By
2026-03-11 16:18:52
R W Run
42.65 KB
2026-03-11 16:18:51
R W Run
401 By
2026-03-11 16:18:51
R W Run
6.61 KB
2026-03-11 16:18:51
R W Run
664 By
2026-03-11 16:18:52
R W Run
20.63 KB
2026-03-11 16:18:51
R W Run
2.18 KB
2026-03-11 16:18:52
R W Run
453 By
2026-03-11 16:18:52
R W Run
457 By
2026-03-11 16:18:51
R W Run
36.83 KB
2026-03-11 16:18:52
R W Run
2.41 KB
2026-03-11 16:18:52
R W Run
8.28 KB
2026-03-11 16:18:51
R W Run
13.89 KB
2026-03-11 16:18:51
R W Run
11.76 KB
2026-03-11 16:18:51
R W Run
2.65 KB
2026-03-11 16:18:51
R W Run
7.43 KB
2026-03-11 16:18:51
R W Run
17.46 KB
2026-03-11 16:18:51
R W Run
5.14 KB
2026-03-11 16:18:52
R W Run
16.7 KB
2026-03-11 16:18:51
R W Run
8.28 KB
2026-03-11 16:18:52
R W Run
2.92 KB
2026-03-11 16:18:52
R W Run
1.32 KB
2026-03-11 16:18:51
R W Run
4.6 KB
2026-03-11 16:18:52
R W Run
11.62 KB
2026-03-11 16:18:52
R W Run
2.5 KB
2026-03-11 16:18:51
R W Run
1.97 KB
2026-03-11 16:18:51
R W Run
11.25 KB
2026-03-11 16:18:52
R W Run
5.32 KB
2026-03-11 16:18:51
R W Run
10.99 KB
2026-03-11 16:18:52
R W Run
68.32 KB
2026-03-11 16:18:51
R W Run
6.34 KB
2026-03-11 16:18:51
R W Run
5.49 KB
2026-03-11 16:18:51
R W Run
1.99 KB
2026-03-11 16:18:52
R W Run
7.02 KB
2026-03-11 16:18:51
R W Run
4.91 KB
2026-03-11 16:18:52
R W Run
16.86 KB
2026-03-11 16:18:51
R W Run
24.23 KB
2026-03-11 16:18:51
R W Run
3.97 KB
2026-03-11 16:18:51
R W Run
47.66 KB
2026-03-11 16:18:51
R W Run
9.22 KB
2026-03-11 16:18:51
R W Run
25.51 KB
2026-03-11 16:18:51
R W Run
198.38 KB
2026-03-11 16:18:52
R W Run
56.65 KB
2026-03-11 16:18:51
R W Run
10.46 KB
2026-03-11 16:18:51
R W Run
10.95 KB
2026-03-11 16:18:52
R W Run
29.26 KB
2026-03-11 16:18:51
R W Run
70.91 KB
2026-03-11 16:18:52
R W Run
35.3 KB
2026-03-11 16:18:52
R W Run
16.61 KB
2026-03-11 16:18:52
R W Run
2.57 KB
2026-03-11 16:18:52
R W Run
39.83 KB
2026-03-11 16:18:51
R W Run
70.64 KB
2026-03-11 16:18:51
R W Run
15.56 KB
2026-03-11 16:18:52
R W Run
7.33 KB
2026-03-11 16:18:52
R W Run
253 By
2026-03-11 16:18:51
R W Run
7.96 KB
2026-03-11 16:18:52
R W Run
3.23 KB
2026-03-11 16:18:52
R W Run
969 By
2026-03-11 16:18:52
R W Run
16.28 KB
2026-03-11 16:18:51
R W Run
7.22 KB
2026-03-11 16:18:51
R W Run
12.95 KB
2026-03-11 16:18:51
R W Run
6.53 KB
2026-03-11 16:18:51
R W Run
3.42 KB
2026-03-11 16:18:52
R W Run
5.84 KB
2026-03-11 16:18:51
R W Run
1.97 KB
2026-03-11 16:18:51
R W Run
4.3 KB
2026-03-11 16:18:52
R W Run
2.91 KB
2026-03-11 16:18:51
R W Run
16.46 KB
2026-03-11 16:18:52
R W Run
40.6 KB
2026-03-11 16:18:51
R W Run
20.22 KB
2026-03-11 16:18:51
R W Run
36.11 KB
2026-03-11 16:18:52
R W Run
17.01 KB
2026-03-11 16:18:51
R W Run
7.27 KB
2026-03-11 16:18:52
R W Run
6.62 KB
2026-03-11 16:18:52
R W Run
16.49 KB
2026-03-11 16:18:52
R W Run
1.79 KB
2026-03-11 16:18:52
R W Run
29.82 KB
2026-03-11 16:18:51
R W Run
6.67 KB
2026-03-11 16:18:52
R W Run
8.98 KB
2026-03-11 16:18:52
R W Run
19.42 KB
2026-03-11 16:18:51
R W Run
12.01 KB
2026-03-11 16:18:51
R W Run
17.11 KB
2026-03-11 16:18:51
R W Run
6.74 KB
2026-03-11 16:18:52
R W Run
30.93 KB
2026-03-11 16:18:51
R W Run
4.99 KB
2026-03-11 16:18:51
R W Run
4.25 KB
2026-03-11 16:18:51
R W Run
24.72 KB
2026-03-11 16:18:51
R W Run
29.96 KB
2026-03-11 16:18:52
R W Run
6.41 KB
2026-03-11 16:18:51
R W Run
160 KB
2026-03-11 16:18:51
R W Run
6.72 KB
2026-03-11 16:18:52
R W Run
10.92 KB
2026-03-11 16:18:51
R W Run
4.77 KB
2026-03-11 16:18:51
R W Run
3.38 KB
2026-03-11 16:18:51
R W Run
11.18 KB
2026-03-11 16:18:51
R W Run
62.19 KB
2026-03-11 16:18:51
R W Run
2.46 KB
2026-03-11 16:18:51
R W Run
9.17 KB
2026-03-11 16:18:51
R W Run
32.15 KB
2026-03-11 16:18:51
R W Run
34.05 KB
2026-03-11 16:18:52
R W Run
7.15 KB
2026-03-11 16:18:51
R W Run
3.47 KB
2026-03-11 16:18:52
R W Run
1.87 KB
2026-03-11 16:18:52
R W Run
30.91 KB
2026-03-11 16:18:51
R W Run
7.29 KB
2026-03-11 16:18:52
R W Run
7.35 KB
2026-03-11 16:18:51
R W Run
12.54 KB
2026-03-11 16:18:51
R W Run
19.12 KB
2026-03-11 16:18:51
R W Run
18.12 KB
2026-03-11 16:18:52
R W Run
39.99 KB
2026-03-11 16:18:52
R W Run
5.17 KB
2026-03-11 16:18:52
R W Run
979 By
2026-03-11 16:18:51
R W Run
18.44 KB
2026-03-11 16:18:52
R W Run
10.24 KB
2026-03-11 16:18:51
R W Run
1.77 KB
2026-03-11 16:18:52
R W Run
34.9 KB
2026-03-11 16:18:51
R W Run
7.19 KB
2026-03-11 16:18:52
R W Run
160.5 KB
2026-03-11 16:18:51
R W Run
64.27 KB
2026-03-11 16:18:51
R W Run
27.95 KB
2026-03-11 16:18:51
R W Run
4.69 KB
2026-03-11 16:18:51
R W Run
2.94 KB
2026-03-11 16:18:51
R W Run
43.13 KB
2026-03-11 16:18:52
R W Run
2.25 KB
2026-03-11 16:18:52
R W Run
22.5 KB
2026-03-11 16:18:51
R W Run
13.01 KB
2026-03-11 16:18:52
R W Run
3.27 KB
2026-03-11 16:18:51
R W Run
18 KB
2026-03-11 16:18:51
R W Run
210.4 KB
2026-03-11 16:18:52
R W Run
25.86 KB
2026-03-11 16:18:52
R W Run
115.85 KB
2026-03-11 16:18:51
R W Run
373 By
2026-03-11 16:18:52
R W Run
343 By
2026-03-11 16:18:52
R W Run
338 By
2026-03-11 16:18:51
R W Run
100.73 KB
2026-03-11 16:18:52
R W Run
130.93 KB
2026-03-11 16:18:51
R W Run
19.1 KB
2026-03-11 16:18:51
R W Run
17.41 KB
2026-03-11 16:18:52
R W Run
41.98 KB
2026-03-11 16:18:52
R W Run
400 By
2026-03-11 16:18:52
R W Run
11.1 KB
2026-03-11 16:18:52
R W Run
37.02 KB
2026-03-11 16:18:51
R W Run
2.24 KB
2026-03-11 16:18:51
R W Run
188.13 KB
2026-03-11 16:18:51
R W Run
338 By
2026-03-11 16:18:51
R W Run
38 KB
2026-03-11 16:18:51
R W Run
4.02 KB
2026-03-11 16:18:52
R W Run
5.38 KB
2026-03-11 16:18:51
R W Run
3.05 KB
2026-03-11 16:18:52
R W Run
2.61 KB
2026-03-11 16:18:51
R W Run
1.16 KB
2026-03-11 16:18:52
R W Run
4.04 KB
2026-03-11 16:18:51
R W Run
3.71 KB
2026-03-11 16:18:51
R W Run
24.6 KB
2026-03-11 16:18:51
R W Run
9.56 KB
2026-03-11 16:18:51
R W Run
346.43 KB
2026-03-11 16:18:52
R W Run
281.84 KB
2026-03-11 16:18:52
R W Run
14.95 KB
2026-03-11 16:18:51
R W Run
8.44 KB
2026-03-11 16:18:52
R W Run
168.95 KB
2026-03-11 16:18:52
R W Run
20.71 KB
2026-03-11 16:18:52
R W Run
25.27 KB
2026-03-11 16:18:51
R W Run
5.72 KB
2026-03-11 16:18:51
R W Run
4.63 KB
2026-03-11 16:18:52
R W Run
81.73 KB
2026-03-11 16:18:51
R W Run
67.18 KB
2026-03-11 16:18:51
R W Run
156.36 KB
2026-03-11 16:18:52
R W Run
55.19 KB
2026-03-11 16:18:51
R W Run
162 By
2026-03-11 16:18:51
R W Run
61.72 KB
2026-03-11 16:18:51
R W Run
216.06 KB
2026-03-11 16:18:52
R W Run
65.09 KB
2026-03-11 16:18:51
R W Run
25.24 KB
2026-03-11 16:18:52
R W Run
4.81 KB
2026-03-11 16:18:51
R W Run
6.48 KB
2026-03-11 16:18:52
R W Run
21.25 KB
2026-03-11 16:18:51
R W Run
2.79 KB
2026-03-11 16:18:52
R W Run
89.69 KB
2026-03-11 16:18:52
R W Run
19.42 KB
2026-03-11 16:18:52
R W Run
3.69 KB
2026-03-11 16:18:52
R W Run
4.11 KB
2026-03-11 16:18:51
R W Run
40.74 KB
2026-03-11 16:18:51
R W Run
25.38 KB
2026-03-11 16:18:51
R W Run
43.31 KB
2026-03-11 16:18:52
R W Run
102.57 KB
2026-03-11 16:18:52
R W Run
6.18 KB
2026-03-11 16:18:51
R W Run
124.47 KB
2026-03-11 16:18:52
R W Run
35.65 KB
2026-03-11 16:18:52
R W Run
6.94 KB
2026-03-11 16:18:52
R W Run
67.04 KB
2026-03-11 16:18:52
R W Run
10.62 KB
2026-03-11 16:18:51
R W Run
289.35 KB
2026-03-11 16:18:52
R W Run
36.23 KB
2026-03-11 16:18:51
R W Run
200 By
2026-03-11 16:18:52
R W Run
200 By
2026-03-11 16:18:52
R W Run
98.29 KB
2026-03-11 16:18:52
R W Run
30.02 KB
2026-03-11 16:18:52
R W Run
19.03 KB
2026-03-11 16:18:52
R W Run
5.06 KB
2026-03-11 16:18:52
R W Run
255 By
2026-03-11 16:18:51
R W Run
22.66 KB
2026-03-11 16:18:52
R W Run
154.63 KB
2026-03-11 16:18:51
R W Run
9.68 KB
2026-03-11 16:18:51
R W Run
258 By
2026-03-11 16:18:51
R W Run
23.49 KB
2026-03-11 16:18:51
R W Run
3.16 KB
2026-03-11 16:18:51
R W Run
8.4 KB
2026-03-11 16:18:52
R W Run
441 By
2026-03-11 16:18:51
R W Run
7.39 KB
2026-03-11 16:18:51
R W Run
173 KB
2026-03-11 16:18:52
R W Run
544 By
2026-03-11 16:18:52
R W Run
4.17 KB
2026-03-11 16:18:51
R W Run
35.97 KB
2026-03-11 16:18:52
R W Run
1.69 KB
2026-03-11 16:18:51
R W Run
2.84 KB
2026-03-11 16:18:52
R W Run
6.09 KB
2026-03-11 16:18:51
R W Run
8.71 KB
2026-03-11 16:18:51
R W Run
131.84 KB
2026-03-11 16:18:51
R W Run
37.45 KB
2026-03-11 16:18:51
R W Run
173.89 KB
2026-03-11 16:18:51
R W Run
7.09 KB
2026-03-11 16:18:51
R W Run
6.41 KB
2026-03-11 16:18:51
R W Run
1.08 KB
2026-03-11 16:18:51
R W Run
69.46 KB
2026-03-11 16:18:52
R W Run
445 By
2026-03-11 16:18:51
R W Run
799 By
2026-03-11 16:18:52
R W Run
error_log
📄class-wp-tax-query.php
1<?php
2/**
3 * Taxonomy API: WP_Tax_Query class
4 *
5 * @package WordPress
6 * @subpackage Taxonomy
7 * @since 4.4.0
8 */
9
10/**
11 * Core class used to implement taxonomy queries for the Taxonomy API.
12 *
13 * Used for generating SQL clauses that filter a primary query according to object
14 * taxonomy terms.
15 *
16 * WP_Tax_Query is a helper that allows primary query classes, such as WP_Query, to filter
17 * their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be
18 * attached to the primary SQL query string.
19 *
20 * @since 3.1.0
21 */
22#[AllowDynamicProperties]
23class WP_Tax_Query {
24
25 /**
26 * Array of taxonomy queries.
27 *
28 * See WP_Tax_Query::__construct() for information on tax query arguments.
29 *
30 * @since 3.1.0
31 * @var array
32 */
33 public $queries = array();
34
35 /**
36 * The relation between the queries. Can be one of 'AND' or 'OR'.
37 *
38 * @since 3.1.0
39 * @var string
40 */
41 public $relation;
42
43 /**
44 * Standard response when the query should not return any rows.
45 *
46 * @since 3.2.0
47 * @var array<string, array<string>>
48 */
49 private static $no_results = array(
50 'join' => array( '' ),
51 'where' => array( '0 = 1' ),
52 );
53
54 /**
55 * A flat list of table aliases used in the JOIN clauses.
56 *
57 * @since 4.1.0
58 * @var array
59 */
60 protected $table_aliases = array();
61
62 /**
63 * Terms and taxonomies fetched by this query.
64 *
65 * We store this data in a flat array because they are referenced in a
66 * number of places by WP_Query.
67 *
68 * @since 4.1.0
69 * @var array
70 */
71 public $queried_terms = array();
72
73 /**
74 * Database table that where the metadata's objects are stored (eg $wpdb->users).
75 *
76 * @since 4.1.0
77 * @var string
78 */
79 public $primary_table;
80
81 /**
82 * Column in 'primary_table' that represents the ID of the object.
83 *
84 * @since 4.1.0
85 * @var string
86 */
87 public $primary_id_column;
88
89 /**
90 * Constructor.
91 *
92 * @since 3.1.0
93 * @since 4.1.0 Added support for `$operator` 'NOT EXISTS' and 'EXISTS' values.
94 *
95 * @param array $tax_query {
96 * Array of taxonomy query clauses.
97 *
98 * @type string $relation Optional. The MySQL keyword used to join
99 * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'.
100 * @type array ...$0 {
101 * An array of first-order clause parameters, or another fully-formed tax query.
102 *
103 * @type string $taxonomy Taxonomy being queried. Optional when field=term_taxonomy_id.
104 * @type string|int|array $terms Term or terms to filter by.
105 * @type string $field Field to match $terms against. Accepts 'term_id', 'slug',
106 * 'name', or 'term_taxonomy_id'. Default: 'term_id'.
107 * @type string $operator MySQL operator to be used with $terms in the WHERE clause.
108 * Accepts 'AND', 'IN', 'NOT IN', 'EXISTS', 'NOT EXISTS'.
109 * Default: 'IN'.
110 * @type bool $include_children Optional. Whether to include child terms.
111 * Requires a $taxonomy. Default: true.
112 * }
113 * }
114 */
115 public function __construct( $tax_query ) {
116 if ( isset( $tax_query['relation'] ) ) {
117 $this->relation = $this->sanitize_relation( $tax_query['relation'] );
118 } else {
119 $this->relation = 'AND';
120 }
121
122 $this->queries = $this->sanitize_query( $tax_query );
123 }
124
125 /**
126 * Ensures the 'tax_query' argument passed to the class constructor is well-formed.
127 *
128 * Ensures that each query-level clause has a 'relation' key, and that
129 * each first-order clause contains all the necessary keys from `$defaults`.
130 *
131 * @since 4.1.0
132 *
133 * @param array $queries Array of queries clauses.
134 * @return array Sanitized array of query clauses.
135 */
136 public function sanitize_query( $queries ) {
137 $cleaned_query = array();
138
139 $defaults = array(
140 'taxonomy' => '',
141 'terms' => array(),
142 'field' => 'term_id',
143 'operator' => 'IN',
144 'include_children' => true,
145 );
146
147 foreach ( $queries as $key => $query ) {
148 if ( 'relation' === $key ) {
149 $cleaned_query['relation'] = $this->sanitize_relation( $query );
150
151 // First-order clause.
152 } elseif ( self::is_first_order_clause( $query ) ) {
153
154 $cleaned_clause = array_merge( $defaults, $query );
155 $cleaned_clause['terms'] = (array) $cleaned_clause['terms'];
156 $cleaned_query[] = $cleaned_clause;
157
158 /*
159 * Keep a copy of the clause in the flate
160 * $queried_terms array, for use in WP_Query.
161 */
162 if ( ! empty( $cleaned_clause['taxonomy'] ) && 'NOT IN' !== $cleaned_clause['operator'] ) {
163 $taxonomy = $cleaned_clause['taxonomy'];
164 if ( ! isset( $this->queried_terms[ $taxonomy ] ) ) {
165 $this->queried_terms[ $taxonomy ] = array();
166 }
167
168 /*
169 * Backward compatibility: Only store the first
170 * 'terms' and 'field' found for a given taxonomy.
171 */
172 if ( ! empty( $cleaned_clause['terms'] ) && ! isset( $this->queried_terms[ $taxonomy ]['terms'] ) ) {
173 $this->queried_terms[ $taxonomy ]['terms'] = $cleaned_clause['terms'];
174 }
175
176 if ( ! empty( $cleaned_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) {
177 $this->queried_terms[ $taxonomy ]['field'] = $cleaned_clause['field'];
178 }
179 }
180
181 // Otherwise, it's a nested query, so we recurse.
182 } elseif ( is_array( $query ) ) {
183 $cleaned_subquery = $this->sanitize_query( $query );
184
185 if ( ! empty( $cleaned_subquery ) ) {
186 // All queries with children must have a relation.
187 if ( ! isset( $cleaned_subquery['relation'] ) ) {
188 $cleaned_subquery['relation'] = 'AND';
189 }
190
191 $cleaned_query[] = $cleaned_subquery;
192 }
193 }
194 }
195
196 return $cleaned_query;
197 }
198
199 /**
200 * Sanitizes a 'relation' operator.
201 *
202 * @since 4.1.0
203 *
204 * @param string $relation Raw relation key from the query argument.
205 * @return string Sanitized relation. Either 'AND' or 'OR'.
206 */
207 public function sanitize_relation( $relation ) {
208 if ( 'OR' === strtoupper( $relation ) ) {
209 return 'OR';
210 } else {
211 return 'AND';
212 }
213 }
214
215 /**
216 * Determines whether a clause is first-order.
217 *
218 * A "first-order" clause is one that contains any of the first-order
219 * clause keys ('terms', 'taxonomy', 'include_children', 'field',
220 * 'operator'). An empty clause also counts as a first-order clause,
221 * for backward compatibility. Any clause that doesn't meet this is
222 * determined, by process of elimination, to be a higher-order query.
223 *
224 * @since 4.1.0
225 *
226 * @param array $query Tax query arguments.
227 * @return bool Whether the query clause is a first-order clause.
228 */
229 protected static function is_first_order_clause( $query ) {
230 return is_array( $query ) && ( empty( $query ) || array_key_exists( 'terms', $query ) || array_key_exists( 'taxonomy', $query ) || array_key_exists( 'include_children', $query ) || array_key_exists( 'field', $query ) || array_key_exists( 'operator', $query ) );
231 }
232
233 /**
234 * Generates SQL clauses to be appended to a main query.
235 *
236 * @since 3.1.0
237 *
238 * @param string $primary_table Database table where the object being filtered is stored (eg wp_users).
239 * @param string $primary_id_column ID column for the filtered object in $primary_table.
240 * @return string[] {
241 * Array containing JOIN and WHERE SQL clauses to append to the main query.
242 *
243 * @type string $join SQL fragment to append to the main JOIN clause.
244 * @type string $where SQL fragment to append to the main WHERE clause.
245 * }
246 */
247 public function get_sql( $primary_table, $primary_id_column ) {
248 $this->primary_table = $primary_table;
249 $this->primary_id_column = $primary_id_column;
250
251 return $this->get_sql_clauses();
252 }
253
254 /**
255 * Generates SQL clauses to be appended to a main query.
256 *
257 * Called by the public WP_Tax_Query::get_sql(), this method
258 * is abstracted out to maintain parity with the other Query classes.
259 *
260 * @since 4.1.0
261 *
262 * @return string[] {
263 * Array containing JOIN and WHERE SQL clauses to append to the main query.
264 *
265 * @type string $join SQL fragment to append to the main JOIN clause.
266 * @type string $where SQL fragment to append to the main WHERE clause.
267 * }
268 */
269 protected function get_sql_clauses() {
270 /*
271 * $queries are passed by reference to get_sql_for_query() for recursion.
272 * To keep $this->queries unaltered, pass a copy.
273 */
274 $queries = $this->queries;
275 $sql = $this->get_sql_for_query( $queries );
276
277 if ( ! empty( $sql['where'] ) ) {
278 $sql['where'] = ' AND ' . $sql['where'];
279 }
280
281 return $sql;
282 }
283
284 /**
285 * Generates SQL clauses for a single query array.
286 *
287 * If nested subqueries are found, this method recurses the tree to
288 * produce the properly nested SQL.
289 *
290 * @since 4.1.0
291 *
292 * @param array $query Query to parse (passed by reference).
293 * @param int $depth Optional. Number of tree levels deep we currently are.
294 * Used to calculate indentation. Default 0.
295 * @return string[] {
296 * Array containing JOIN and WHERE SQL clauses to append to a single query array.
297 *
298 * @type string $join SQL fragment to append to the main JOIN clause.
299 * @type string $where SQL fragment to append to the main WHERE clause.
300 * }
301 */
302 protected function get_sql_for_query( &$query, $depth = 0 ) {
303 $sql_chunks = array(
304 'join' => array(),
305 'where' => array(),
306 );
307
308 $sql = array(
309 'join' => '',
310 'where' => '',
311 );
312
313 $indent = '';
314 for ( $i = 0; $i < $depth; $i++ ) {
315 $indent .= ' ';
316 }
317
318 foreach ( $query as $key => &$clause ) {
319 if ( 'relation' === $key ) {
320 $relation = $query['relation'];
321 } elseif ( is_array( $clause ) ) {
322
323 // This is a first-order clause.
324 if ( $this->is_first_order_clause( $clause ) ) {
325 $clause_sql = $this->get_sql_for_clause( $clause, $query );
326
327 $where_count = count( $clause_sql['where'] );
328 if ( ! $where_count ) {
329 $sql_chunks['where'][] = '';
330 } elseif ( 1 === $where_count ) {
331 $sql_chunks['where'][] = $clause_sql['where'][0];
332 } else {
333 $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
334 }
335
336 $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
337 // This is a subquery, so we recurse.
338 } else {
339 $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
340
341 $sql_chunks['where'][] = $clause_sql['where'];
342 $sql_chunks['join'][] = $clause_sql['join'];
343 }
344 }
345 }
346
347 // Filter to remove empties.
348 $sql_chunks['join'] = array_filter( $sql_chunks['join'] );
349 $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
350
351 if ( empty( $relation ) ) {
352 $relation = 'AND';
353 }
354
355 // Filter duplicate JOIN clauses and combine into a single string.
356 if ( ! empty( $sql_chunks['join'] ) ) {
357 $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
358 }
359
360 // Generate a single WHERE clause with proper brackets and indentation.
361 if ( ! empty( $sql_chunks['where'] ) ) {
362 $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
363 }
364
365 return $sql;
366 }
367
368 /**
369 * Generates SQL JOIN and WHERE clauses for a "first-order" query clause.
370 *
371 * @since 4.1.0
372 *
373 * @global wpdb $wpdb The WordPress database abstraction object.
374 *
375 * @param array $clause Query clause (passed by reference).
376 * @param array $parent_query Parent query array.
377 * @return array {
378 * Array containing JOIN and WHERE SQL clauses to append to a first-order query.
379 *
380 * @type string[] $join Array of SQL fragments to append to the main JOIN clause.
381 * @type string[] $where Array of SQL fragments to append to the main WHERE clause.
382 * }
383 */
384 public function get_sql_for_clause( &$clause, $parent_query ) {
385 global $wpdb;
386
387 $sql = array(
388 'where' => array(),
389 'join' => array(),
390 );
391
392 $join = '';
393 $where = '';
394
395 $this->clean_query( $clause );
396
397 if ( is_wp_error( $clause ) ) {
398 return self::$no_results;
399 }
400
401 $terms = $clause['terms'];
402 $operator = strtoupper( $clause['operator'] );
403
404 if ( 'IN' === $operator ) {
405
406 if ( empty( $terms ) ) {
407 return self::$no_results;
408 }
409
410 $terms = implode( ',', $terms );
411
412 /*
413 * Before creating another table join, see if this clause has a
414 * sibling with an existing join that can be shared.
415 */
416 $alias = $this->find_compatible_table_alias( $clause, $parent_query );
417 if ( false === $alias ) {
418 $i = count( $this->table_aliases );
419 $alias = $i ? 'tt' . $i : $wpdb->term_relationships;
420
421 // Store the alias as part of a flat array to build future iterators.
422 $this->table_aliases[] = $alias;
423
424 // Store the alias with this clause, so later siblings can use it.
425 $clause['alias'] = $alias;
426
427 $join .= " LEFT JOIN $wpdb->term_relationships";
428 $join .= $i ? " AS $alias" : '';
429 $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)";
430 }
431
432 $where = "$alias.term_taxonomy_id $operator ($terms)";
433
434 } elseif ( 'NOT IN' === $operator ) {
435
436 if ( empty( $terms ) ) {
437 return $sql;
438 }
439
440 $terms = implode( ',', $terms );
441
442 $where = "$this->primary_table.$this->primary_id_column NOT IN (
443 SELECT object_id
444 FROM $wpdb->term_relationships
445 WHERE term_taxonomy_id IN ($terms)
446 )";
447
448 } elseif ( 'AND' === $operator ) {
449
450 if ( empty( $terms ) ) {
451 return $sql;
452 }
453
454 $num_terms = count( $terms );
455
456 $terms = implode( ',', $terms );
457
458 $where = "(
459 SELECT COUNT(1)
460 FROM $wpdb->term_relationships
461 WHERE term_taxonomy_id IN ($terms)
462 AND object_id = $this->primary_table.$this->primary_id_column
463 ) = $num_terms";
464
465 } elseif ( 'NOT EXISTS' === $operator || 'EXISTS' === $operator ) {
466
467 $where = $wpdb->prepare(
468 "$operator (
469 SELECT 1
470 FROM $wpdb->term_relationships
471 INNER JOIN $wpdb->term_taxonomy
472 ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id
473 WHERE $wpdb->term_taxonomy.taxonomy = %s
474 AND $wpdb->term_relationships.object_id = $this->primary_table.$this->primary_id_column
475 )",
476 $clause['taxonomy']
477 );
478
479 }
480
481 $sql['join'][] = $join;
482 $sql['where'][] = $where;
483 return $sql;
484 }
485
486 /**
487 * Identifies an existing table alias that is compatible with the current query clause.
488 *
489 * We avoid unnecessary table joins by allowing each clause to look for
490 * an existing table alias that is compatible with the query that it
491 * needs to perform.
492 *
493 * An existing alias is compatible if (a) it is a sibling of `$clause`
494 * (ie, it's under the scope of the same relation), and (b) the combination
495 * of operator and relation between the clauses allows for a shared table
496 * join. In the case of WP_Tax_Query, this only applies to 'IN'
497 * clauses that are connected by the relation 'OR'.
498 *
499 * @since 4.1.0
500 *
501 * @param array $clause Query clause.
502 * @param array $parent_query Parent query of $clause.
503 * @return string|false Table alias if found, otherwise false.
504 */
505 protected function find_compatible_table_alias( $clause, $parent_query ) {
506 $alias = false;
507
508 // Confidence check. Only IN queries use the JOIN syntax.
509 if ( ! isset( $clause['operator'] ) || 'IN' !== $clause['operator'] ) {
510 return $alias;
511 }
512
513 // Since we're only checking IN queries, we're only concerned with OR relations.
514 if ( ! isset( $parent_query['relation'] ) || 'OR' !== $parent_query['relation'] ) {
515 return $alias;
516 }
517
518 $compatible_operators = array( 'IN' );
519
520 foreach ( $parent_query as $sibling ) {
521 if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
522 continue;
523 }
524
525 if ( empty( $sibling['alias'] ) || empty( $sibling['operator'] ) ) {
526 continue;
527 }
528
529 // The sibling must both have compatible operator to share its alias.
530 if ( in_array( strtoupper( $sibling['operator'] ), $compatible_operators, true ) ) {
531 $alias = preg_replace( '/\W/', '_', $sibling['alias'] );
532 break;
533 }
534 }
535
536 return $alias;
537 }
538
539 /**
540 * Validates a single query.
541 *
542 * @since 3.2.0
543 *
544 * @param array $query The single query. Passed by reference.
545 */
546 private function clean_query( &$query ) {
547 if ( empty( $query['taxonomy'] ) ) {
548 if ( 'term_taxonomy_id' !== $query['field'] ) {
549 $query = new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
550 return;
551 }
552
553 // So long as there are shared terms, 'include_children' requires that a taxonomy is set.
554 $query['include_children'] = false;
555 } elseif ( ! taxonomy_exists( $query['taxonomy'] ) ) {
556 $query = new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
557 return;
558 }
559
560 if ( 'slug' === $query['field'] || 'name' === $query['field'] ) {
561 $query['terms'] = array_unique( (array) $query['terms'] );
562 } else {
563 $query['terms'] = wp_parse_id_list( $query['terms'] );
564 }
565
566 if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) {
567 $this->transform_query( $query, 'term_id' );
568
569 if ( is_wp_error( $query ) ) {
570 return;
571 }
572
573 $children = array();
574 foreach ( $query['terms'] as $term ) {
575 $children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) );
576 $children[] = $term;
577 }
578 $query['terms'] = $children;
579 }
580
581 $this->transform_query( $query, 'term_taxonomy_id' );
582 }
583
584 /**
585 * Transforms a single query, from one field to another.
586 *
587 * Operates on the `$query` object by reference. In the case of error,
588 * `$query` is converted to a WP_Error object.
589 *
590 * @since 3.2.0
591 *
592 * @param array $query The single query. Passed by reference.
593 * @param string $resulting_field The resulting field. Accepts 'slug', 'name', 'term_taxonomy_id',
594 * or 'term_id'. Default 'term_id'.
595 */
596 public function transform_query( &$query, $resulting_field ) {
597 if ( empty( $query['terms'] ) ) {
598 return;
599 }
600
601 if ( $query['field'] === $resulting_field ) {
602 return;
603 }
604
605 $resulting_field = sanitize_key( $resulting_field );
606
607 // Empty 'terms' always results in a null transformation.
608 $terms = array_filter( $query['terms'] );
609 if ( empty( $terms ) ) {
610 $query['terms'] = array();
611 $query['field'] = $resulting_field;
612 return;
613 }
614
615 $args = array(
616 'get' => 'all',
617 'number' => 0,
618 'taxonomy' => $query['taxonomy'],
619 'update_term_meta_cache' => false,
620 'orderby' => 'none',
621 );
622
623 // Term query parameter name depends on the 'field' being searched on.
624 switch ( $query['field'] ) {
625 case 'slug':
626 $args['slug'] = $terms;
627 break;
628 case 'name':
629 $args['name'] = $terms;
630 break;
631 case 'term_taxonomy_id':
632 $args['term_taxonomy_id'] = $terms;
633 break;
634 default:
635 $args['include'] = wp_parse_id_list( $terms );
636 break;
637 }
638
639 if ( ! is_taxonomy_hierarchical( $query['taxonomy'] ) ) {
640 $args['number'] = count( $terms );
641 }
642
643 $term_query = new WP_Term_Query();
644 $term_list = $term_query->query( $args );
645
646 if ( is_wp_error( $term_list ) ) {
647 $query = $term_list;
648 return;
649 }
650
651 if ( 'AND' === $query['operator'] && count( $term_list ) < count( $query['terms'] ) ) {
652 $query = new WP_Error( 'inexistent_terms', __( 'Inexistent terms.' ) );
653 return;
654 }
655
656 $query['terms'] = wp_list_pluck( $term_list, $resulting_field );
657 $query['field'] = $resulting_field;
658 }
659}
660