You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

753 lines
22 KiB

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