Ticket #29642: 29642.5.patch
File 29642.5.patch, 30.8 KB (added by , 11 years ago) |
---|
-
src/wp-includes/meta.php
849 849 } 850 850 851 851 /** 852 * C ontainer class for a multiple metadata query852 * Class for generating SQL clauses that filter a primary query according to metadata keys and values. 853 853 * 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 * 854 858 * @since 3.2.0 855 859 */ 856 860 class WP_Meta_Query { 857 861 /** 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 */ 873 870 public $queries = array(); 874 871 875 872 /** … … 882 879 public $relation; 883 880 884 881 /** 885 * Constructor882 * Database table to query for the metadata. 886 883 * 887 * @param array $meta_query (optional) A meta query 884 * @since 4.1.0 885 * @access public 886 * @var string 888 887 */ 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 */ 889 953 public function __construct( $meta_query = false ) { 890 954 if ( !$meta_query ) 891 955 return; … … 896 960 $this->relation = 'AND'; 897 961 } 898 962 899 $this->queries = array(); 963 $this->queries = $this->sanitize_query( $meta_query ); 964 } 900 965 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(); 904 979 905 $this->queries[] = $query; 980 if ( ! is_array( $queries ) ) { 981 return $clean_queries; 906 982 } 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; 907 1024 } 908 1025 909 1026 /** 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 /** 910 1043 * Constructs a meta query based on 'meta_*' query vars 911 1044 * 912 1045 * @since 3.2.0 … … 917 1050 public function parse_query_vars( $qv ) { 918 1051 $meta_query = array(); 919 1052 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. 921 1054 foreach ( array( 'key', 'compare', 'type' ) as $key ) { 922 1055 if ( !empty( $qv[ "meta_$key" ] ) ) 923 1056 $meta_query[0][ $key ] = $qv[ "meta_$key" ]; 924 1057 } 925 1058 926 // WP_Query sets 'meta_value' = '' by default 1059 // WP_Query sets 'meta_value' = '' by default. 927 1060 if ( isset( $qv[ 'meta_value' ] ) && '' !== $qv[ 'meta_value' ] && ( ! is_array( $qv[ 'meta_value' ] ) || $qv[ 'meta_value' ] ) ) 928 1061 $meta_query[0]['value'] = $qv[ 'meta_value' ]; 929 1062 … … 935 1068 } 936 1069 937 1070 /** 938 * Given a meta type, return the appropriate alias if applicable1071 * Return the appropriate alias for the given meta type if applicable. 939 1072 * 940 1073 * @since 3.7.0 1074 * @access public 941 1075 * 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. 944 1078 */ 945 1079 public function get_cast_for_type( $type = '' ) { 946 1080 if ( empty( $type ) ) … … 963 1097 * @since 3.2.0 964 1098 * @access public 965 1099 * 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 * } 971 1110 */ 972 1111 public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) { 973 1112 global $wpdb; 974 1113 975 if ( ! $meta_table = _get_meta_table( $type ) ) 1114 if ( ! $meta_table = _get_meta_table( $type ) ) { 976 1115 return false; 1116 } 977 1117 978 $meta_id_column = sanitize_key( $type . '_id' ); 1118 $this->meta_table = $meta_table; 1119 $this->meta_id_column = sanitize_key( $type . '_id' ); 979 1120 980 $ join = array();981 $ where = array();1121 $this->primary_table = $primary_table; 1122 $this->primary_id_column = $primary_id_column; 982 1123 983 $key_only_queries = array(); 984 $queries = array(); 1124 $sql = $this->get_sql_clauses(); 985 1125 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"; 992 1166 } 993 1167 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 } 1001 1232 } 1002 } else {1003 $queries = $this->queries;1004 1233 } 1005 1234 1006 // Specify all the meta_key only queries in one go1007 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'] ); 1009 1238 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'; 1012 1241 } 1013 1242 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 } 1017 1247 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 } 1020 1252 1021 $meta_value = isset( $q['value'] ) ? $q['value'] : null; 1253 return $sql; 1254 } 1022 1255 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; 1027 1275 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 ); 1037 1280 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; 1040 1283 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 } 1045 1289 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 } 1047 1300 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 ) ) { 1050 1313 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'] ) ); 1054 1317 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; 1058 1324 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'] ) ); 1063 1345 } 1064 1346 1347 $meta_type = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' ); 1348 1349 $meta_value = isset( $clause['value'] ) ? $clause['value'] : ''; 1350 1065 1351 if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) { 1066 if ( ! is_array( $meta_value ) ) 1352 if ( ! is_array( $meta_value ) ) { 1067 1353 $meta_value = preg_split( '/[,\s]+/', $meta_value ); 1068 1069 if ( empty( $meta_value ) ) {1070 unset( $join[$i] );1071 continue;1072 1354 } 1073 1355 } else { 1074 1356 $meta_value = trim( $meta_value ); 1075 1357 } 1076 1358 1077 if ( 'IN' == substr( $meta_compare, -2 ) ) {1359 if ( 'IN' == substr( $meta_compare, -2 ) ) { 1078 1360 $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 ) ) { 1080 1362 $meta_value = array_slice( $meta_value, 0, 2 ); 1081 1363 $meta_compare_string = '%s AND %s'; 1082 1364 } elseif ( 'LIKE' == $meta_compare || 'NOT LIKE' == $meta_compare ) { … … 1086 1368 $meta_compare_string = '%s'; 1087 1369 } 1088 1370 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 } 1091 1373 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'] ) . ' )' ); 1093 1380 } 1094 1381 1095 $ where = array_filter( $where );1382 $this->table_aliases[] = $alias; 1096 1383 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; 1123 1385 } 1124 1386 } 1125 1387 -
tests/phpunit/tests/meta/query.php
40 40 array(), 41 41 ) ); 42 42 43 $this->assertSame( array( array()), $query->queries );43 $this->assertSame( array(), $query->queries ); 44 44 } 45 45 46 46 /** … … 153 153 $query = new WP_Meta_Query(); 154 154 155 155 // 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 ); 157 166 158 $this->assertEquals( array( array( 'key' => 'abc' ) ), $query->queries );159 160 167 // 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 ); 162 180 163 $this->assertEquals( array( array( 'key' => 'abc', 'value' => 'def' ) ), $query->queries );164 165 181 // 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 ); 169 194 } 170 195 171 196 /** … … 202 227 $this->assertEquals( 'CHAR', $query->get_cast_for_type( 'ANYTHING ELSE' ) ); 203 228 } 204 229 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 205 453 /** 206 454 * Invalid $type will fail to get a table from _get_meta_table() 207 455 */ … … 229 477 $sql = $query->get_sql( 'post', $wpdb->posts, 'ID', $this ); 230 478 231 479 // We should have 2 joins - one for my_first_key and one for my_second_key 232 $this->assertEquals( 2, substr_count( $sql['join'], ' INNERJOIN' ) );480 $this->assertEquals( 2, substr_count( $sql['join'], 'JOIN' ) ); 233 481 234 482 // The WHERE should check my_third_key against an unaliased table 235 483 $this->assertEquals( 1, substr_count( $sql['where'], "$wpdb->postmeta.meta_key = 'my_third_key'" ) ); … … 247 495 ) ); 248 496 $sql = $query->get_sql( 'post', $wpdb->posts, 'ID', $this ); 249 497 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) = ''" ) ); 251 499 } 252 500 253 501 /** … … 558 806 ) ); 559 807 560 808 $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'] ); 562 812 $this->assertNotContains( "{$wpdb->postmeta}.post_id IS NULL", $sql['where'] ); 563 813 } 564 814 } -
tests/phpunit/tests/post/query.php
1408 1408 $this->assertEquals( array( $p1, $p2 ), $q->posts ); 1409 1409 } 1410 1410 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 1411 1494 /** 1412 1495 * @ticket 20604 1413 1496 * @group taxonomy