17 * to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached |
17 * to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached |
18 * to the primary SQL query string. |
18 * to the primary SQL query string. |
19 * |
19 * |
20 * @since 3.2.0 |
20 * @since 3.2.0 |
21 */ |
21 */ |
|
22 #[AllowDynamicProperties] |
22 class WP_Meta_Query { |
23 class WP_Meta_Query { |
23 /** |
24 /** |
24 * Array of metadata queries. |
25 * Array of metadata queries. |
25 * |
26 * |
26 * See WP_Meta_Query::__construct() for information on meta query arguments. |
27 * See WP_Meta_Query::__construct() for information on meta query arguments. |
120 * - 'NOT LIKE' |
121 * - 'NOT LIKE' |
121 * - 'IN' |
122 * - 'IN' |
122 * - 'NOT IN' |
123 * - 'NOT IN' |
123 * - 'REGEXP' |
124 * - 'REGEXP' |
124 * - 'NOT REGEXP' |
125 * - 'NOT REGEXP' |
125 * - 'RLIKE', |
126 * - 'RLIKE' |
126 * - 'EXISTS' (alias of '=') |
127 * - 'EXISTS' (alias of '=') |
127 * - 'NOT EXISTS' (alias of '!=') |
128 * - 'NOT EXISTS' (alias of '!=') |
128 * Default is 'IN' when `$key` is an array, '=' otherwise. |
129 * Default is 'IN' when `$key` is an array, '=' otherwise. |
129 * @type string $type_key MySQL data type that the meta_key column will be CAST to for |
130 * @type string $type_key MySQL data type that the meta_key column will be CAST to for |
130 * comparisons. Accepts 'BINARY' for case-sensitive regular expression |
131 * comparisons. Accepts 'BINARY' for case-sensitive regular expression |
131 * comparisons. Default is ''. |
132 * comparisons. Default is ''. |
132 * @type string|string[] $value Meta value or values to filter by. |
133 * @type string|string[] $value Meta value or values to filter by. |
133 * @type string $compare MySQL operator used for comparing the $value. Accepts: |
134 * @type string $compare MySQL operator used for comparing the $value. Accepts: |
134 * - '=', |
135 * - '=' |
135 * - '!=' |
136 * - '!=' |
136 * - '>' |
137 * - '>' |
137 * - '>=' |
138 * - '>=' |
138 * - '<' |
139 * - '<' |
139 * - '<=' |
140 * - '<=' |
342 * to 'post', 'comment', 'blog', 'term', and 'user'. |
343 * to 'post', 'comment', 'blog', 'term', and 'user'. |
343 * @param string $primary_table Database table where the object being filtered is stored (eg wp_users). |
344 * @param string $primary_table Database table where the object being filtered is stored (eg wp_users). |
344 * @param string $primary_id_column ID column for the filtered object in $primary_table. |
345 * @param string $primary_id_column ID column for the filtered object in $primary_table. |
345 * @param object $context Optional. The main query object that corresponds to the type, for |
346 * @param object $context Optional. The main query object that corresponds to the type, for |
346 * example a `WP_Query`, `WP_User_Query`, or `WP_Site_Query`. |
347 * example a `WP_Query`, `WP_User_Query`, or `WP_Site_Query`. |
|
348 * Default null. |
347 * @return string[]|false { |
349 * @return string[]|false { |
348 * Array containing JOIN and WHERE SQL clauses to append to the main query, |
350 * Array containing JOIN and WHERE SQL clauses to append to the main query, |
349 * or false if no table exists for the requested meta type. |
351 * or false if no table exists for the requested meta type. |
350 * |
352 * |
351 * @type string $join SQL fragment to append to the main JOIN clause. |
353 * @type string $join SQL fragment to append to the main JOIN clause. |
370 |
372 |
371 /* |
373 /* |
372 * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should |
374 * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should |
373 * be LEFT. Otherwise posts with no metadata will be excluded from results. |
375 * be LEFT. Otherwise posts with no metadata will be excluded from results. |
374 */ |
376 */ |
375 if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) { |
377 if ( str_contains( $sql['join'], 'LEFT JOIN' ) ) { |
376 $sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] ); |
378 $sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] ); |
377 } |
379 } |
378 |
380 |
379 /** |
381 /** |
380 * Filters the meta query's generated SQL. |
382 * Filters the meta query's generated SQL. |
392 */ |
394 */ |
393 return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) ); |
395 return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) ); |
394 } |
396 } |
395 |
397 |
396 /** |
398 /** |
397 * Generate SQL clauses to be appended to a main query. |
399 * Generates SQL clauses to be appended to a main query. |
398 * |
400 * |
399 * Called by the public WP_Meta_Query::get_sql(), this method is abstracted |
401 * Called by the public WP_Meta_Query::get_sql(), this method is abstracted |
400 * out to maintain parity with the other Query classes. |
402 * out to maintain parity with the other Query classes. |
401 * |
403 * |
402 * @since 4.1.0 |
404 * @since 4.1.0 |
518 * |
520 * |
519 * @param array $clause Query clause (passed by reference). |
521 * @param array $clause Query clause (passed by reference). |
520 * @param array $parent_query Parent query array. |
522 * @param array $parent_query Parent query array. |
521 * @param string $clause_key Optional. The array key used to name the clause in the original `$meta_query` |
523 * @param string $clause_key Optional. The array key used to name the clause in the original `$meta_query` |
522 * parameters. If not provided, a key will be generated automatically. |
524 * parameters. If not provided, a key will be generated automatically. |
523 * @return string[] { |
525 * Default empty string. |
|
526 * @return array { |
524 * Array containing JOIN and WHERE SQL clauses to append to a first-order query. |
527 * Array containing JOIN and WHERE SQL clauses to append to a first-order query. |
525 * |
528 * |
526 * @type string $join SQL fragment to append to the main JOIN clause. |
529 * @type string[] $join Array of SQL fragments to append to the main JOIN clause. |
527 * @type string $where SQL fragment to append to the main WHERE clause. |
530 * @type string[] $where Array of SQL fragments to append to the main WHERE clause. |
528 * } |
531 * } |
529 */ |
532 */ |
530 public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) { |
533 public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) { |
531 global $wpdb; |
534 global $wpdb; |
532 |
535 |
628 // Ensure unique clause keys, so none are overwritten. |
631 // Ensure unique clause keys, so none are overwritten. |
629 $iterator = 1; |
632 $iterator = 1; |
630 $clause_key_base = $clause_key; |
633 $clause_key_base = $clause_key; |
631 while ( isset( $this->clauses[ $clause_key ] ) ) { |
634 while ( isset( $this->clauses[ $clause_key ] ) ) { |
632 $clause_key = $clause_key_base . '-' . $iterator; |
635 $clause_key = $clause_key_base . '-' . $iterator; |
633 $iterator++; |
636 ++$iterator; |
634 } |
637 } |
635 |
638 |
636 // Store the clause in our flat array. |
639 // Store the clause in our flat array. |
637 $this->clauses[ $clause_key ] =& $clause; |
640 $this->clauses[ $clause_key ] =& $clause; |
638 |
641 |
677 break; |
680 break; |
678 case 'RLIKE': |
681 case 'RLIKE': |
679 case 'REGEXP': |
682 case 'REGEXP': |
680 $operator = $meta_compare_key; |
683 $operator = $meta_compare_key; |
681 if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) { |
684 if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) { |
682 $cast = 'BINARY'; |
685 $cast = 'BINARY'; |
|
686 $meta_key = "CAST($alias.meta_key AS BINARY)"; |
683 } else { |
687 } else { |
684 $cast = ''; |
688 $cast = ''; |
|
689 $meta_key = "$alias.meta_key"; |
685 } |
690 } |
686 $where = $wpdb->prepare( "$alias.meta_key $operator $cast %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
691 $where = $wpdb->prepare( "$meta_key $operator $cast %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
687 break; |
692 break; |
688 |
693 |
689 case '!=': |
694 case '!=': |
690 case 'NOT EXISTS': |
695 case 'NOT EXISTS': |
691 $meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key = %s " . $meta_compare_string_end; |
696 $meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key = %s " . $meta_compare_string_end; |
703 $where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
708 $where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
704 break; |
709 break; |
705 case 'NOT REGEXP': |
710 case 'NOT REGEXP': |
706 $operator = $meta_compare_key; |
711 $operator = $meta_compare_key; |
707 if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) { |
712 if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) { |
708 $cast = 'BINARY'; |
713 $cast = 'BINARY'; |
|
714 $meta_key = "CAST($subquery_alias.meta_key AS BINARY)"; |
709 } else { |
715 } else { |
710 $cast = ''; |
716 $cast = ''; |
|
717 $meta_key = "$subquery_alias.meta_key"; |
711 } |
718 } |
712 |
719 |
713 $meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key REGEXP $cast %s " . $meta_compare_string_end; |
720 $meta_compare_string = $meta_compare_string_start . "AND $meta_key REGEXP $cast %s " . $meta_compare_string_end; |
714 $where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
721 $where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
715 break; |
722 break; |
716 } |
723 } |
717 |
724 |
718 $sql_chunks['where'][] = $where; |
725 $sql_chunks['where'][] = $where; |
799 public function get_clauses() { |
806 public function get_clauses() { |
800 return $this->clauses; |
807 return $this->clauses; |
801 } |
808 } |
802 |
809 |
803 /** |
810 /** |
804 * Identify an existing table alias that is compatible with the current |
811 * Identifies an existing table alias that is compatible with the current |
805 * query clause. |
812 * query clause. |
806 * |
813 * |
807 * We avoid unnecessary table joins by allowing each clause to look for |
814 * We avoid unnecessary table joins by allowing each clause to look for |
808 * an existing table alias that is compatible with the query that it |
815 * an existing table alias that is compatible with the query that it |
809 * needs to perform. |
816 * needs to perform. |