Make WordPress Core

Ticket #29642: 29642.5.patch

File 29642.5.patch, 30.8 KB (added by DrewAPicture, 11 years ago)

Docs adjustments

  • src/wp-includes/meta.php

     
    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.
    853853 *
     854 * `WP_Meta_Query` is a helper that allows primary query classes, such as {@see WP_Query} and {@see WP_User_Query},
     855 * to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached
     856 * to the primary SQL query string.
     857 *
    854858 * @since 3.2.0
    855859 */
    856860class WP_Meta_Query {
    857861        /**
    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'
    868         *
    869         * @since 3.2.0
    870         * @access public
    871         * @var array
    872         */
     862         * Array of metadata queries.
     863         *
     864         * See {@see WP_Meta_Query::__construct()} for information on meta query arguments.
     865         *
     866         * @since 3.2.0
     867         * @access public
     868         * @var array
     869         */
    873870        public $queries = array();
    874871
    875872        /**
     
    882879        public $relation;
    883880
    884881        /**
    885          * Constructor
     882         * Database table to query for the metadata.
    886883         *
    887          * @param array $meta_query (optional) A meta query
     884         * @since 4.1.0
     885         * @access public
     886         * @var string
    888887         */
     888        public $meta_table;
     889
     890        /**
     891         * Column in meta_table that represents the ID of the object the metadata belongs to.
     892         *
     893         * @since 4.1.0
     894         * @access public
     895         * @var string
     896         */
     897        public $meta_id_column;
     898
     899        /**
     900         * Database table that where the metadata's objects are stored (eg $wpdb->users).
     901         *
     902         * @since 4.1.0
     903         * @access public
     904         * @var string
     905         */
     906        public $primary_table;
     907
     908        /**
     909         * Column in primary_table that represents the ID of the object.
     910         *
     911         * @since 4.1.0
     912         * @access public
     913         * @var string
     914         */
     915        public $primary_id_column;
     916
     917        /**
     918         * A flat list of table aliases used in JOIN clauses.
     919         *
     920         * @since 4.1.0
     921         * @access protected
     922         * @var array
     923         */
     924        protected $table_aliases = array();
     925
     926        /**
     927         * Constructor.
     928         *
     929         * @since 3.2.0
     930         * @access public
     931         *
     932         * @param array $meta_query {
     933         *     Array of meta query clauses.
     934         *
     935         *     @type string $relation Optional. The MySQL keyword used to join
     936         *                            the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'.
     937         *     @type array {
     938         *         Optional. An array of first-order clause parameters, or another fully-formed meta query.
     939         *
     940         *         @type string $key     Meta key to filter by.
     941         *         @type string $value   Meta value to filter by.
     942         *         @type string $compare MySQL operator used for comparing the $value. Accepts '=',
     943         *                               '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN',
     944         *                               'BETWEEN', 'NOT BETWEEN', 'REGEXP', 'NOT REGEXP', or 'RLIKE'.
     945         *                               Default is 'IN when `$value` is an array, '=' otherwise.
     946         *         @type string $type    MySQL data type that the meta_value column will be CAST to for
     947         *                               comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE',
     948         *                               'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'.
     949         *                               Default is 'CHAR'.
     950         *     }
     951         * }
     952         */
    889953        public function __construct( $meta_query = false ) {
    890954                if ( !$meta_query )
    891955                        return;
     
    896960                        $this->relation = 'AND';
    897961                }
    898962
    899                 $this->queries = array();
     963                $this->queries = $this->sanitize_query( $meta_query );
     964        }
    900965
    901                 foreach ( $meta_query as $key => $query ) {
    902                         if ( ! is_array( $query ) )
    903                                 continue;
     966        /**
     967         * Ensure the `meta_query` argument passed to the class constructor is well-formed.
     968         *
     969         * Eliminates empty items and ensures that a 'relation' is set.
     970         *
     971         * @since 4.1.0
     972         * @access public
     973         *
     974         * @param array $queries Array of query clauses.
     975         * @return array Sanitized array of query clauses.
     976         */
     977        public function sanitize_query( $queries ) {
     978                $clean_queries = array();
    904979
    905                         $this->queries[] = $query;
     980                if ( ! is_array( $queries ) ) {
     981                        return $clean_queries;
    906982                }
     983
     984                foreach ( $queries as $key => $query ) {
     985                        if ( 'relation' === $key ) {
     986                                $relation = $query;
     987
     988                        // First-order clause.
     989                        } else if ( $this->is_first_order_clause( $query ) ) {
     990                                $clean_queries[] = $query;
     991
     992                        // Otherwise, it's a nested query.
     993                        } else {
     994                                $cleaned_query = $this->sanitize_query( $query );
     995
     996                                if ( ! empty( $cleaned_query ) ) {
     997                                        $clean_queries[] = $cleaned_query;
     998                                }
     999                        }
     1000                }
     1001
     1002                if ( empty( $clean_queries ) ) {
     1003                        return $clean_queries;
     1004                }
     1005
     1006                // Sanitize the 'relation' key provided in the query.
     1007                if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
     1008                        $clean_queries['relation'] = 'OR';
     1009
     1010                /*
     1011                 * If there is only a single clause, call the relation 'OR'.
     1012                 * This value will not actually be used to join clauses, but it
     1013                 * simplifies the logic around combining key-only queries.
     1014                 */
     1015                } else if ( 1 === count( $clean_queries ) ) {
     1016                        $clean_queries['relation'] = 'OR';
     1017
     1018                // Default to AND.
     1019                } else {
     1020                        $clean_queries['relation'] = 'AND';
     1021                }
     1022
     1023                return $clean_queries;
    9071024        }
    9081025
    9091026        /**
     1027         * Determine whether a query clause is first-order.
     1028         *
     1029         * A first-order meta query clause is one that has either a 'key' or
     1030         * a 'value' array key.
     1031         *
     1032         * @since 4.1.0
     1033         * @access protected
     1034         *
     1035         * @param array $query Meta query arguments.
     1036         * @return bool Whether the query clause is a first-order clause.
     1037         */
     1038        protected function is_first_order_clause( $query ) {
     1039                return isset( $query['key'] ) || isset( $query['value'] );
     1040        }
     1041
     1042        /**
    9101043         * Constructs a meta query based on 'meta_*' query vars
    9111044         *
    9121045         * @since 3.2.0
     
    9171050        public function parse_query_vars( $qv ) {
    9181051                $meta_query = array();
    9191052
    920                 // Simple query needs to be first for orderby=meta_value to work correctly
     1053                // Simple query needs to be first for orderby=meta_value to work correctly.
    9211054                foreach ( array( 'key', 'compare', 'type' ) as $key ) {
    9221055                        if ( !empty( $qv[ "meta_$key" ] ) )
    9231056                                $meta_query[0][ $key ] = $qv[ "meta_$key" ];
    9241057                }
    9251058
    926                 // WP_Query sets 'meta_value' = '' by default
     1059                // WP_Query sets 'meta_value' = '' by default.
    9271060                if ( isset( $qv[ 'meta_value' ] ) && '' !== $qv[ 'meta_value' ] && ( ! is_array( $qv[ 'meta_value' ] ) || $qv[ 'meta_value' ] ) )
    9281061                        $meta_query[0]['value'] = $qv[ 'meta_value' ];
    9291062
     
    9351068        }
    9361069
    9371070        /**
    938          * Given a meta type, return the appropriate alias if applicable
     1071         * Return the appropriate alias for the given meta type if applicable.
    9391072         *
    9401073         * @since 3.7.0
     1074         * @access public
    9411075         *
    942          * @param string $type MySQL type to cast meta_value
    943          * @return string MySQL type
     1076         * @param string $type MySQL type to cast meta_value.
     1077         * @return string MySQL type.
    9441078         */
    9451079        public function get_cast_for_type( $type = '' ) {
    9461080                if ( empty( $type ) )
     
    9631097         * @since 3.2.0
    9641098         * @access public
    9651099         *
    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 )
     1100         * @param string $type              Type of meta, eg 'user', 'post'.
     1101         * @param string $primary_table     Database table where the object being filtered is stored (eg wp_users).
     1102         * @param string $primary_id_column ID column for the filtered object in $primary_table.
     1103         * @param object $context           Optional. The main query object.
     1104         * @return array {
     1105         *     Array containing JOIN and WHERE SQL clauses to append to the main query.
     1106         *
     1107         *     @type string $join  SQL fragment to append to the main JOIN clause.
     1108         *     @type string $where SQL fragment to append to the main WHERE clause.
     1109         * }
    9711110         */
    9721111        public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
    9731112                global $wpdb;
    9741113
    975                 if ( ! $meta_table = _get_meta_table( $type ) )
     1114                if ( ! $meta_table = _get_meta_table( $type ) ) {
    9761115                        return false;
     1116                }
    9771117
    978                 $meta_id_column = sanitize_key( $type . '_id' );
     1118                $this->meta_table     = $meta_table;
     1119                $this->meta_id_column = sanitize_key( $type . '_id' );
    9791120
    980                 $join = array();
    981                 $where = array();
     1121                $this->primary_table     = $primary_table;
     1122                $this->primary_id_column = $primary_id_column;
    9821123
    983                 $key_only_queries = array();
    984                 $queries = array();
     1124                $sql = $this->get_sql_clauses();
    9851125
    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                         }
     1126                /**
     1127                 * Filter the meta query's generated SQL.
     1128                 *
     1129                 * @since 3.1.0
     1130                 *
     1131                 * @param array $args {
     1132                 *     An array of meta query SQL arguments.
     1133                 *
     1134                 *     @type array  $clauses           Array containing the query's JOIN and WHERE clauses.
     1135                 *     @type array  $queries           Array of meta queries.
     1136                 *     @type string $type              Type of meta.
     1137                 *     @type string $primary_table     Primary table.
     1138                 *     @type string $primary_id_column Primary column ID.
     1139                 *     @type object $context           The main query object.
     1140                 * }
     1141                 */
     1142                return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
     1143        }
     1144
     1145        /**
     1146         * Generate SQL clauses to be appended to a main query.
     1147         *
     1148         * Called by the public {@see WP_Meta_Query::get_sql()}, this method
     1149         * is abstracted out to maintain parity with the other Query classes.
     1150         *
     1151         * @since 4.1.0
     1152         * @access protected
     1153         *
     1154         * @return array {
     1155         *     Array containing JOIN and WHERE SQL clauses to append to the main query.
     1156         *
     1157         *     @type string $join  SQL fragment to append to the main JOIN clause.
     1158         *     @type string $where SQL fragment to append to the main WHERE clause.
     1159         * }
     1160         */
     1161        protected function get_sql_clauses() {
     1162                $sql = $this->get_sql_for_query( $this->queries );
     1163
     1164                if ( ! empty( $sql['where'] ) ) {
     1165                        $sql['where'] = ' AND ' . "\n" . $sql['where'] . "\n";
    9921166                }
    9931167
    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;
     1168                return $sql;
     1169        }
     1170
     1171        /**
     1172         * Generate SQL clauses for a single query array.
     1173         *
     1174         * If nested subqueries are found, this method recurses the tree to
     1175         * produce the properly nested SQL.
     1176         *
     1177         * @since 4.1.0
     1178         * @access protected
     1179         *
     1180         * @param array $query Query to parse.
     1181         * @param int   $depth Optional. Number of tree levels deep we currently are.
     1182         *               Used to calculate indentation.
     1183         * @return array {
     1184         *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
     1185         *
     1186         *     @type string $join  SQL fragment to append to the main JOIN clause.
     1187         *     @type string $where SQL fragment to append to the main WHERE clause.
     1188         * }
     1189         */
     1190        protected function get_sql_for_query( $query, $depth = 0 ) {
     1191                $sql_chunks = array(
     1192                        'join'  => array(),
     1193                        'where' => array(),
     1194                );
     1195
     1196                $sql = array(
     1197                        'join'  => '',
     1198                        'where' => '',
     1199                );
     1200
     1201                $indent = '';
     1202                for ( $i = 0; $i < $depth; $i++ ) {
     1203                        $indent .= "\t";
     1204                }
     1205
     1206                foreach ( $query as $key => $clause ) {
     1207                        if ( 'relation' === $key ) {
     1208                                $relation = $query['relation'];
     1209                        } else if ( is_array( $clause ) ) {
     1210
     1211                                // This is a first-order clause.
     1212                                if ( $this->is_first_order_clause( $clause ) ) {
     1213                                        $clause_sql = $this->get_sql_for_clause( $clause, $query );
     1214
     1215                                        $where_count = count( $clause_sql['where'] );
     1216                                        if ( ! $where_count ) {
     1217                                                $sql_chunks['where'][] = '';
     1218                                        } else if ( 1 === $where_count ) {
     1219                                                $sql_chunks['where'][] = $clause_sql['where'][0];
     1220                                        } else {
     1221                                                $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
     1222                                        }
     1223
     1224                                        $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
     1225                                // This is a subquery, so we recurse.
     1226                                } else {
     1227                                        $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
     1228
     1229                                        $sql_chunks['where'][] = $clause_sql['where'];
     1230                                        $sql_chunks['join'][]  = $clause_sql['join'];
     1231                                }
    10011232                        }
    1002                 } else {
    1003                         $queries = $this->queries;
    10041233                }
    10051234
    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";
     1235                // Filter to remove empties.
     1236                $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
     1237                $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
    10091238
    1010                         foreach ( $key_only_queries as $key => $q )
    1011                                 $where["key-only-$key"] = $wpdb->prepare( "$meta_table.meta_key = %s", trim( $q['key'] ) );
     1239                if ( empty( $relation ) ) {
     1240                        $relation = 'AND';
    10121241                }
    10131242
    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'] : '' );
     1243                // Filter duplicate JOIN clauses and combine into a single string.
     1244                if ( ! empty( $sql_chunks['join'] ) ) {
     1245                        $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
     1246                }
    10171247
    1018                         if ( array_key_exists( 'value', $q ) && is_null( $q['value'] ) )
    1019                                 $q['value'] = '';
     1248                // Generate a single WHERE clause with proper brackets and indentation.
     1249                if ( ! empty( $sql_chunks['where'] ) ) {
     1250                        $sql['where'] = '( ' . "\n\t" . $indent . implode( ' ' . "\n\t" . $indent . $relation . ' ' . "\n\t" . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')' . "\n";
     1251                }
    10201252
    1021                         $meta_value = isset( $q['value'] ) ? $q['value'] : null;
     1253                return $sql;
     1254        }
    10221255
    1023                         if ( isset( $q['compare'] ) )
    1024                                 $meta_compare = strtoupper( $q['compare'] );
    1025                         else
    1026                                 $meta_compare = is_array( $meta_value ) ? 'IN' : '=';
     1256        /**
     1257         * Generate SQL JOIN and WHERE clauses for a first-order query clause.
     1258         *
     1259         * "First-order" means that it's an array with a 'key' or 'value'.
     1260         *
     1261         * @since 4.1.0
     1262         * @access public
     1263         *
     1264         * @param array $clause       Query clause.
     1265         * @param array $parent_query Parent query array.
     1266         * @return array {
     1267         *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
     1268         *
     1269         *     @type string $join  SQL fragment to append to the main JOIN clause.
     1270         *     @type string $where SQL fragment to append to the main WHERE clause.
     1271         * }
     1272         */
     1273        public function get_sql_for_clause( $clause, $parent_query ) {
     1274                global $wpdb;
    10271275
    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 = '=';
     1276                $sql_chunks = array(
     1277                        'where' => array(),
     1278                        'join' => array(),
     1279                );
    10371280
    1038                         $i = count( $join );
    1039                         $alias = $i ? 'mt' . $i : $meta_table;
     1281                $i = count( $this->table_aliases );
     1282                $alias = $i ? 'mt' . $i : $this->meta_table;
    10401283
    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')";
     1284                if ( isset( $clause['compare'] ) ) {
     1285                        $meta_compare = strtoupper( $clause['compare'] );
     1286                } else {
     1287                        $meta_compare = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
     1288                }
    10451289
    1046                                 $where[$k] = ' ' . $alias . '.' . $meta_id_column . ' IS NULL';
     1290                if ( ! in_array( $meta_compare, array(
     1291                        '=', '!=', '>', '>=', '<', '<=',
     1292                        'LIKE', 'NOT LIKE',
     1293                        'IN', 'NOT IN',
     1294                        'BETWEEN', 'NOT BETWEEN',
     1295                        'EXISTS', 'NOT EXISTS',
     1296                        'REGEXP', 'NOT REGEXP', 'RLIKE'
     1297                ) ) ) {
     1298                        $meta_compare = '=';
     1299                }
    10471300
    1048                                 continue;
    1049                         }
     1301                /*
     1302                 * There are a number of different query structures that get
     1303                 * built in different ways.
     1304                 * 1. Key-only clauses - (a) clauses without a 'value' key that
     1305                 *    appear in the context of an OR relation and do not use
     1306                 *    'NOT EXISTS' as the 'compare', or (b) clauses with an
     1307                 *    empty array for 'value'.
     1308                 */
     1309                if ( ! empty( $clause['key'] ) && (
     1310                        ( ! array_key_exists( 'value', $clause ) && 'NOT EXISTS' !== $meta_compare && 'OR' === $parent_query['relation'] ) ||
     1311                        ( isset( $clause['value'] ) && is_array( $clause['value'] ) && empty( $clause['value'] ) )
     1312                ) ) {
    10501313
    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)";
     1314                        $alias = $this->meta_table;
     1315                        $sql_chunks['join'][] = " INNER JOIN $this->meta_table ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column)";
     1316                        $sql_chunks['where'][] = $wpdb->prepare( "$this->meta_table.meta_key = %s", trim( $clause['key'] ) );
    10541317
    1055                         $where[$k] = '';
    1056                         if ( !empty( $meta_key ) )
    1057                                 $where[$k] = $wpdb->prepare( "$alias.meta_key = %s", $meta_key );
     1318                // 2. NOT EXISTS.
     1319                } else if ( 'NOT EXISTS' === $meta_compare ) {
     1320                        $join  = " LEFT JOIN $this->meta_table";
     1321                        $join .= $i ? " AS $alias" : '';
     1322                        $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
     1323                        $sql_chunks['join'][] = $join;
    10581324
    1059                         if ( is_null( $meta_value ) ) {
    1060                                 if ( empty( $where[$k] ) )
    1061                                         unset( $join[$i] );
    1062                                 continue;
     1325                        $sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
     1326
     1327                // 3. EXISTS and other key-only queries.
     1328                } else if ( 'EXISTS' === $meta_compare || ( ! empty( $clause['key'] ) && ! array_key_exists( 'value', $clause ) ) ) {
     1329                        $join  = " INNER JOIN $this->meta_table";
     1330                        $join .= $i ? " AS $alias" : '';
     1331                        $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
     1332                        $sql_chunks['join'][] = $join;
     1333
     1334                        $sql_chunks['where'][] = $wpdb->prepare( $alias . '.meta_key = %s', trim( $clause['key'] ) );
     1335
     1336                // 4. Clauses that have a value.
     1337                } else if ( array_key_exists( 'value', $clause ) ) {
     1338                        $join  = " INNER JOIN $this->meta_table";
     1339                        $join .= $i ? " AS $alias" : '';
     1340                        $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column)";
     1341                        $sql_chunks['join'][] = $join;
     1342
     1343                        if ( ! empty( $clause['key'] ) ) {
     1344                                $sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) );
    10631345                        }
    10641346
     1347                        $meta_type = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' );
     1348
     1349                        $meta_value = isset( $clause['value'] ) ? $clause['value'] : '';
     1350
    10651351                        if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
    1066                                 if ( ! is_array( $meta_value ) )
     1352                                if ( ! is_array( $meta_value ) ) {
    10671353                                        $meta_value = preg_split( '/[,\s]+/', $meta_value );
    1068 
    1069                                 if ( empty( $meta_value ) ) {
    1070                                         unset( $join[$i] );
    1071                                         continue;
    10721354                                }
    10731355                        } else {
    10741356                                $meta_value = trim( $meta_value );
    10751357                        }
    10761358
    1077                         if ( 'IN' == substr( $meta_compare, -2) ) {
     1359                        if ( 'IN' == substr( $meta_compare, -2 ) ) {
    10781360                                $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
    1079                         } elseif ( 'BETWEEN' == substr( $meta_compare, -7) ) {
     1361                        } elseif ( 'BETWEEN' == substr( $meta_compare, -7 ) ) {
    10801362                                $meta_value = array_slice( $meta_value, 0, 2 );
    10811363                                $meta_compare_string = '%s AND %s';
    10821364                        } elseif ( 'LIKE' == $meta_compare || 'NOT LIKE' == $meta_compare ) {
     
    10861368                                $meta_compare_string = '%s';
    10871369                        }
    10881370
    1089                         if ( ! empty( $where[$k] ) )
    1090                                 $where[$k] .= ' AND ';
     1371                        $sql_chunks['where'][] = $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string}", $meta_value );
     1372                }
    10911373
    1092                         $where[$k] = ' (' . $where[$k] . $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string})", $meta_value );
     1374                /*
     1375                 * Multiple WHERE clauses (for meta_key and meta_value) should
     1376                 * be joined in parentheses.
     1377                 */
     1378                if ( 1 < count( $sql_chunks['where'] ) ) {
     1379                        $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
    10931380                }
    10941381
    1095                 $where = array_filter( $where );
     1382                $this->table_aliases[] = $alias;
    10961383
    1097                 if ( empty( $where ) )
    1098                         $where = '';
    1099                 else
    1100                         $where = ' AND (' . implode( "\n{$this->relation} ", $where ) . ' )';
    1101 
    1102                 $join = implode( "\n", $join );
    1103                 if ( ! empty( $join ) )
    1104                         $join = ' ' . $join;
    1105 
    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 ) );
     1384                return $sql_chunks;
    11231385        }
    11241386}
    11251387
  • tests/phpunit/tests/meta/query.php

     
    4040                        array(),
    4141                ) );
    4242
    43                 $this->assertSame( array( array() ), $query->queries );
     43                $this->assertSame( array(), $query->queries );
    4444        }
    4545
    4646        /**
     
    153153                $query = new WP_Meta_Query();
    154154
    155155                // just meta_value
    156                 $query->parse_query_vars( array( 'meta_key' => 'abc' ) );
     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 );
    157166
    158                 $this->assertEquals( array( array( 'key' => 'abc' ) ), $query->queries );
    159 
    160167                // meta_key & meta_value
    161                 $query->parse_query_vars( array( 'meta_key' => 'abc', 'meta_value' => 'def' ) );
     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 );
    162180
    163                 $this->assertEquals( array( array( 'key' => 'abc', 'value' => 'def' ) ), $query->queries );
    164 
    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        /**
     
    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         */
     
    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'" ) );
     
    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        /**
     
    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

     
    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