97 /** |
97 /** |
98 * Constructor. |
98 * Constructor. |
99 * |
99 * |
100 * @since 3.2.0 |
100 * @since 3.2.0 |
101 * @since 4.2.0 Introduced support for naming query clauses by associative array keys. |
101 * @since 4.2.0 Introduced support for naming query clauses by associative array keys. |
102 * |
102 * @since 5.1.0 Introduced $compare_key clause parameter, which enables LIKE key matches. |
103 * |
103 * |
104 * @param array $meta_query { |
104 * @param array $meta_query { |
105 * Array of meta query clauses. When first-order clauses or sub-clauses use strings as |
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. |
106 * their array keys, they may be referenced in the 'orderby' parameter of the parent query. |
107 * |
107 * |
108 * @type string $relation Optional. The MySQL keyword used to join |
108 * @type string $relation Optional. The MySQL keyword used to join |
109 * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'. |
109 * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'. |
110 * @type array { |
110 * @type array { |
111 * Optional. An array of first-order clause parameters, or another fully-formed meta query. |
111 * Optional. An array of first-order clause parameters, or another fully-formed meta query. |
112 * |
112 * |
113 * @type string $key Meta key to filter by. |
113 * @type string $key Meta key to filter by. |
114 * @type string $value Meta value to filter by. |
114 * @type string $compare_key MySQL operator used for comparing the $key. Accepts '=' and 'LIKE'. |
115 * @type string $compare MySQL operator used for comparing the $value. Accepts '=', |
115 * Default '='. |
116 * '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', |
116 * @type string $value Meta value to filter by. |
117 * 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP', |
117 * @type string $compare MySQL operator used for comparing the $value. Accepts '=', |
118 * 'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'. |
118 * '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', |
119 * Default is 'IN' when `$value` is an array, '=' otherwise. |
119 * 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP', |
120 * @type string $type MySQL data type that the meta_value column will be CAST to for |
120 * 'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'. |
121 * comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', |
121 * Default is 'IN' when `$value` is an array, '=' otherwise. |
122 * 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'. |
122 * @type string $type MySQL data type that the meta_value column will be CAST to for |
123 * Default is 'CHAR'. |
123 * comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', |
|
124 * 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'. |
|
125 * Default is 'CHAR'. |
124 * } |
126 * } |
125 * } |
127 * } |
126 */ |
128 */ |
127 public function __construct( $meta_query = false ) { |
129 public function __construct( $meta_query = false ) { |
128 if ( !$meta_query ) |
130 if ( ! $meta_query ) { |
129 return; |
131 return; |
|
132 } |
130 |
133 |
131 if ( isset( $meta_query['relation'] ) && strtoupper( $meta_query['relation'] ) == 'OR' ) { |
134 if ( isset( $meta_query['relation'] ) && strtoupper( $meta_query['relation'] ) == 'OR' ) { |
132 $this->relation = 'OR'; |
135 $this->relation = 'OR'; |
133 } else { |
136 } else { |
134 $this->relation = 'AND'; |
137 $this->relation = 'AND'; |
159 $relation = $query; |
162 $relation = $query; |
160 |
163 |
161 } elseif ( ! is_array( $query ) ) { |
164 } elseif ( ! is_array( $query ) ) { |
162 continue; |
165 continue; |
163 |
166 |
164 // First-order clause. |
167 // First-order clause. |
165 } elseif ( $this->is_first_order_clause( $query ) ) { |
168 } elseif ( $this->is_first_order_clause( $query ) ) { |
166 if ( isset( $query['value'] ) && array() === $query['value'] ) { |
169 if ( isset( $query['value'] ) && array() === $query['value'] ) { |
167 unset( $query['value'] ); |
170 unset( $query['value'] ); |
168 } |
171 } |
169 |
172 |
170 $clean_queries[ $key ] = $query; |
173 $clean_queries[ $key ] = $query; |
171 |
174 |
172 // Otherwise, it's a nested query, so we recurse. |
175 // Otherwise, it's a nested query, so we recurse. |
173 } else { |
176 } else { |
174 $cleaned_query = $this->sanitize_query( $query ); |
177 $cleaned_query = $this->sanitize_query( $query ); |
175 |
178 |
176 if ( ! empty( $cleaned_query ) ) { |
179 if ( ! empty( $cleaned_query ) ) { |
177 $clean_queries[ $key ] = $cleaned_query; |
180 $clean_queries[ $key ] = $cleaned_query; |
184 } |
187 } |
185 |
188 |
186 // Sanitize the 'relation' key provided in the query. |
189 // Sanitize the 'relation' key provided in the query. |
187 if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) { |
190 if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) { |
188 $clean_queries['relation'] = 'OR'; |
191 $clean_queries['relation'] = 'OR'; |
189 $this->has_or_relation = true; |
192 $this->has_or_relation = true; |
190 |
193 |
191 /* |
194 /* |
192 * If there is only a single clause, call the relation 'OR'. |
195 * If there is only a single clause, call the relation 'OR'. |
193 * This value will not actually be used to join clauses, but it |
196 * This value will not actually be used to join clauses, but it |
194 * simplifies the logic around combining key-only queries. |
197 * simplifies the logic around combining key-only queries. |
195 */ |
198 */ |
196 } elseif ( 1 === count( $clean_queries ) ) { |
199 } elseif ( 1 === count( $clean_queries ) ) { |
197 $clean_queries['relation'] = 'OR'; |
200 $clean_queries['relation'] = 'OR'; |
198 |
201 |
199 // Default to AND. |
202 // Default to AND. |
200 } else { |
203 } else { |
201 $clean_queries['relation'] = 'AND'; |
204 $clean_queries['relation'] = 'AND'; |
202 } |
205 } |
203 |
206 |
204 return $clean_queries; |
207 return $clean_queries; |
480 public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) { |
486 public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) { |
481 global $wpdb; |
487 global $wpdb; |
482 |
488 |
483 $sql_chunks = array( |
489 $sql_chunks = array( |
484 'where' => array(), |
490 'where' => array(), |
485 'join' => array(), |
491 'join' => array(), |
486 ); |
492 ); |
487 |
493 |
488 if ( isset( $clause['compare'] ) ) { |
494 if ( isset( $clause['compare'] ) ) { |
489 $clause['compare'] = strtoupper( $clause['compare'] ); |
495 $clause['compare'] = strtoupper( $clause['compare'] ); |
490 } else { |
496 } else { |
491 $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '='; |
497 $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '='; |
492 } |
498 } |
493 |
499 |
494 if ( ! in_array( $clause['compare'], array( |
500 if ( ! in_array( |
495 '=', '!=', '>', '>=', '<', '<=', |
501 $clause['compare'], |
496 'LIKE', 'NOT LIKE', |
502 array( |
497 'IN', 'NOT IN', |
503 '=', |
498 'BETWEEN', 'NOT BETWEEN', |
504 '!=', |
499 'EXISTS', 'NOT EXISTS', |
505 '>', |
500 'REGEXP', 'NOT REGEXP', 'RLIKE' |
506 '>=', |
501 ) ) ) { |
507 '<', |
|
508 '<=', |
|
509 'LIKE', |
|
510 'NOT LIKE', |
|
511 'IN', |
|
512 'NOT IN', |
|
513 'BETWEEN', |
|
514 'NOT BETWEEN', |
|
515 'EXISTS', |
|
516 'NOT EXISTS', |
|
517 'REGEXP', |
|
518 'NOT REGEXP', |
|
519 'RLIKE', |
|
520 ) |
|
521 ) ) { |
502 $clause['compare'] = '='; |
522 $clause['compare'] = '='; |
503 } |
523 } |
504 |
524 |
505 $meta_compare = $clause['compare']; |
525 if ( isset( $clause['compare_key'] ) && 'LIKE' === strtoupper( $clause['compare_key'] ) ) { |
|
526 $clause['compare_key'] = strtoupper( $clause['compare_key'] ); |
|
527 } else { |
|
528 $clause['compare_key'] = '='; |
|
529 } |
|
530 |
|
531 $meta_compare = $clause['compare']; |
|
532 $meta_compare_key = $clause['compare_key']; |
506 |
533 |
507 // First build the JOIN clause, if one is required. |
534 // First build the JOIN clause, if one is required. |
508 $join = ''; |
535 $join = ''; |
509 |
536 |
510 // We prefer to avoid joins if possible. Look for an existing join compatible with this clause. |
537 // 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 ); |
538 $alias = $this->find_compatible_table_alias( $clause, $parent_query ); |
512 if ( false === $alias ) { |
539 if ( false === $alias ) { |
513 $i = count( $this->table_aliases ); |
540 $i = count( $this->table_aliases ); |
514 $alias = $i ? 'mt' . $i : $this->meta_table; |
541 $alias = $i ? 'mt' . $i : $this->meta_table; |
515 |
542 |
516 // JOIN clauses for NOT EXISTS have their own syntax. |
543 // JOIN clauses for NOT EXISTS have their own syntax. |
517 if ( 'NOT EXISTS' === $meta_compare ) { |
544 if ( 'NOT EXISTS' === $meta_compare ) { |
518 $join .= " LEFT JOIN $this->meta_table"; |
545 $join .= " LEFT JOIN $this->meta_table"; |
519 $join .= $i ? " AS $alias" : ''; |
546 $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'] ); |
547 |
521 |
548 if ( 'LIKE' === $meta_compare_key ) { |
522 // All other JOIN clauses. |
549 $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key LIKE %s )", '%' . $wpdb->esc_like( $clause['key'] ) . '%' ); |
|
550 } else { |
|
551 $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] ); |
|
552 } |
|
553 |
|
554 // All other JOIN clauses. |
523 } else { |
555 } else { |
524 $join .= " INNER JOIN $this->meta_table"; |
556 $join .= " INNER JOIN $this->meta_table"; |
525 $join .= $i ? " AS $alias" : ''; |
557 $join .= $i ? " AS $alias" : ''; |
526 $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )"; |
558 $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )"; |
527 } |
559 } |
528 |
560 |
529 $this->table_aliases[] = $alias; |
561 $this->table_aliases[] = $alias; |
530 $sql_chunks['join'][] = $join; |
562 $sql_chunks['join'][] = $join; |
531 } |
563 } |
532 |
564 |
533 // Save the alias to this clause, for future siblings to find. |
565 // Save the alias to this clause, for future siblings to find. |
534 $clause['alias'] = $alias; |
566 $clause['alias'] = $alias; |
535 |
567 |
536 // Determine the data type. |
568 // Determine the data type. |
537 $_meta_type = isset( $clause['type'] ) ? $clause['type'] : ''; |
569 $_meta_type = isset( $clause['type'] ) ? $clause['type'] : ''; |
538 $meta_type = $this->get_cast_for_type( $_meta_type ); |
570 $meta_type = $this->get_cast_for_type( $_meta_type ); |
539 $clause['cast'] = $meta_type; |
571 $clause['cast'] = $meta_type; |
540 |
572 |
541 // Fallback for clause keys is the table alias. Key must be a string. |
573 // Fallback for clause keys is the table alias. Key must be a string. |
542 if ( is_int( $clause_key ) || ! $clause_key ) { |
574 if ( is_int( $clause_key ) || ! $clause_key ) { |
543 $clause_key = $clause['alias']; |
575 $clause_key = $clause['alias']; |
544 } |
576 } |
545 |
577 |
546 // Ensure unique clause keys, so none are overwritten. |
578 // Ensure unique clause keys, so none are overwritten. |
547 $iterator = 1; |
579 $iterator = 1; |
548 $clause_key_base = $clause_key; |
580 $clause_key_base = $clause_key; |
549 while ( isset( $this->clauses[ $clause_key ] ) ) { |
581 while ( isset( $this->clauses[ $clause_key ] ) ) { |
550 $clause_key = $clause_key_base . '-' . $iterator; |
582 $clause_key = $clause_key_base . '-' . $iterator; |
551 $iterator++; |
583 $iterator++; |
552 } |
584 } |
576 } else { |
612 } else { |
577 $meta_value = trim( $meta_value ); |
613 $meta_value = trim( $meta_value ); |
578 } |
614 } |
579 |
615 |
580 switch ( $meta_compare ) { |
616 switch ( $meta_compare ) { |
581 case 'IN' : |
617 case 'IN': |
582 case 'NOT IN' : |
618 case 'NOT IN': |
583 $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')'; |
619 $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')'; |
584 $where = $wpdb->prepare( $meta_compare_string, $meta_value ); |
620 $where = $wpdb->prepare( $meta_compare_string, $meta_value ); |
585 break; |
621 break; |
586 |
622 |
587 case 'BETWEEN' : |
623 case 'BETWEEN': |
588 case 'NOT BETWEEN' : |
624 case 'NOT BETWEEN': |
589 $meta_value = array_slice( $meta_value, 0, 2 ); |
625 $meta_value = array_slice( $meta_value, 0, 2 ); |
590 $where = $wpdb->prepare( '%s AND %s', $meta_value ); |
626 $where = $wpdb->prepare( '%s AND %s', $meta_value ); |
591 break; |
627 break; |
592 |
628 |
593 case 'LIKE' : |
629 case 'LIKE': |
594 case 'NOT LIKE' : |
630 case 'NOT LIKE': |
595 $meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%'; |
631 $meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%'; |
596 $where = $wpdb->prepare( '%s', $meta_value ); |
632 $where = $wpdb->prepare( '%s', $meta_value ); |
597 break; |
633 break; |
598 |
634 |
599 // EXISTS with a value is interpreted as '='. |
635 // EXISTS with a value is interpreted as '='. |
600 case 'EXISTS' : |
636 case 'EXISTS': |
601 $meta_compare = '='; |
637 $meta_compare = '='; |
602 $where = $wpdb->prepare( '%s', $meta_value ); |
638 $where = $wpdb->prepare( '%s', $meta_value ); |
603 break; |
639 break; |
604 |
640 |
605 // 'value' is ignored for NOT EXISTS. |
641 // 'value' is ignored for NOT EXISTS. |
606 case 'NOT EXISTS' : |
642 case 'NOT EXISTS': |
607 $where = ''; |
643 $where = ''; |
608 break; |
644 break; |
609 |
645 |
610 default : |
646 default: |
611 $where = $wpdb->prepare( '%s', $meta_value ); |
647 $where = $wpdb->prepare( '%s', $meta_value ); |
612 break; |
648 break; |
613 |
649 |
614 } |
650 } |
615 |
651 |
685 |
721 |
686 // Clauses connected by OR can share joins as long as they have "positive" operators. |
722 // Clauses connected by OR can share joins as long as they have "positive" operators. |
687 if ( 'OR' === $parent_query['relation'] ) { |
723 if ( 'OR' === $parent_query['relation'] ) { |
688 $compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' ); |
724 $compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' ); |
689 |
725 |
690 // Clauses joined by AND with "negative" operators share a join only if they also share a key. |
726 // Clauses joined by AND with "negative" operators share a join only if they also share a key. |
691 } elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) { |
727 } elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) { |
692 $compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' ); |
728 $compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' ); |
693 } |
729 } |
694 |
730 |
695 $clause_compare = strtoupper( $clause['compare'] ); |
731 $clause_compare = strtoupper( $clause['compare'] ); |