class-wp-meta-query.php 27 KB

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