wp/wp-includes/class-wp-meta-query.php
changeset 7 cf61fcea0001
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
       
     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'
       
   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 				default :
       
   611 					$where = $wpdb->prepare( '%s', $meta_value );
       
   612 					break;
       
   613 
       
   614 			}
       
   615 
       
   616 			if ( $where ) {
       
   617 				if ( 'CHAR' === $meta_type ) {
       
   618 					$sql_chunks['where'][] = "$alias.meta_value {$meta_compare} {$where}";
       
   619 				} else {
       
   620 					$sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}";
       
   621 				}
       
   622 			}
       
   623 		}
       
   624 
       
   625 		/*
       
   626 		 * Multiple WHERE clauses (for meta_key and meta_value) should
       
   627 		 * be joined in parentheses.
       
   628 		 */
       
   629 		if ( 1 < count( $sql_chunks['where'] ) ) {
       
   630 			$sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
       
   631 		}
       
   632 
       
   633 		return $sql_chunks;
       
   634 	}
       
   635 
       
   636 	/**
       
   637 	 * Get a flattened list of sanitized meta clauses.
       
   638 	 *
       
   639 	 * This array should be used for clause lookup, as when the table alias and CAST type must be determined for
       
   640 	 * a value of 'orderby' corresponding to a meta clause.
       
   641 	 *
       
   642 	 * @since 4.2.0
       
   643 	 *
       
   644 	 * @return array Meta clauses.
       
   645 	 */
       
   646 	public function get_clauses() {
       
   647 		return $this->clauses;
       
   648 	}
       
   649 
       
   650 	/**
       
   651 	 * Identify an existing table alias that is compatible with the current
       
   652 	 * query clause.
       
   653 	 *
       
   654 	 * We avoid unnecessary table joins by allowing each clause to look for
       
   655 	 * an existing table alias that is compatible with the query that it
       
   656 	 * needs to perform.
       
   657 	 *
       
   658 	 * An existing alias is compatible if (a) it is a sibling of `$clause`
       
   659 	 * (ie, it's under the scope of the same relation), and (b) the combination
       
   660 	 * of operator and relation between the clauses allows for a shared table join.
       
   661 	 * In the case of WP_Meta_Query, this only applies to 'IN' clauses that are
       
   662 	 * connected by the relation 'OR'.
       
   663 	 *
       
   664 	 * @since 4.1.0
       
   665 	 *
       
   666 	 * @param  array       $clause       Query clause.
       
   667 	 * @param  array       $parent_query Parent query of $clause.
       
   668 	 * @return string|bool Table alias if found, otherwise false.
       
   669 	 */
       
   670 	protected function find_compatible_table_alias( $clause, $parent_query ) {
       
   671 		$alias = false;
       
   672 
       
   673 		foreach ( $parent_query as $sibling ) {
       
   674 			// If the sibling has no alias yet, there's nothing to check.
       
   675 			if ( empty( $sibling['alias'] ) ) {
       
   676 				continue;
       
   677 			}
       
   678 
       
   679 			// We're only interested in siblings that are first-order clauses.
       
   680 			if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
       
   681 				continue;
       
   682 			}
       
   683 
       
   684 			$compatible_compares = array();
       
   685 
       
   686 			// Clauses connected by OR can share joins as long as they have "positive" operators.
       
   687 			if ( 'OR' === $parent_query['relation'] ) {
       
   688 				$compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
       
   689 
       
   690 			// 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'] ) {
       
   692 				$compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' );
       
   693 			}
       
   694 
       
   695 			$clause_compare  = strtoupper( $clause['compare'] );
       
   696 			$sibling_compare = strtoupper( $sibling['compare'] );
       
   697 			if ( in_array( $clause_compare, $compatible_compares ) && in_array( $sibling_compare, $compatible_compares ) ) {
       
   698 				$alias = $sibling['alias'];
       
   699 				break;
       
   700 			}
       
   701 		}
       
   702 
       
   703 		/**
       
   704 		 * Filters the table alias identified as compatible with the current clause.
       
   705 		 *
       
   706 		 * @since 4.1.0
       
   707 		 *
       
   708 		 * @param string|bool $alias        Table alias, or false if none was found.
       
   709 		 * @param array       $clause       First-order query clause.
       
   710 		 * @param array       $parent_query Parent of $clause.
       
   711 		 * @param object      $this         WP_Meta_Query object.
       
   712 		 */
       
   713 		return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this ) ;
       
   714 	}
       
   715 
       
   716 	/**
       
   717 	 * Checks whether the current query has any OR relations.
       
   718 	 *
       
   719 	 * In some cases, the presence of an OR relation somewhere in the query will require
       
   720 	 * the use of a `DISTINCT` or `GROUP BY` keyword in the `SELECT` clause. The current
       
   721 	 * method can be used in these cases to determine whether such a clause is necessary.
       
   722 	 *
       
   723 	 * @since 4.3.0
       
   724 	 *
       
   725 	 * @return bool True if the query contains any `OR` relations, otherwise false.
       
   726 	 */
       
   727 	public function has_or_relation() {
       
   728 		return $this->has_or_relation;
       
   729 	}
       
   730 }