1 | <?php |
---|
2 | /** |
---|
3 | * Meta API: WP_Meta_Query class |
---|
4 | * |
---|
5 | * @package WordPress |
---|
6 | * @subpackage Meta |
---|
7 | * @since 4.4.0 |
---|
8 | */ |
---|
9 | |
---|
10 | /** |
---|
11 | * Core class used to implement meta queries for the Meta API. |
---|
12 | * |
---|
13 | * Used for generating SQL clauses that filter a primary query according to metadata keys and values. |
---|
14 | * |
---|
15 | * WP_Meta_Query is a helper that allows primary query classes, such as WP_Query and WP_User_Query, |
---|
16 | * |
---|
17 | * to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached |
---|
18 | * to the primary SQL query string. |
---|
19 | * |
---|
20 | * @since 3.2.0 |
---|
21 | */ |
---|
22 | class WP_Meta_Query { |
---|
23 | /** |
---|
24 | * Array of metadata queries. |
---|
25 | * |
---|
26 | * See WP_Meta_Query::__construct() for information on meta query arguments. |
---|
27 | * |
---|
28 | * @since 3.2.0 |
---|
29 | * @var array |
---|
30 | */ |
---|
31 | public $queries = array(); |
---|
32 | |
---|
33 | /** |
---|
34 | * The relation between the queries. Can be one of 'AND' or 'OR'. |
---|
35 | * |
---|
36 | * @since 3.2.0 |
---|
37 | * @var string |
---|
38 | */ |
---|
39 | public $relation; |
---|
40 | |
---|
41 | /** |
---|
42 | * Database table to query for the metadata. |
---|
43 | * |
---|
44 | * @since 4.1.0 |
---|
45 | * @var string |
---|
46 | */ |
---|
47 | public $meta_table; |
---|
48 | |
---|
49 | /** |
---|
50 | * Column in meta_table that represents the ID of the object the metadata belongs to. |
---|
51 | * |
---|
52 | * @since 4.1.0 |
---|
53 | * @var string |
---|
54 | */ |
---|
55 | public $meta_id_column; |
---|
56 | |
---|
57 | /** |
---|
58 | * Database table that where the metadata's objects are stored (eg $wpdb->users). |
---|
59 | * |
---|
60 | * @since 4.1.0 |
---|
61 | * @var string |
---|
62 | */ |
---|
63 | public $primary_table; |
---|
64 | |
---|
65 | /** |
---|
66 | * Column in primary_table that represents the ID of the object. |
---|
67 | * |
---|
68 | * @since 4.1.0 |
---|
69 | * @var string |
---|
70 | */ |
---|
71 | public $primary_id_column; |
---|
72 | |
---|
73 | /** |
---|
74 | * A flat list of table aliases used in JOIN clauses. |
---|
75 | * |
---|
76 | * @since 4.1.0 |
---|
77 | * @var array |
---|
78 | */ |
---|
79 | protected $table_aliases = array(); |
---|
80 | |
---|
81 | /** |
---|
82 | * A flat list of clauses, keyed by clause 'name'. |
---|
83 | * |
---|
84 | * @since 4.2.0 |
---|
85 | * @var array |
---|
86 | */ |
---|
87 | protected $clauses = array(); |
---|
88 | |
---|
89 | /** |
---|
90 | * Whether the query contains any OR relations. |
---|
91 | * |
---|
92 | * @since 4.3.0 |
---|
93 | * @var bool |
---|
94 | */ |
---|
95 | protected $has_or_relation = false; |
---|
96 | |
---|
97 | /** |
---|
98 | * Constructor. |
---|
99 | * |
---|
100 | * @since 3.2.0 |
---|
101 | * @since 4.2.0 Introduced support for naming query clauses by associative array keys. |
---|
102 | * |
---|
103 | * |
---|
104 | * @param array $meta_query { |
---|
105 | * Array of meta query clauses. When first-order clauses or sub-clauses use strings as |
---|
106 | * their array keys, they may be referenced in the 'orderby' parameter of the parent query. |
---|
107 | * |
---|
108 | * @type string $relation Optional. The MySQL keyword used to join |
---|
109 | * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'. |
---|
110 | * @type array { |
---|
111 | * Optional. An array of first-order clause parameters, or another fully-formed meta query. |
---|
112 | * |
---|
113 | * @type string $key Meta key to filter by. |
---|
114 | * @type string $value Meta value to filter by. |
---|
115 | * @type string $compare MySQL operator used for comparing the $value. Accepts '=', |
---|
116 | * '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', |
---|
117 | * 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP', |
---|
118 | * 'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'. |
---|
119 | * Default is 'IN' when `$value` is an array, '=' otherwise. |
---|
120 | * @type string $type MySQL data type that the meta_value column will be CAST to for |
---|
121 | * comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', |
---|
122 | * 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'. |
---|
123 | * Default is 'CHAR'. |
---|
124 | * } |
---|
125 | * } |
---|
126 | */ |
---|
127 | public function __construct( $meta_query = false ) { |
---|
128 | if ( !$meta_query ) |
---|
129 | return; |
---|
130 | |
---|
131 | if ( isset( $meta_query['relation'] ) && strtoupper( $meta_query['relation'] ) == 'OR' ) { |
---|
132 | $this->relation = 'OR'; |
---|
133 | } else { |
---|
134 | $this->relation = 'AND'; |
---|
135 | } |
---|
136 | |
---|
137 | $this->queries = $this->sanitize_query( $meta_query ); |
---|
138 | } |
---|
139 | |
---|
140 | /** |
---|
141 | * Ensure the 'meta_query' argument passed to the class constructor is well-formed. |
---|
142 | * |
---|
143 | * Eliminates empty items and ensures that a 'relation' is set. |
---|
144 | * |
---|
145 | * @since 4.1.0 |
---|
146 | * |
---|
147 | * @param array $queries Array of query clauses. |
---|
148 | * @return array Sanitized array of query clauses. |
---|
149 | */ |
---|
150 | public function sanitize_query( $queries ) { |
---|
151 | $clean_queries = array(); |
---|
152 | |
---|
153 | if ( ! is_array( $queries ) ) { |
---|
154 | return $clean_queries; |
---|
155 | } |
---|
156 | |
---|
157 | foreach ( $queries as $key => $query ) { |
---|
158 | if ( 'relation' === $key ) { |
---|
159 | $relation = $query; |
---|
160 | |
---|
161 | } elseif ( ! is_array( $query ) ) { |
---|
162 | continue; |
---|
163 | |
---|
164 | // First-order clause. |
---|
165 | } elseif ( $this->is_first_order_clause( $query ) ) { |
---|
166 | if ( isset( $query['value'] ) && array() === $query['value'] ) { |
---|
167 | unset( $query['value'] ); |
---|
168 | } |
---|
169 | |
---|
170 | $clean_queries[ $key ] = $query; |
---|
171 | |
---|
172 | // Otherwise, it's a nested query, so we recurse. |
---|
173 | } else { |
---|
174 | $cleaned_query = $this->sanitize_query( $query ); |
---|
175 | |
---|
176 | if ( ! empty( $cleaned_query ) ) { |
---|
177 | $clean_queries[ $key ] = $cleaned_query; |
---|
178 | } |
---|
179 | } |
---|
180 | } |
---|
181 | |
---|
182 | if ( empty( $clean_queries ) ) { |
---|
183 | return $clean_queries; |
---|
184 | } |
---|
185 | |
---|
186 | // Sanitize the 'relation' key provided in the query. |
---|
187 | if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) { |
---|
188 | $clean_queries['relation'] = 'OR'; |
---|
189 | $this->has_or_relation = true; |
---|
190 | |
---|
191 | /* |
---|
192 | * If there is only a single clause, call the relation 'OR'. |
---|
193 | * This value will not actually be used to join clauses, but it |
---|
194 | * simplifies the logic around combining key-only queries. |
---|
195 | */ |
---|
196 | } elseif ( 1 === count( $clean_queries ) ) { |
---|
197 | $clean_queries['relation'] = 'OR'; |
---|
198 | |
---|
199 | // Default to AND. |
---|
200 | } else { |
---|
201 | $clean_queries['relation'] = 'AND'; |
---|
202 | } |
---|
203 | |
---|
204 | return $clean_queries; |
---|
205 | } |
---|
206 | |
---|
207 | /** |
---|
208 | * Determine whether a query clause is first-order. |
---|
209 | * |
---|
210 | * A first-order meta query clause is one that has either a 'key' or |
---|
211 | * a 'value' array key. |
---|
212 | * |
---|
213 | * @since 4.1.0 |
---|
214 | * |
---|
215 | * @param array $query Meta query arguments. |
---|
216 | * @return bool Whether the query clause is a first-order clause. |
---|
217 | */ |
---|
218 | protected function is_first_order_clause( $query ) { |
---|
219 | return isset( $query['key'] ) || isset( $query['value'] ); |
---|
220 | } |
---|
221 | |
---|
222 | /** |
---|
223 | * Constructs a meta query based on 'meta_*' query vars |
---|
224 | * |
---|
225 | * @since 3.2.0 |
---|
226 | * |
---|
227 | * @param array $qv The query variables |
---|
228 | */ |
---|
229 | public function parse_query_vars( $qv ) { |
---|
230 | $meta_query = array(); |
---|
231 | |
---|
232 | /* |
---|
233 | * For orderby=meta_value to work correctly, simple query needs to be |
---|
234 | * first (so that its table join is against an unaliased meta table) and |
---|
235 | * needs to be its own clause (so it doesn't interfere with the logic of |
---|
236 | * the rest of the meta_query). |
---|
237 | */ |
---|
238 | $primary_meta_query = array(); |
---|
239 | foreach ( array( 'key', 'compare', 'type' ) as $key ) { |
---|
240 | if ( ! empty( $qv[ "meta_$key" ] ) ) { |
---|
241 | $primary_meta_query[ $key ] = $qv[ "meta_$key" ]; |
---|
242 | } |
---|
243 | } |
---|
244 | |
---|
245 | // WP_Query sets 'meta_value' = '' by default. |
---|
246 | if ( isset( $qv['meta_value'] ) && '' !== $qv['meta_value'] && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) { |
---|
247 | $primary_meta_query['value'] = $qv['meta_value']; |
---|
248 | } |
---|
249 | |
---|
250 | $existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array(); |
---|
251 | |
---|
252 | if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) { |
---|
253 | $meta_query = array( |
---|
254 | 'relation' => 'AND', |
---|
255 | $primary_meta_query, |
---|
256 | $existing_meta_query, |
---|
257 | ); |
---|
258 | } elseif ( ! empty( $primary_meta_query ) ) { |
---|
259 | $meta_query = array( |
---|
260 | $primary_meta_query, |
---|
261 | ); |
---|
262 | } elseif ( ! empty( $existing_meta_query ) ) { |
---|
263 | $meta_query = $existing_meta_query; |
---|
264 | } |
---|
265 | |
---|
266 | $this->__construct( $meta_query ); |
---|
267 | } |
---|
268 | |
---|
269 | /** |
---|
270 | * Return the appropriate alias for the given meta type if applicable. |
---|
271 | * |
---|
272 | * @since 3.7.0 |
---|
273 | * |
---|
274 | * @param string $type MySQL type to cast meta_value. |
---|
275 | * @return string MySQL type. |
---|
276 | */ |
---|
277 | public function get_cast_for_type( $type = '' ) { |
---|
278 | if ( empty( $type ) ) |
---|
279 | return 'CHAR'; |
---|
280 | |
---|
281 | $meta_type = strtoupper( $type ); |
---|
282 | |
---|
283 | if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) ) |
---|
284 | return 'CHAR'; |
---|
285 | |
---|
286 | if ( 'NUMERIC' == $meta_type ) |
---|
287 | $meta_type = 'SIGNED'; |
---|
288 | |
---|
289 | return $meta_type; |
---|
290 | } |
---|
291 | |
---|
292 | /** |
---|
293 | * Generates SQL clauses to be appended to a main query. |
---|
294 | * |
---|
295 | * @since 3.2.0 |
---|
296 | * |
---|
297 | * @param string $type Type of meta, eg 'user', 'post'. |
---|
298 | * @param string $primary_table Database table where the object being filtered is stored (eg wp_users). |
---|
299 | * @param string $primary_id_column ID column for the filtered object in $primary_table. |
---|
300 | * @param object $context Optional. The main query object. |
---|
301 | * @return false|array { |
---|
302 | * Array containing JOIN and WHERE SQL clauses to append to the main query. |
---|
303 | * |
---|
304 | * @type string $join SQL fragment to append to the main JOIN clause. |
---|
305 | * @type string $where SQL fragment to append to the main WHERE clause. |
---|
306 | * } |
---|
307 | */ |
---|
308 | public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) { |
---|
309 | if ( ! $meta_table = _get_meta_table( $type ) ) { |
---|
310 | return false; |
---|
311 | } |
---|
312 | |
---|
313 | $this->table_aliases = array(); |
---|
314 | |
---|
315 | $this->meta_table = $meta_table; |
---|
316 | $this->meta_id_column = sanitize_key( $type . '_id' ); |
---|
317 | |
---|
318 | $this->primary_table = $primary_table; |
---|
319 | $this->primary_id_column = $primary_id_column; |
---|
320 | |
---|
321 | $sql = $this->get_sql_clauses(); |
---|
322 | |
---|
323 | /* |
---|
324 | * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should |
---|
325 | * be LEFT. Otherwise posts with no metadata will be excluded from results. |
---|
326 | */ |
---|
327 | if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) { |
---|
328 | $sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] ); |
---|
329 | } |
---|
330 | |
---|
331 | /** |
---|
332 | * Filters the meta query's generated SQL. |
---|
333 | * |
---|
334 | * @since 3.1.0 |
---|
335 | * |
---|
336 | * @param array $clauses Array containing the query's JOIN and WHERE clauses. |
---|
337 | * @param array $queries Array of meta queries. |
---|
338 | * @param string $type Type of meta. |
---|
339 | * @param string $primary_table Primary table. |
---|
340 | * @param string $primary_id_column Primary column ID. |
---|
341 | * @param object $context The main query object. |
---|
342 | */ |
---|
343 | return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) ); |
---|
344 | } |
---|
345 | |
---|
346 | /** |
---|
347 | * Generate SQL clauses to be appended to a main query. |
---|
348 | * |
---|
349 | * Called by the public WP_Meta_Query::get_sql(), this method is abstracted |
---|
350 | * out to maintain parity with the other Query classes. |
---|
351 | * |
---|
352 | * @since 4.1.0 |
---|
353 | * |
---|
354 | * @return array { |
---|
355 | * Array containing JOIN and WHERE SQL clauses to append to the main query. |
---|
356 | * |
---|
357 | * @type string $join SQL fragment to append to the main JOIN clause. |
---|
358 | * @type string $where SQL fragment to append to the main WHERE clause. |
---|
359 | * } |
---|
360 | */ |
---|
361 | protected function get_sql_clauses() { |
---|
362 | /* |
---|
363 | * $queries are passed by reference to get_sql_for_query() for recursion. |
---|
364 | * To keep $this->queries unaltered, pass a copy. |
---|
365 | */ |
---|
366 | $queries = $this->queries; |
---|
367 | $sql = $this->get_sql_for_query( $queries ); |
---|
368 | |
---|
369 | if ( ! empty( $sql['where'] ) ) { |
---|
370 | $sql['where'] = ' AND ' . $sql['where']; |
---|
371 | } |
---|
372 | |
---|
373 | return $sql; |
---|
374 | } |
---|
375 | |
---|
376 | /** |
---|
377 | * Generate SQL clauses for a single query array. |
---|
378 | * |
---|
379 | * If nested subqueries are found, this method recurses the tree to |
---|
380 | * produce the properly nested SQL. |
---|
381 | * |
---|
382 | * @since 4.1.0 |
---|
383 | * |
---|
384 | * @param array $query Query to parse (passed by reference). |
---|
385 | * @param int $depth Optional. Number of tree levels deep we currently are. |
---|
386 | * Used to calculate indentation. Default 0. |
---|
387 | * @return array { |
---|
388 | * Array containing JOIN and WHERE SQL clauses to append to a single query array. |
---|
389 | * |
---|
390 | * @type string $join SQL fragment to append to the main JOIN clause. |
---|
391 | * @type string $where SQL fragment to append to the main WHERE clause. |
---|
392 | * } |
---|
393 | */ |
---|
394 | protected function get_sql_for_query( &$query, $depth = 0 ) { |
---|
395 | $sql_chunks = array( |
---|
396 | 'join' => array(), |
---|
397 | 'where' => array(), |
---|
398 | ); |
---|
399 | |
---|
400 | $sql = array( |
---|
401 | 'join' => '', |
---|
402 | 'where' => '', |
---|
403 | ); |
---|
404 | |
---|
405 | $indent = ''; |
---|
406 | for ( $i = 0; $i < $depth; $i++ ) { |
---|
407 | $indent .= " "; |
---|
408 | } |
---|
409 | |
---|
410 | foreach ( $query as $key => &$clause ) { |
---|
411 | if ( 'relation' === $key ) { |
---|
412 | $relation = $query['relation']; |
---|
413 | } elseif ( is_array( $clause ) ) { |
---|
414 | |
---|
415 | // This is a first-order clause. |
---|
416 | if ( $this->is_first_order_clause( $clause ) ) { |
---|
417 | $clause_sql = $this->get_sql_for_clause( $clause, $query, $key ); |
---|
418 | |
---|
419 | $where_count = count( $clause_sql['where'] ); |
---|
420 | if ( ! $where_count ) { |
---|
421 | $sql_chunks['where'][] = ''; |
---|
422 | } elseif ( 1 === $where_count ) { |
---|
423 | $sql_chunks['where'][] = $clause_sql['where'][0]; |
---|
424 | } else { |
---|
425 | $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; |
---|
426 | } |
---|
427 | |
---|
428 | $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); |
---|
429 | // This is a subquery, so we recurse. |
---|
430 | } else { |
---|
431 | $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); |
---|
432 | |
---|
433 | $sql_chunks['where'][] = $clause_sql['where']; |
---|
434 | $sql_chunks['join'][] = $clause_sql['join']; |
---|
435 | } |
---|
436 | } |
---|
437 | } |
---|
438 | |
---|
439 | // Filter to remove empties. |
---|
440 | $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); |
---|
441 | $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); |
---|
442 | |
---|
443 | if ( empty( $relation ) ) { |
---|
444 | $relation = 'AND'; |
---|
445 | } |
---|
446 | |
---|
447 | // Filter duplicate JOIN clauses and combine into a single string. |
---|
448 | if ( ! empty( $sql_chunks['join'] ) ) { |
---|
449 | $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); |
---|
450 | } |
---|
451 | |
---|
452 | // Generate a single WHERE clause with proper brackets and indentation. |
---|
453 | if ( ! empty( $sql_chunks['where'] ) ) { |
---|
454 | $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; |
---|
455 | } |
---|
456 | |
---|
457 | return $sql; |
---|
458 | } |
---|
459 | |
---|
460 | /** |
---|
461 | * Generate SQL JOIN and WHERE clauses for a first-order query clause. |
---|
462 | * |
---|
463 | * "First-order" means that it's an array with a 'key' or 'value'. |
---|
464 | * |
---|
465 | * @since 4.1.0 |
---|
466 | * |
---|
467 | * @global wpdb $wpdb WordPress database abstraction object. |
---|
468 | * |
---|
469 | * @param array $clause Query clause (passed by reference). |
---|
470 | * @param array $parent_query Parent query array. |
---|
471 | * @param string $clause_key Optional. The array key used to name the clause in the original `$meta_query` |
---|
472 | * parameters. If not provided, a key will be generated automatically. |
---|
473 | * @return array { |
---|
474 | * Array containing JOIN and WHERE SQL clauses to append to a first-order query. |
---|
475 | * |
---|
476 | * @type string $join SQL fragment to append to the main JOIN clause. |
---|
477 | * @type string $where SQL fragment to append to the main WHERE clause. |
---|
478 | * } |
---|
479 | */ |
---|
480 | public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) { |
---|
481 | global $wpdb; |
---|
482 | |
---|
483 | $sql_chunks = array( |
---|
484 | 'where' => array(), |
---|
485 | 'join' => array(), |
---|
486 | ); |
---|
487 | |
---|
488 | if ( isset( $clause['compare'] ) ) { |
---|
489 | $clause['compare'] = strtoupper( $clause['compare'] ); |
---|
490 | } else { |
---|
491 | $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '='; |
---|
492 | } |
---|
493 | |
---|
494 | if ( ! in_array( $clause['compare'], array( |
---|
495 | '=', '!=', '>', '>=', '<', '<=', |
---|
496 | 'LIKE', 'NOT LIKE', |
---|
497 | 'IN', 'NOT IN', |
---|
498 | 'BETWEEN', 'NOT BETWEEN', |
---|
499 | 'EXISTS', 'NOT EXISTS', |
---|
500 | 'REGEXP', 'NOT REGEXP', 'RLIKE', 'FIND_IN_SET' |
---|
501 | ) ) ) { |
---|
502 | $clause['compare'] = '='; |
---|
503 | } |
---|
504 | |
---|
505 | $meta_compare = $clause['compare']; |
---|
506 | |
---|
507 | // First build the JOIN clause, if one is required. |
---|
508 | $join = ''; |
---|
509 | |
---|
510 | // We prefer to avoid joins if possible. Look for an existing join compatible with this clause. |
---|
511 | $alias = $this->find_compatible_table_alias( $clause, $parent_query ); |
---|
512 | if ( false === $alias ) { |
---|
513 | $i = count( $this->table_aliases ); |
---|
514 | $alias = $i ? 'mt' . $i : $this->meta_table; |
---|
515 | |
---|
516 | // JOIN clauses for NOT EXISTS have their own syntax. |
---|
517 | if ( 'NOT EXISTS' === $meta_compare ) { |
---|
518 | $join .= " LEFT JOIN $this->meta_table"; |
---|
519 | $join .= $i ? " AS $alias" : ''; |
---|
520 | $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] ); |
---|
521 | |
---|
522 | // All other JOIN clauses. |
---|
523 | } else { |
---|
524 | $join .= " INNER JOIN $this->meta_table"; |
---|
525 | $join .= $i ? " AS $alias" : ''; |
---|
526 | $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )"; |
---|
527 | } |
---|
528 | |
---|
529 | $this->table_aliases[] = $alias; |
---|
530 | $sql_chunks['join'][] = $join; |
---|
531 | } |
---|
532 | |
---|
533 | // Save the alias to this clause, for future siblings to find. |
---|
534 | $clause['alias'] = $alias; |
---|
535 | |
---|
536 | // Determine the data type. |
---|
537 | $_meta_type = isset( $clause['type'] ) ? $clause['type'] : ''; |
---|
538 | $meta_type = $this->get_cast_for_type( $_meta_type ); |
---|
539 | $clause['cast'] = $meta_type; |
---|
540 | |
---|
541 | // Fallback for clause keys is the table alias. Key must be a string. |
---|
542 | if ( is_int( $clause_key ) || ! $clause_key ) { |
---|
543 | $clause_key = $clause['alias']; |
---|
544 | } |
---|
545 | |
---|
546 | // Ensure unique clause keys, so none are overwritten. |
---|
547 | $iterator = 1; |
---|
548 | $clause_key_base = $clause_key; |
---|
549 | while ( isset( $this->clauses[ $clause_key ] ) ) { |
---|
550 | $clause_key = $clause_key_base . '-' . $iterator; |
---|
551 | $iterator++; |
---|
552 | } |
---|
553 | |
---|
554 | // Store the clause in our flat array. |
---|
555 | $this->clauses[ $clause_key ] =& $clause; |
---|
556 | |
---|
557 | // Next, build the WHERE clause. |
---|
558 | |
---|
559 | // meta_key. |
---|
560 | if ( array_key_exists( 'key', $clause ) ) { |
---|
561 | if ( 'NOT EXISTS' === $meta_compare ) { |
---|
562 | $sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL'; |
---|
563 | } else { |
---|
564 | $sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) ); |
---|
565 | } |
---|
566 | } |
---|
567 | |
---|
568 | // meta_value. |
---|
569 | if ( array_key_exists( 'value', $clause ) ) { |
---|
570 | $meta_value = $clause['value']; |
---|
571 | |
---|
572 | if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) { |
---|
573 | if ( ! is_array( $meta_value ) ) { |
---|
574 | $meta_value = preg_split( '/[,\s]+/', $meta_value ); |
---|
575 | } |
---|
576 | } else { |
---|
577 | $meta_value = trim( $meta_value ); |
---|
578 | } |
---|
579 | |
---|
580 | switch ( $meta_compare ) { |
---|
581 | case 'IN' : |
---|
582 | case 'NOT IN' : |
---|
583 | $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')'; |
---|
584 | $where = $wpdb->prepare( $meta_compare_string, $meta_value ); |
---|
585 | break; |
---|
586 | |
---|
587 | case 'BETWEEN' : |
---|
588 | case 'NOT BETWEEN' : |
---|
589 | $meta_value = array_slice( $meta_value, 0, 2 ); |
---|
590 | $where = $wpdb->prepare( '%s AND %s', $meta_value ); |
---|
591 | break; |
---|
592 | |
---|
593 | case 'LIKE' : |
---|
594 | case 'NOT LIKE' : |
---|
595 | $meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%'; |
---|
596 | $where = $wpdb->prepare( '%s', $meta_value ); |
---|
597 | break; |
---|
598 | |
---|
599 | // EXISTS with a value is interpreted as '='. |
---|
600 | case 'EXISTS' : |
---|
601 | $meta_compare = '='; |
---|
602 | $where = $wpdb->prepare( '%s', $meta_value ); |
---|
603 | break; |
---|
604 | |
---|
605 | // 'value' is ignored for NOT EXISTS. |
---|
606 | case 'NOT EXISTS' : |
---|
607 | $where = ''; |
---|
608 | break; |
---|
609 | |
---|
610 | case 'FIND_IN_SET': |
---|
611 | $where = $wpdb->prepare( "FIND_IN_SET( %s, $alias.meta_value )", $meta_value ); |
---|
612 | break; |
---|
613 | |
---|
614 | default : |
---|
615 | $where = $wpdb->prepare( '%s', $meta_value ); |
---|
616 | break; |
---|
617 | |
---|
618 | } |
---|
619 | |
---|
620 | if ( $where ) { |
---|
621 | if( 'FIND_IN_SET' == $meta_compare ) |
---|
622 | $sql_chunks['where'][] = $where; |
---|
623 | else { |
---|
624 | if ( 'CHAR' === $meta_type ) { |
---|
625 | $sql_chunks['where'][] = "$alias.meta_value {$meta_compare} {$where}"; |
---|
626 | } else { |
---|
627 | $sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}"; |
---|
628 | } |
---|
629 | } |
---|
630 | } |
---|
631 | } |
---|
632 | |
---|
633 | /* |
---|
634 | * Multiple WHERE clauses (for meta_key and meta_value) should |
---|
635 | * be joined in parentheses. |
---|
636 | */ |
---|
637 | if ( 1 < count( $sql_chunks['where'] ) ) { |
---|
638 | $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' ); |
---|
639 | } |
---|
640 | |
---|
641 | return $sql_chunks; |
---|
642 | } |
---|
643 | |
---|
644 | /** |
---|
645 | * Get a flattened list of sanitized meta clauses. |
---|
646 | * |
---|
647 | * This array should be used for clause lookup, as when the table alias and CAST type must be determined for |
---|
648 | * a value of 'orderby' corresponding to a meta clause. |
---|
649 | * |
---|
650 | * @since 4.2.0 |
---|
651 | * |
---|
652 | * @return array Meta clauses. |
---|
653 | */ |
---|
654 | public function get_clauses() { |
---|
655 | return $this->clauses; |
---|
656 | } |
---|
657 | |
---|
658 | /** |
---|
659 | * Identify an existing table alias that is compatible with the current |
---|
660 | * query clause. |
---|
661 | * |
---|
662 | * We avoid unnecessary table joins by allowing each clause to look for |
---|
663 | * an existing table alias that is compatible with the query that it |
---|
664 | * needs to perform. |
---|
665 | * |
---|
666 | * An existing alias is compatible if (a) it is a sibling of `$clause` |
---|
667 | * (ie, it's under the scope of the same relation), and (b) the combination |
---|
668 | * of operator and relation between the clauses allows for a shared table join. |
---|
669 | * In the case of WP_Meta_Query, this only applies to 'IN' clauses that are |
---|
670 | * connected by the relation 'OR'. |
---|
671 | * |
---|
672 | * @since 4.1.0 |
---|
673 | * |
---|
674 | * @param array $clause Query clause. |
---|
675 | * @param array $parent_query Parent query of $clause. |
---|
676 | * @return string|bool Table alias if found, otherwise false. |
---|
677 | */ |
---|
678 | protected function find_compatible_table_alias( $clause, $parent_query ) { |
---|
679 | $alias = false; |
---|
680 | |
---|
681 | foreach ( $parent_query as $sibling ) { |
---|
682 | // If the sibling has no alias yet, there's nothing to check. |
---|
683 | if ( empty( $sibling['alias'] ) ) { |
---|
684 | continue; |
---|
685 | } |
---|
686 | |
---|
687 | // We're only interested in siblings that are first-order clauses. |
---|
688 | if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) { |
---|
689 | continue; |
---|
690 | } |
---|
691 | |
---|
692 | $compatible_compares = array(); |
---|
693 | |
---|
694 | // Clauses connected by OR can share joins as long as they have "positive" operators. |
---|
695 | if ( 'OR' === $parent_query['relation'] ) { |
---|
696 | $compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' ); |
---|
697 | |
---|
698 | // Clauses joined by AND with "negative" operators share a join only if they also share a key. |
---|
699 | } elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) { |
---|
700 | $compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' ); |
---|
701 | } |
---|
702 | |
---|
703 | $clause_compare = strtoupper( $clause['compare'] ); |
---|
704 | $sibling_compare = strtoupper( $sibling['compare'] ); |
---|
705 | if ( in_array( $clause_compare, $compatible_compares ) && in_array( $sibling_compare, $compatible_compares ) ) { |
---|
706 | $alias = $sibling['alias']; |
---|
707 | break; |
---|
708 | } |
---|
709 | } |
---|
710 | |
---|
711 | /** |
---|
712 | * Filters the table alias identified as compatible with the current clause. |
---|
713 | * |
---|
714 | * @since 4.1.0 |
---|
715 | * |
---|
716 | * @param string|bool $alias Table alias, or false if none was found. |
---|
717 | * @param array $clause First-order query clause. |
---|
718 | * @param array $parent_query Parent of $clause. |
---|
719 | * @param object $this WP_Meta_Query object. |
---|
720 | */ |
---|
721 | return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this ) ; |
---|
722 | } |
---|
723 | |
---|
724 | /** |
---|
725 | * Checks whether the current query has any OR relations. |
---|
726 | * |
---|
727 | * In some cases, the presence of an OR relation somewhere in the query will require |
---|
728 | * the use of a `DISTINCT` or `GROUP BY` keyword in the `SELECT` clause. The current |
---|
729 | * method can be used in these cases to determine whether such a clause is necessary. |
---|
730 | * |
---|
731 | * @since 4.3.0 |
---|
732 | * |
---|
733 | * @return bool True if the query contains any `OR` relations, otherwise false. |
---|
734 | */ |
---|
735 | public function has_or_relation() { |
---|
736 | return $this->has_or_relation; |
---|
737 | } |
---|
738 | } |
---|