WordPress.org

Make WordPress Core

Ticket #29642: 29642.4.patch

File 29642.4.patch, 30.4 KB (added by boonebgorges, 5 years ago)
  • src/wp-includes/meta.php

    diff --git src/wp-includes/meta.php src/wp-includes/meta.php
    index e3c43cf..9244e70 100644
    function get_meta_sql( $meta_query, $type, $primary_table, $primary_id_column, $ 
    849849}
    850850
    851851/**
    852  * Container class for a multiple metadata query
     852 * Class for generating SQL clauses that filter a primary query according to metadata keys and values.
     853 *
     854 * WP_Meta_Query is a helper that allows primary query classes, such as WP_Query
     855 * and WP_User_Query, to filter their results by object metadata, by generating
     856 * JOIN and WHERE subclauses to be attached to the primary SQL query string.
     857 *
     858 * The fundamental unit of a meta query is the "first-order clause", an array
     859 * of arguments of the following form:
     860 *     array(
     861 *         'key' => 'your_meta_key',
     862 *         'value' => 'Some value',
     863 *         'compare' => '=',
     864 *         'type' => 'CHAR',
     865 *      )
     866 * (See the documentation for {@link WP_Meta_Query::__construct()} for more
     867 * details on accepted paramaters.) WP_Meta_Query accepts an array of these
     868 * first-order clauses, which can be joined by a 'relation' of 'AND' or 'OR.
     869 *
     870 * Furthermore, these arrays can be nested and combined using multiple values
     871 * of 'relation', to generate queries of arbitrary complexity. For example:
     872 *     array(
     873 *         'relation' => 'OR',
     874 *         array(
     875 *             'key' => 'city',
     876 *             'value' => 'New York',
     877 *         ),
     878 *         array(
     879 *             'relation' => 'AND',
     880 *             array(
     881 *                 'key' => 'state',
     882 *                 'value' => 'Massachusetts',
     883 *             ),
     884 *             array(
     885 *                 'key' => 'city'
     886 *                 'value' => 'Boston',
     887 *                 'compare' => '!=',
     888 *             ),
     889 *         ),
     890 *     )
     891 * will return items that are either labeled New York, or labeled Massachusetts
     892 * but not Boston.
    853893 *
    854894 * @since 3.2.0
    855895 */
    856896class WP_Meta_Query {
    857897        /**
    858         * List of metadata queries. A single query is an associative array:
    859         * - 'key' string The meta key
    860         * - 'value' string|array The meta value
    861         * - 'compare' (optional) string How to compare the key to the value.
    862         *              Possible values: '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN',
    863         *              'BETWEEN', 'NOT BETWEEN', 'REGEXP', 'NOT REGEXP', 'RLIKE'.
    864         *              Default: '='
    865         * - 'type' string (optional) The type of the value.
    866         *              Possible values: 'NUMERIC', 'BINARY', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', 'UNSIGNED'.
    867         *              Default: 'CHAR'
     898        * Array of metadata queries.
    868899        *
    869900        * @since 3.2.0
    870901        * @access public
    871902        * @var array
     903        * @see WP_Meta_Query::__construct() for description of params.
    872904        */
    873905        public $queries = array();
    874906
    class WP_Meta_Query { 
    882914        public $relation;
    883915
    884916        /**
    885          * Constructor
     917         * Database table to query for the metadata.
     918         *
     919         * @access public
     920         * @var string
     921         */
     922        public $meta_table;
     923
     924        /**
     925         * Column in meta_table that represents the ID of the object the metadata belongs to.
    886926         *
    887          * @param array $meta_query (optional) A meta query
     927         * @access public
     928         * @var string
     929         */
     930        public $meta_id_column;
     931
     932        /**
     933         * Database table that where the metadata's objects are stored (eg $wpdb->users).
     934         *
     935         * @access public
     936         * @var string
     937         */
     938        public $primary_table;
     939
     940        /**
     941         * Column in primary_table that represents the ID of the object.
     942         *
     943         * @access public
     944         * @var string
     945         */
     946        public $primary_id_column;
     947
     948        /**
     949         * A flat list of table aliases used in JOIN clauses.
     950         *
     951         * @since 4.1.0
     952         * @access protected
     953         * @var array
     954         */
     955        protected $table_aliases = array();
     956
     957        /**
     958         * Constructor.
     959         *
     960         * @param array $meta_query {
     961         *     Meta query clauses.
     962         *     @type string $relation Optional. The MySQL keyword used to join
     963         *                            the clauses of the query. (AND|OR)
     964         *     @type array {
     965         *         A first-order clause, or another fully-formed meta query.
     966         *         Parameters for first-order clauses are described here. All are optional.
     967         *         @type string $key     Meta key to filter by.
     968         *         @type string $value   Meta value to filter by.
     969         *         @type string $compare MySQL operator used for comparing the $value.
     970         *                               Default: 'IN when $value is an array, otherwise '='.
     971         *                               (=|!=|>|>=|<|<=|LIKE|NOT LIKE|IN|NOT IN|BETWEEN|NOT BETWEEN|REGEXP|NOT REGEXP|RLIKE)
     972         *         @type string $type    MySQL data type that the meta_value column will be CAST to
     973         *                               for comparisons. Default: 'CHAR'.
     974         *                               (NUMERIC|BINARY|CHAR|DATE|DATETIME|DECIMAL|SIGNED|TIME|UNSIGNED)
     975         *     }
     976         * }
    888977         */
    889978        public function __construct( $meta_query = false ) {
    890979                if ( !$meta_query )
    class WP_Meta_Query { 
    896985                        $this->relation = 'AND';
    897986                }
    898987
    899                 $this->queries = array();
     988                $this->queries = $this->sanitize_query( $meta_query );
     989        }
     990
     991        /**
     992         * Ensure that the meta_query argument passed to the class constructor is well-formed.
     993         *
     994         * Eliminates empty items and ensures that a 'relation' is set.
     995         *
     996         * @param  array $queries Array of query clauses.
     997         * @return array Sanitized array of query clauses.
     998         */
     999        public function sanitize_query( $queries ) {
     1000                $clean_queries = array();
     1001
     1002                if ( ! is_array( $queries ) ) {
     1003                        return $clean_queries;
     1004                }
     1005
     1006                foreach ( $queries as $key => $query ) {
     1007                        if ( 'relation' === $key ) {
     1008                                $relation = $query;
     1009
     1010                        // First-order clause.
     1011                        } else if ( $this->is_first_order_clause( $query ) ) {
     1012                                $clean_queries[] = $query;
     1013
     1014                        // Otherwise, it's a nested query.
     1015                        } else {
     1016                                $cleaned_query = $this->sanitize_query( $query );
     1017
     1018                                if ( ! empty( $cleaned_query ) ) {
     1019                                        $clean_queries[] = $cleaned_query;
     1020                                }
     1021                        }
     1022                }
     1023
     1024                if ( empty( $clean_queries ) ) {
     1025                        return $clean_queries;
     1026                }
    9001027
    901                 foreach ( $meta_query as $key => $query ) {
    902                         if ( ! is_array( $query ) )
    903                                 continue;
     1028                // Sanitize the 'relation' key provided in the query.
     1029                if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
     1030                        $clean_queries['relation'] = 'OR';
    9041031
    905                         $this->queries[] = $query;
     1032                /*
     1033                 * If there is only a single clause, call the relation 'OR'.
     1034                 * This value will not actually be used to join clauses, but it
     1035                 * simplifies the logic around combining key-only queries.
     1036                 */
     1037                } else if ( 1 === count( $clean_queries ) ) {
     1038                        $clean_queries['relation'] = 'OR';
     1039
     1040                // Default to AND.
     1041                } else {
     1042                        $clean_queries['relation'] = 'AND';
    9061043                }
     1044
     1045                return $clean_queries;
     1046        }
     1047
     1048        /**
     1049         * Determine whether a query clause is first-order.
     1050         *
     1051         * A first-order meta query clause is one that has either a 'key' or
     1052         * a 'value' array key.
     1053         *
     1054         * @since 4.1.0
     1055         * @access protected
     1056         *
     1057         * @param  array $query Meta query arguments.
     1058         * @return bool
     1059         */
     1060        protected function is_first_order_clause( $query ) {
     1061                return isset( $query['key'] ) || isset( $query['value'] );
    9071062        }
    9081063
    9091064        /**
    class WP_Meta_Query { 
    9631118         * @since 3.2.0
    9641119         * @access public
    9651120         *
    966          * @param string $type Type of meta
    967          * @param string $primary_table
    968          * @param string $primary_id_column
    969          * @param object $context (optional) The main query object
    970          * @return array( 'join' => $join_sql, 'where' => $where_sql )
     1121         * @param  string $type              Type of meta, eg 'user', 'post'.
     1122         * @param  string $primary_table     Database table where the object being
     1123         *                                   filtered is stored (eg wp_users).
     1124         * @param  string $primary_id_column ID column for the filtered object in $primary_table.
     1125         * @param  object $context           Optional. The main query object.
     1126         * @return array {
     1127         *      @type string $join SQL fragment to append to the main JOIN clause.
     1128         *      @type string $where SQL fragment to append to the main WHERE clause.
     1129         * }
    9711130         */
    9721131        public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
    9731132                global $wpdb;
    9741133
    975                 if ( ! $meta_table = _get_meta_table( $type ) )
     1134                if ( ! $meta_table = _get_meta_table( $type ) ) {
    9761135                        return false;
     1136                }
    9771137
    978                 $meta_id_column = sanitize_key( $type . '_id' );
     1138                $this->meta_table     = $meta_table;
     1139                $this->meta_id_column = sanitize_key( $type . '_id' );
    9791140
    980                 $join = array();
    981                 $where = array();
     1141                $this->primary_table     = $primary_table;
     1142                $this->primary_id_column = $primary_id_column;
    9821143
    983                 $key_only_queries = array();
    984                 $queries = array();
     1144                $sql = $this->get_sql_clauses();
    9851145
    986                 // Split out the queries with empty arrays as value
    987                 foreach ( $this->queries as $k => $q ) {
    988                         if ( isset( $q['value'] ) && is_array( $q['value'] ) && empty( $q['value'] ) ) {
    989                                 $key_only_queries[$k] = $q;
    990                                 unset( $this->queries[$k] );
    991                         }
     1146                /**
     1147                 * Filter the meta query's generated SQL.
     1148                 *
     1149                 * @since 3.1.0
     1150                 *
     1151                 * @param array $args {
     1152                 *     An array of arguments.
     1153                 *
     1154                 *     @type array  $clauses           Array containing the query's JOIN and WHERE clauses.
     1155                 *     @type array  $queries           Array of meta queries.
     1156                 *     @type string $type              Type of meta.
     1157                 *     @type string $primary_table     Primary table.
     1158                 *     @type string $primary_id_column Primary column ID.
     1159                 *     @type object $context           The main query object.
     1160                 * }
     1161                 */
     1162                return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
     1163        }
     1164
     1165        /**
     1166         * Generate SQL clauses to be appended to a main query.
     1167         *
     1168         * Called by the public {@link WP_Meta_Query::get_sql()}, this method
     1169         * is abstracted out to maintain parity with the other Query classes.
     1170         *
     1171         * @since 4.1.0
     1172         * @access protected
     1173         *
     1174         * @return array {
     1175         *      @type string $join SQL fragment to append to the main JOIN clause.
     1176         *      @type string $where SQL fragment to append to the main WHERE clause.
     1177         * }
     1178         */
     1179        protected function get_sql_clauses() {
     1180                $sql = $this->get_sql_for_query( $this->queries );
     1181
     1182                if ( ! empty( $sql['where'] ) ) {
     1183                        $sql['where'] = ' AND ' . "\n" . $sql['where'] . "\n";
    9921184                }
    9931185
    994                 // Split out the meta_key only queries (we can only do this for OR)
    995                 if ( 'OR' == $this->relation ) {
    996                         foreach ( $this->queries as $k => $q ) {
    997                                 if ( ( empty( $q['compare'] ) || 'NOT EXISTS' != $q['compare'] ) && ! array_key_exists( 'value', $q ) && ! empty( $q['key'] ) )
    998                                         $key_only_queries[$k] = $q;
    999                                 else
    1000                                         $queries[$k] = $q;
     1186                return $sql;
     1187        }
     1188
     1189        /**
     1190         * Generate SQL clauses for a single query array.
     1191         *
     1192         * If nested subqueries are found, this method recurses the tree to
     1193         * produce the properly nested SQL.
     1194         *
     1195         * @since 4.1.0
     1196         * @access protected
     1197         *
     1198         * @param  array $query Query to parse.
     1199         * @param  int   $depth Optional. Number of tree levels deep we
     1200         *                      currently are. Used to calculate indentation.
     1201         * @return array {
     1202         *      @type string $join SQL fragment to append to the main JOIN clause.
     1203         *      @type string $where SQL fragment to append to the main WHERE clause.
     1204         * }
     1205         */
     1206        protected function get_sql_for_query( $query, $depth = 0 ) {
     1207                $sql_chunks = array(
     1208                        'join'  => array(),
     1209                        'where' => array(),
     1210                );
     1211
     1212                $sql = array(
     1213                        'join'  => '',
     1214                        'where' => '',
     1215                );
     1216
     1217                $indent = '';
     1218                for ( $i = 0; $i < $depth; $i++ ) {
     1219                        $indent .= "\t";
     1220                }
     1221
     1222                foreach ( $query as $key => $clause ) {
     1223                        if ( 'relation' === $key ) {
     1224                                $relation = $query['relation'];
     1225                        } else if ( is_array( $clause ) ) {
     1226
     1227                                // This is a first-order clause.
     1228                                if ( $this->is_first_order_clause( $clause ) ) {
     1229                                        $clause_sql = $this->get_sql_for_clause( $clause, $query );
     1230
     1231                                        $where_count = count( $clause_sql['where'] );
     1232                                        if ( ! $where_count ) {
     1233                                                $sql_chunks['where'][] = '';
     1234                                        } else if ( 1 === $where_count ) {
     1235                                                $sql_chunks['where'][] = $clause_sql['where'][0];
     1236                                        } else {
     1237                                                $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
     1238                                        }
     1239
     1240                                        $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
     1241                                // This is a subquery, so we recurse.
     1242                                } else {
     1243                                        $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
     1244
     1245                                        $sql_chunks['where'][] = $clause_sql['where'];
     1246                                        $sql_chunks['join'][]  = $clause_sql['join'];
     1247                                }
    10011248                        }
    1002                 } else {
    1003                         $queries = $this->queries;
    10041249                }
    10051250
    1006                 // Specify all the meta_key only queries in one go
    1007                 if ( $key_only_queries ) {
    1008                         $join[]  = "INNER JOIN $meta_table ON $primary_table.$primary_id_column = $meta_table.$meta_id_column";
     1251                // Filter to remove empties.
     1252                $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
     1253                $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
    10091254
    1010                         foreach ( $key_only_queries as $key => $q )
    1011                                 $where["key-only-$key"] = $wpdb->prepare( "$meta_table.meta_key = %s", trim( $q['key'] ) );
     1255                if ( empty( $relation ) ) {
     1256                        $relation = 'AND';
    10121257                }
    10131258
    1014                 foreach ( $queries as $k => $q ) {
    1015                         $meta_key = isset( $q['key'] ) ? trim( $q['key'] ) : '';
    1016                         $meta_type = $this->get_cast_for_type( isset( $q['type'] ) ? $q['type'] : '' );
     1259                // Filter duplicate JOIN clauses and combine into a single string.
     1260                if ( ! empty( $sql_chunks['join'] ) ) {
     1261                        $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
     1262                }
    10171263
    1018                         if ( array_key_exists( 'value', $q ) && is_null( $q['value'] ) )
    1019                                 $q['value'] = '';
     1264                // Generate a single WHERE clause with proper brackets and indentation.
     1265                if ( ! empty( $sql_chunks['where'] ) ) {
     1266                        $sql['where'] = '( ' . "\n\t" . $indent . implode( ' ' . "\n\t" . $indent . $relation . ' ' . "\n\t" . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')' . "\n";
     1267                }
    10201268
    1021                         $meta_value = isset( $q['value'] ) ? $q['value'] : null;
     1269                return $sql;
     1270        }
    10221271
    1023                         if ( isset( $q['compare'] ) )
    1024                                 $meta_compare = strtoupper( $q['compare'] );
    1025                         else
    1026                                 $meta_compare = is_array( $meta_value ) ? 'IN' : '=';
     1272        /**
     1273         * Generate SQL JOIN and WHERE clauses for a first-order query clause.
     1274         *
     1275         * "First-order" means that it's an array with a 'key' or 'value'.
     1276         *
     1277         * @param  array $clause       Query clause.
     1278         * @param  array $parent_query Parent query array.
     1279         * @return array {
     1280         *      @type string $join SQL fragment to append to the main JOIN clause.
     1281         *      @type string $where SQL fragment to append to the main WHERE clause.
     1282         * }
     1283         */
     1284        public function get_sql_for_clause( $clause, $parent_query ) {
     1285                global $wpdb;
    10271286
    1028                         if ( ! in_array( $meta_compare, array(
    1029                                 '=', '!=', '>', '>=', '<', '<=',
    1030                                 'LIKE', 'NOT LIKE',
    1031                                 'IN', 'NOT IN',
    1032                                 'BETWEEN', 'NOT BETWEEN',
    1033                                 'NOT EXISTS',
    1034                                 'REGEXP', 'NOT REGEXP', 'RLIKE'
    1035                         ) ) )
    1036                                 $meta_compare = '=';
     1287                $sql_chunks = array(
     1288                        'where' => array(),
     1289                        'join' => array(),
     1290                );
    10371291
    1038                         $i = count( $join );
    1039                         $alias = $i ? 'mt' . $i : $meta_table;
     1292                $i = count( $this->table_aliases );
     1293                $alias = $i ? 'mt' . $i : $this->meta_table;
    10401294
    1041                         if ( 'NOT EXISTS' == $meta_compare ) {
    1042                                 $join[$i]  = "LEFT JOIN $meta_table";
    1043                                 $join[$i] .= $i ? " AS $alias" : '';
    1044                                 $join[$i] .= " ON ($primary_table.$primary_id_column = $alias.$meta_id_column AND $alias.meta_key = '$meta_key')";
     1295                if ( isset( $clause['compare'] ) ) {
     1296                        $meta_compare = strtoupper( $clause['compare'] );
     1297                } else {
     1298                        $meta_compare = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
     1299                }
    10451300
    1046                                 $where[$k] = ' ' . $alias . '.' . $meta_id_column . ' IS NULL';
     1301                if ( ! in_array( $meta_compare, array(
     1302                        '=', '!=', '>', '>=', '<', '<=',
     1303                        'LIKE', 'NOT LIKE',
     1304                        'IN', 'NOT IN',
     1305                        'BETWEEN', 'NOT BETWEEN',
     1306                        'EXISTS', 'NOT EXISTS',
     1307                        'REGEXP', 'NOT REGEXP', 'RLIKE'
     1308                ) ) ) {
     1309                        $meta_compare = '=';
     1310                }
    10471311
    1048                                 continue;
     1312                /*
     1313                 * There are a number of different query structures that get
     1314                 * built in different ways.
     1315                 * 1. Key-only clauses - (a) clauses without a 'value' key that
     1316                 *    appear in the context of an OR relation and do not use
     1317                 *    'NOT EXISTS' as the 'compare', or (b) clauses with an
     1318                 *    empty array for 'value'.
     1319                 */
     1320                if ( ! empty( $clause['key'] ) && (
     1321                        ( ! array_key_exists( 'value', $clause ) && 'NOT EXISTS' !== $meta_compare && 'OR' === $parent_query['relation'] ) ||
     1322                        ( isset( $clause['value'] ) && is_array( $clause['value'] ) && empty( $clause['value'] ) )
     1323                ) ) {
     1324
     1325                        $alias = $this->meta_table;
     1326                        $sql_chunks['join'][] = " INNER JOIN $this->meta_table ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column)";
     1327                        $sql_chunks['where'][] = $wpdb->prepare( "$this->meta_table.meta_key = %s", trim( $clause['key'] ) );
     1328
     1329                // 2. NOT EXISTS.
     1330                } else if ( 'NOT EXISTS' === $meta_compare ) {
     1331                        $join  = " LEFT JOIN $this->meta_table";
     1332                        $join .= $i ? " AS $alias" : '';
     1333                        $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
     1334                        $sql_chunks['join'][] = $join;
     1335
     1336                        $sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
     1337
     1338                // 3. EXISTS and other key-only queries.
     1339                } else if ( 'EXISTS' === $meta_compare || ( ! empty( $clause['key'] ) && ! array_key_exists( 'value', $clause ) ) ) {
     1340                        $join  = " INNER JOIN $this->meta_table";
     1341                        $join .= $i ? " AS $alias" : '';
     1342                        $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
     1343                        $sql_chunks['join'][] = $join;
     1344
     1345                        $sql_chunks['where'][] = $wpdb->prepare( $alias . '.meta_key = %s', trim( $clause['key'] ) );
     1346
     1347                // 4. Clauses that have a value.
     1348                } else if ( array_key_exists( 'value', $clause ) ) {
     1349                        $join  = " INNER JOIN $this->meta_table";
     1350                        $join .= $i ? " AS $alias" : '';
     1351                        $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column)";
     1352                        $sql_chunks['join'][] = $join;
     1353
     1354                        if ( ! empty( $clause['key'] ) ) {
     1355                                $sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) );
    10491356                        }
    10501357
    1051                         $join[$i]  = "INNER JOIN $meta_table";
    1052                         $join[$i] .= $i ? " AS $alias" : '';
    1053                         $join[$i] .= " ON ($primary_table.$primary_id_column = $alias.$meta_id_column)";
    1054 
    1055                         $where[$k] = '';
    1056                         if ( !empty( $meta_key ) )
    1057                                 $where[$k] = $wpdb->prepare( "$alias.meta_key = %s", $meta_key );
     1358                        $meta_type = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' );
    10581359
    1059                         if ( is_null( $meta_value ) ) {
    1060                                 if ( empty( $where[$k] ) )
    1061                                         unset( $join[$i] );
    1062                                 continue;
    1063                         }
     1360                        $meta_value = isset( $clause['value'] ) ? $clause['value'] : '';
    10641361
    10651362                        if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
    1066                                 if ( ! is_array( $meta_value ) )
     1363                                if ( ! is_array( $meta_value ) ) {
    10671364                                        $meta_value = preg_split( '/[,\s]+/', $meta_value );
    1068 
    1069                                 if ( empty( $meta_value ) ) {
    1070                                         unset( $join[$i] );
    1071                                         continue;
    10721365                                }
    10731366                        } else {
    10741367                                $meta_value = trim( $meta_value );
    10751368                        }
    10761369
    1077                         if ( 'IN' == substr( $meta_compare, -2) ) {
     1370                        if ( 'IN' == substr( $meta_compare, -2 ) ) {
    10781371                                $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
    1079                         } elseif ( 'BETWEEN' == substr( $meta_compare, -7) ) {
     1372                        } elseif ( 'BETWEEN' == substr( $meta_compare, -7 ) ) {
    10801373                                $meta_value = array_slice( $meta_value, 0, 2 );
    10811374                                $meta_compare_string = '%s AND %s';
    10821375                        } elseif ( 'LIKE' == $meta_compare || 'NOT LIKE' == $meta_compare ) {
    class WP_Meta_Query { 
    10861379                                $meta_compare_string = '%s';
    10871380                        }
    10881381
    1089                         if ( ! empty( $where[$k] ) )
    1090                                 $where[$k] .= ' AND ';
    1091 
    1092                         $where[$k] = ' (' . $where[$k] . $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string})", $meta_value );
     1382                        $sql_chunks['where'][] = $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string}", $meta_value );
    10931383                }
    10941384
    1095                 $where = array_filter( $where );
    1096 
    1097                 if ( empty( $where ) )
    1098                         $where = '';
    1099                 else
    1100                         $where = ' AND (' . implode( "\n{$this->relation} ", $where ) . ' )';
     1385                /*
     1386                 * Multiple WHERE clauses (for meta_key and meta_value) should
     1387                 * be joined in parentheses.
     1388                 */
     1389                if ( 1 < count( $sql_chunks['where'] ) ) {
     1390                        $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
     1391                }
    11011392
    1102                 $join = implode( "\n", $join );
    1103                 if ( ! empty( $join ) )
    1104                         $join = ' ' . $join;
     1393                $this->table_aliases[] = $alias;
    11051394
    1106                 /**
    1107                  * Filter the meta query's generated SQL.
    1108                  *
    1109                  * @since 3.1.0
    1110                  *
    1111                  * @param array $args {
    1112                  *     An array of arguments.
    1113                  *
    1114                  *     @type array  $clauses           Array containing the query's JOIN and WHERE clauses.
    1115                  *     @type array  $queries           Array of meta queries.
    1116                  *     @type string $type              Type of meta.
    1117                  *     @type string $primary_table     Primary table.
    1118                  *     @type string $primary_id_column Primary column ID.
    1119                  *     @type object $context           The main query object.
    1120                  * }
    1121                  */
    1122                 return apply_filters_ref_array( 'get_meta_sql', array( compact( 'join', 'where' ), $this->queries, $type, $primary_table, $primary_id_column, $context ) );
     1395                return $sql_chunks;
    11231396        }
    11241397}
    11251398
  • tests/phpunit/tests/meta/query.php

    diff --git tests/phpunit/tests/meta/query.php tests/phpunit/tests/meta/query.php
    index fb2a1cc..390ccc9 100644
    class Tests_Meta_Query extends WP_UnitTestCase { 
    4040                        array(),
    4141                ) );
    4242
    43                 $this->assertSame( array( array() ), $query->queries );
     43                $this->assertSame( array(), $query->queries );
    4444        }
    4545
    4646        /**
    class Tests_Meta_Query extends WP_UnitTestCase { 
    153153                $query = new WP_Meta_Query();
    154154
    155155                // just meta_value
    156                 $query->parse_query_vars( array( 'meta_key' => 'abc' ) );
    157 
    158                 $this->assertEquals( array( array( 'key' => 'abc' ) ), $query->queries );
     156                $expected = array(
     157                        'relation' => 'OR',
     158                        array(
     159                                'key' => 'abc',
     160                        ),
     161                );
     162                $query->parse_query_vars( array(
     163                        'meta_key' => 'abc',
     164                ) );
     165                $this->assertEquals( $expected, $query->queries );
    159166
    160167                // meta_key & meta_value
    161                 $query->parse_query_vars( array( 'meta_key' => 'abc', 'meta_value' => 'def' ) );
    162 
    163                 $this->assertEquals( array( array( 'key' => 'abc', 'value' => 'def' ) ), $query->queries );
     168                $expected = array(
     169                        'relation' => 'OR',
     170                        array(
     171                                'key' => 'abc',
     172                                'value' => 'def',
     173                        ),
     174                );
     175                $query->parse_query_vars( array(
     176                        'meta_key' => 'abc',
     177                        'meta_value' => 'def',
     178                ) );
     179                $this->assertEquals( $expected, $query->queries );
    164180
    165181                // meta_compare
    166                 $query->parse_query_vars( array( 'meta_key' => 'abc', 'meta_compare' => '=>' ) );
    167 
    168                 $this->assertEquals( array( array( 'key' => 'abc', 'compare' => '=>' ) ), $query->queries );
     182                $expected = array(
     183                        'relation' => 'OR',
     184                        array(
     185                                'key' => 'abc',
     186                                'compare' => '=>',
     187                        ),
     188                );
     189                $query->parse_query_vars( array(
     190                        'meta_key' => 'abc',
     191                        'meta_compare' => '=>',
     192                ) );
     193                $this->assertEquals( $expected, $query->queries );
    169194        }
    170195
    171196        /**
    class Tests_Meta_Query extends WP_UnitTestCase { 
    202227                $this->assertEquals( 'CHAR', $query->get_cast_for_type( 'ANYTHING ELSE' ) );
    203228        }
    204229
     230        public function test_sanitize_query_single_query() {
     231                $expected = array(
     232                        'relation' => 'OR',
     233                        array(
     234                                'key' => 'foo',
     235                                'value' => 'bar',
     236                        ),
     237                );
     238
     239                $q = new WP_Meta_Query();
     240                $found = $q->sanitize_query( array(
     241                        array(
     242                                'key' => 'foo',
     243                                'value' => 'bar',
     244                        ),
     245                ) );
     246
     247                $this->assertEquals( $expected, $found );
     248        }
     249
     250        public function test_sanitize_query_multiple_first_order_queries_relation_default() {
     251                $expected = array(
     252                        'relation' => 'AND',
     253                        array(
     254                                'key' => 'foo',
     255                                'value' => 'bar',
     256                        ),
     257                        array(
     258                                'key' => 'foo2',
     259                                'value' => 'bar2',
     260                        ),
     261                );
     262
     263                $q = new WP_Meta_Query();
     264                $found = $q->sanitize_query( array(
     265                        array(
     266                                'key' => 'foo',
     267                                'value' => 'bar',
     268                        ),
     269                        array(
     270                                'key' => 'foo2',
     271                                'value' => 'bar2',
     272                        ),
     273                ) );
     274
     275                $this->assertEquals( $expected, $found );
     276        }
     277
     278        public function test_sanitize_query_multiple_first_order_queries_relation_or() {
     279                $expected = array(
     280                        'relation' => 'OR',
     281                        array(
     282                                'key' => 'foo',
     283                                'value' => 'bar',
     284                        ),
     285                        array(
     286                                'key' => 'foo2',
     287                                'value' => 'bar2',
     288                        ),
     289                );
     290
     291                $q = new WP_Meta_Query();
     292                $found = $q->sanitize_query( array(
     293                        'relation' => 'OR',
     294                        array(
     295                                'key' => 'foo',
     296                                'value' => 'bar',
     297                        ),
     298                        array(
     299                                'key' => 'foo2',
     300                                'value' => 'bar2',
     301                        ),
     302                ) );
     303
     304                $this->assertEquals( $expected, $found );
     305        }
     306
     307        public function test_sanitize_query_multiple_first_order_queries_relation_or_lowercase() {
     308                $expected = array(
     309                        'relation' => 'OR',
     310                        array(
     311                                'key' => 'foo',
     312                                'value' => 'bar',
     313                        ),
     314                        array(
     315                                'key' => 'foo2',
     316                                'value' => 'bar2',
     317                        ),
     318                );
     319
     320                $q = new WP_Meta_Query();
     321                $found = $q->sanitize_query( array(
     322                        'relation' => 'or',
     323                        array(
     324                                'key' => 'foo',
     325                                'value' => 'bar',
     326                        ),
     327                        array(
     328                                'key' => 'foo2',
     329                                'value' => 'bar2',
     330                        ),
     331                ) );
     332
     333                $this->assertEquals( $expected, $found );
     334        }
     335
     336        public function test_sanitize_query_multiple_first_order_queries_invalid_relation() {
     337                $expected = array(
     338                        'relation' => 'AND',
     339                        array(
     340                                'key' => 'foo',
     341                                'value' => 'bar',
     342                        ),
     343                        array(
     344                                'key' => 'foo2',
     345                                'value' => 'bar2',
     346                        ),
     347                );
     348
     349                $q = new WP_Meta_Query();
     350                $found = $q->sanitize_query( array(
     351                        'relation' => 'FOO',
     352                        array(
     353                                'key' => 'foo',
     354                                'value' => 'bar',
     355                        ),
     356                        array(
     357                                'key' => 'foo2',
     358                                'value' => 'bar2',
     359                        ),
     360                ) );
     361
     362                $this->assertEquals( $expected, $found );
     363        }
     364
     365        public function test_sanitize_query_single_query_which_is_a_nested_query() {
     366                $expected = array(
     367                        'relation' => 'OR',
     368                        array(
     369                                'relation' => 'AND',
     370                                array(
     371                                        'key' => 'foo',
     372                                        'value' => 'bar',
     373                                ),
     374                                array(
     375                                        'key' => 'foo2',
     376                                        'value' => 'bar2',
     377                                ),
     378                        )
     379                );
     380
     381                $q = new WP_Meta_Query();
     382                $found = $q->sanitize_query( array(
     383                        array(
     384                                array(
     385                                        'key' => 'foo',
     386                                        'value' => 'bar',
     387                                ),
     388                                array(
     389                                        'key' => 'foo2',
     390                                        'value' => 'bar2',
     391                                ),
     392                        ),
     393                ) );
     394
     395                $this->assertEquals( $expected, $found );
     396        }
     397
     398        public function test_sanitize_query_multiple_nested_queries() {
     399                $expected = array(
     400                        'relation' => 'OR',
     401                        array(
     402                                'relation' => 'AND',
     403                                array(
     404                                        'key' => 'foo',
     405                                        'value' => 'bar',
     406                                ),
     407                                array(
     408                                        'key' => 'foo2',
     409                                        'value' => 'bar2',
     410                                ),
     411                        ),
     412                        array(
     413                                'relation' => 'AND',
     414                                array(
     415                                        'key' => 'foo3',
     416                                        'value' => 'bar3',
     417                                ),
     418                                array(
     419                                        'key' => 'foo4',
     420                                        'value' => 'bar4',
     421                                ),
     422                        ),
     423                );
     424
     425                $q = new WP_Meta_Query();
     426                $found = $q->sanitize_query( array(
     427                        'relation' => 'OR',
     428                        array(
     429                                array(
     430                                        'key' => 'foo',
     431                                        'value' => 'bar',
     432                                ),
     433                                array(
     434                                        'key' => 'foo2',
     435                                        'value' => 'bar2',
     436                                ),
     437                        ),
     438                        array(
     439                                array(
     440                                        'key' => 'foo3',
     441                                        'value' => 'bar3',
     442                                ),
     443                                array(
     444                                        'key' => 'foo4',
     445                                        'value' => 'bar4',
     446                                ),
     447                        ),
     448                ) );
     449
     450                $this->assertEquals( $expected, $found );
     451        }
     452
    205453        /**
    206454         * Invalid $type will fail to get a table from _get_meta_table()
    207455         */
    class Tests_Meta_Query extends WP_UnitTestCase { 
    229477                $sql = $query->get_sql( 'post', $wpdb->posts, 'ID', $this );
    230478
    231479                // We should have 2 joins - one for my_first_key and one for my_second_key
    232                 $this->assertEquals( 2, substr_count( $sql['join'], 'INNER JOIN' ) );
     480                $this->assertEquals( 2, substr_count( $sql['join'], 'JOIN' ) );
    233481
    234482                // The WHERE should check my_third_key against an unaliased table
    235483                $this->assertEquals( 1, substr_count( $sql['where'], "$wpdb->postmeta.meta_key = 'my_third_key'" ) );
    class Tests_Meta_Query extends WP_UnitTestCase { 
    247495                ) );
    248496                $sql = $query->get_sql( 'post', $wpdb->posts, 'ID', $this );
    249497
    250                 $this->assertEquals( 1, substr_count( $sql['where'], "CAST($wpdb->postmeta.meta_value AS CHAR) = '')" ) );
     498                $this->assertEquals( 1, substr_count( $sql['where'], "CAST($wpdb->postmeta.meta_value AS CHAR) = ''" ) );
    251499        }
    252500
    253501        /**
    class Tests_Meta_Query extends WP_UnitTestCase { 
    558806                ) );
    559807
    560808                $sql = $query->get_sql( 'post', $wpdb->posts, 'ID', $this );
    561                 $this->assertContains( "{$wpdb->postmeta}.meta_key = 'exclude'\nOR", $sql['where'] );
     809
     810                // Use regex because we don't care about the whitespace before OR.
     811                $this->assertRegExp( "/{$wpdb->postmeta}\.meta_key = \'exclude\'\s+OR/", $sql['where'] );
    562812                $this->assertNotContains( "{$wpdb->postmeta}.post_id IS NULL", $sql['where'] );
    563813        }
    564814}
  • tests/phpunit/tests/post/query.php

    diff --git tests/phpunit/tests/post/query.php tests/phpunit/tests/post/query.php
    index 0539edc..dbe157e 100644
    class Tests_Post_Query extends WP_UnitTestCase { 
    14081408                $this->assertEquals( array( $p1, $p2 ), $q->posts );
    14091409        }
    14101410
     1411        public function test_meta_query_nested() {
     1412                $p1 = $this->factory->post->create();
     1413                $p2 = $this->factory->post->create();
     1414                $p3 = $this->factory->post->create();
     1415
     1416                add_post_meta( $p1, 'foo', 'bar' );
     1417                add_post_meta( $p2, 'foo2', 'bar' );
     1418                add_post_meta( $p3, 'foo2', 'bar' );
     1419                add_post_meta( $p3, 'foo3', 'bar' );
     1420
     1421                $query = new WP_Query( array(
     1422                        'update_post_meta_cache' => false,
     1423                        'update_term_meta_cache' => false,
     1424                        'fields' => 'ids',
     1425                        'meta_query' => array(
     1426                                'relation' => 'OR',
     1427                                array(
     1428                                        'key' => 'foo',
     1429                                        'value' => 'bar',
     1430                                ),
     1431                                array(
     1432                                        'relation' => 'AND',
     1433                                        array(
     1434                                                'key' => 'foo2',
     1435                                                'value' => 'bar',
     1436                                        ),
     1437                                        array(
     1438                                                'key' => 'foo3',
     1439                                                'value' => 'bar',
     1440                                        ),
     1441                                ),
     1442                        ),
     1443                ) );
     1444
     1445                $expected = array( $p1, $p3 );
     1446                $this->assertEqualSets( $expected, $query->posts );
     1447        }
     1448
     1449        public function test_meta_query_nested_two_levels_deep() {
     1450                $p1 = $this->factory->post->create();
     1451                $p2 = $this->factory->post->create();
     1452                $p3 = $this->factory->post->create();
     1453
     1454                add_post_meta( $p1, 'foo', 'bar' );
     1455                add_post_meta( $p3, 'foo2', 'bar' );
     1456                add_post_meta( $p3, 'foo3', 'bar' );
     1457                add_post_meta( $p3, 'foo4', 'bar' );
     1458
     1459                $query = new WP_Query( array(
     1460                        'update_post_meta_cache' => false,
     1461                        'update_term_meta_cache' => false,
     1462                        'fields' => 'ids',
     1463                        'meta_query' => array(
     1464                                'relation' => 'OR',
     1465                                array(
     1466                                        'key' => 'foo',
     1467                                        'value' => 'bar',
     1468                                ),
     1469                                array(
     1470                                        'relation' => 'OR',
     1471                                        array(
     1472                                                'key' => 'foo2',
     1473                                                'value' => 'bar',
     1474                                        ),
     1475                                        array(
     1476                                                'relation' => 'AND',
     1477                                                array(
     1478                                                        'key' => 'foo3',
     1479                                                        'value' => 'bar',
     1480                                                ),
     1481                                                array(
     1482                                                        'key' => 'foo4',
     1483                                                        'value' => 'bar',
     1484                                                ),
     1485                                        ),
     1486                                ),
     1487                        ),
     1488                ) );
     1489
     1490                $expected = array( $p1, $p3 );
     1491                $this->assertEqualSets( $expected, $query->posts );
     1492        }
     1493
    14111494        /**
    14121495         * @ticket 20604
    14131496         * @group taxonomy