WordPress.org

Make WordPress Core

Changeset 29887


Ignore:
Timestamp:
10/13/2014 10:02:18 PM (5 years ago)
Author:
boonebgorges
Message:

Introduce support for nested queries in WP_Meta_Query.

Previously, meta query arguments could be joined by a single AND or OR relation.
Now, these queries can be arbitrarily nested, allowing clauses to be linked
together with multiple relations.

Adds unit tests for the new nesting syntax. Modifies a few existing unit tests
that were overly specific for the old SQL syntax. Backward compatibility with
existing syntax is fully maintained.

Props boonebgorges, DrewAPicture.
See #29642.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/meta.php

    r29650 r29887  
    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 {@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.
    853857 *
    854858 * @since 3.2.0
     
    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
     
    883880
    884881    /**
    885      * Constructor
    886      *
    887      * @param array $meta_query (optional) A meta query
     882     * Database table to query for the metadata.
     883     *
     884     * @since 4.1.0
     885     * @access public
     886     * @var string
     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     * }
    888952     */
    889953    public function __construct( $meta_query = false ) {
     
    897961        }
    898962
    899         $this->queries = array();
    900 
    901         foreach ( $meta_query as $key => $query ) {
    902             if ( ! is_array( $query ) )
    903                 continue;
    904 
    905             $this->queries[] = $query;
    906         }
     963        $this->queries = $this->sanitize_query( $meta_query );
     964    }
     965
     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();
     979
     980        if ( ! is_array( $queries ) ) {
     981            return $clean_queries;
     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, so we recurse.
     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;
     1024    }
     1025
     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'] );
    9071040    }
    9081041
     
    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" ] ) )
     
    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' ];
     
    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
    941      *
    942      * @param string $type MySQL type to cast meta_value
    943      * @return string MySQL type
     1074     * @access public
     1075     *
     1076     * @param string $type MySQL type to cast meta_value.
     1077     * @return string MySQL type.
    9441078     */
    9451079    public function get_cast_for_type( $type = '' ) {
     
    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;
    977 
    978         $meta_id_column = sanitize_key( $type . '_id' );
    979 
    980         $join = array();
    981         $where = array();
    982 
    983         $key_only_queries = array();
    984         $queries = array();
    985 
    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             }
    992         }
    993 
    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;
    1001             }
    1002         } else {
    1003             $queries = $this->queries;
    1004         }
    1005 
    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";
    1009 
    1010             foreach ( $key_only_queries as $key => $q )
    1011                 $where["key-only-$key"] = $wpdb->prepare( "$meta_table.meta_key = %s", trim( $q['key'] ) );
    1012         }
    1013 
    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'] : '' );
    1017 
    1018             if ( array_key_exists( 'value', $q ) && is_null( $q['value'] ) )
    1019                 $q['value'] = '';
    1020 
    1021             $meta_value = isset( $q['value'] ) ? $q['value'] : null;
    1022 
    1023             if ( isset( $q['compare'] ) )
    1024                 $meta_compare = strtoupper( $q['compare'] );
    1025             else
    1026                 $meta_compare = is_array( $meta_value ) ? 'IN' : '=';
    1027 
    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 = '=';
    1037 
    1038             $i = count( $join );
    1039             $alias = $i ? 'mt' . $i : $meta_table;
    1040 
    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')";
    1045 
    1046                 $where[$k] = ' ' . $alias . '.' . $meta_id_column . ' IS NULL';
    1047 
    1048                 continue;
    1049             }
    1050 
    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 );
    1058 
    1059             if ( is_null( $meta_value ) ) {
    1060                 if ( empty( $where[$k] ) )
    1061                     unset( $join[$i] );
    1062                 continue;
    1063             }
    1064 
    1065             if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
    1066                 if ( ! is_array( $meta_value ) )
    1067                     $meta_value = preg_split( '/[,\s]+/', $meta_value );
    1068 
    1069                 if ( empty( $meta_value ) ) {
    1070                     unset( $join[$i] );
    1071                     continue;
    1072                 }
    1073             } else {
    1074                 $meta_value = trim( $meta_value );
    1075             }
    1076 
    1077             if ( 'IN' == substr( $meta_compare, -2) ) {
    1078                 $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
    1079             } elseif ( 'BETWEEN' == substr( $meta_compare, -7) ) {
    1080                 $meta_value = array_slice( $meta_value, 0, 2 );
    1081                 $meta_compare_string = '%s AND %s';
    1082             } elseif ( 'LIKE' == $meta_compare || 'NOT LIKE' == $meta_compare ) {
    1083                 $meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%';
    1084                 $meta_compare_string = '%s';
    1085             } else {
    1086                 $meta_compare_string = '%s';
    1087             }
    1088 
    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 );
    1093         }
    1094 
    1095         $where = array_filter( $where );
    1096 
    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;
     1116        }
     1117
     1118        $this->meta_table     = $meta_table;
     1119        $this->meta_id_column = sanitize_key( $type . '_id' );
     1120
     1121        $this->primary_table     = $primary_table;
     1122        $this->primary_id_column = $primary_id_column;
     1123
     1124        $sql = $this->get_sql_clauses();
    11051125
    11061126        /**
     
    11101130         *
    11111131         * @param array $args {
    1112          *     An array of arguments.
     1132         *     An array of meta query SQL arguments.
    11131133         *
    11141134         *     @type array  $clauses           Array containing the query's JOIN and WHERE clauses.
     
    11201140         * }
    11211141         */
    1122         return apply_filters_ref_array( 'get_meta_sql', array( compact( 'join', 'where' ), $this->queries, $type, $primary_table, $primary_id_column, $context ) );
     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 ' . $sql['where'];
     1166        }
     1167
     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 .= "  ";
     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                }
     1232            }
     1233        }
     1234
     1235        // Filter to remove empties.
     1236        $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
     1237        $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
     1238
     1239        if ( empty( $relation ) ) {
     1240            $relation = 'AND';
     1241        }
     1242
     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        }
     1247
     1248        // Generate a single WHERE clause with proper brackets and indentation.
     1249        if ( ! empty( $sql_chunks['where'] ) ) {
     1250            $sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
     1251        }
     1252
     1253        return $sql;
     1254    }
     1255
     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;
     1275
     1276        $sql_chunks = array(
     1277            'where' => array(),
     1278            'join' => array(),
     1279        );
     1280
     1281        $i = count( $this->table_aliases );
     1282        $alias = $i ? 'mt' . $i : $this->meta_table;
     1283
     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        }
     1289
     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        }
     1300
     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        ) ) {
     1313
     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'] ) );
     1317
     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;
     1324
     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'] ) );
     1345            }
     1346
     1347            $meta_type = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' );
     1348
     1349            $meta_value = isset( $clause['value'] ) ? $clause['value'] : '';
     1350
     1351            if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
     1352                if ( ! is_array( $meta_value ) ) {
     1353                    $meta_value = preg_split( '/[,\s]+/', $meta_value );
     1354                }
     1355            } else {
     1356                $meta_value = trim( $meta_value );
     1357            }
     1358
     1359            if ( 'IN' == substr( $meta_compare, -2 ) ) {
     1360                $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
     1361            } elseif ( 'BETWEEN' == substr( $meta_compare, -7 ) ) {
     1362                $meta_value = array_slice( $meta_value, 0, 2 );
     1363                $meta_compare_string = '%s AND %s';
     1364            } elseif ( 'LIKE' == $meta_compare || 'NOT LIKE' == $meta_compare ) {
     1365                $meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%';
     1366                $meta_compare_string = '%s';
     1367            } else {
     1368                $meta_compare_string = '%s';
     1369            }
     1370
     1371            $sql_chunks['where'][] = $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string}", $meta_value );
     1372        }
     1373
     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'] ) . ' )' );
     1380        }
     1381
     1382        $this->table_aliases[] = $alias;
     1383
     1384        return $sql_chunks;
    11231385    }
    11241386}
  • trunk/tests/phpunit/tests/meta/query.php

    r29799 r29887  
    4141        ) );
    4242
    43         $this->assertSame( array( array() ), $query->queries );
     43        $this->assertSame( array(), $query->queries );
    4444    }
    4545
     
    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
     
    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()
     
    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
     
    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
     
    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    }
  • trunk/tests/phpunit/tests/post/query.php

    r29857 r29887  
    773773    }
    774774
     775    /**
     776     * @ticket 29642
     777     * @group meta
     778     */
     779    public function test_meta_query_nested() {
     780        $p1 = $this->factory->post->create();
     781        $p2 = $this->factory->post->create();
     782        $p3 = $this->factory->post->create();
     783
     784        add_post_meta( $p1, 'foo', 'bar' );
     785        add_post_meta( $p2, 'foo2', 'bar' );
     786        add_post_meta( $p3, 'foo2', 'bar' );
     787        add_post_meta( $p3, 'foo3', 'bar' );
     788
     789        $query = new WP_Query( array(
     790            'update_post_meta_cache' => false,
     791            'update_term_meta_cache' => false,
     792            'fields' => 'ids',
     793            'meta_query' => array(
     794                'relation' => 'OR',
     795                array(
     796                    'key' => 'foo',
     797                    'value' => 'bar',
     798                ),
     799                array(
     800                    'relation' => 'AND',
     801                    array(
     802                        'key' => 'foo2',
     803                        'value' => 'bar',
     804                    ),
     805                    array(
     806                        'key' => 'foo3',
     807                        'value' => 'bar',
     808                    ),
     809                ),
     810            ),
     811        ) );
     812
     813        $expected = array( $p1, $p3 );
     814        $this->assertEqualSets( $expected, $query->posts );
     815    }
     816
     817    /**
     818     * @ticket 29642
     819     * @group meta
     820     */
     821    public function test_meta_query_nested_two_levels_deep() {
     822        $p1 = $this->factory->post->create();
     823        $p2 = $this->factory->post->create();
     824        $p3 = $this->factory->post->create();
     825
     826        add_post_meta( $p1, 'foo', 'bar' );
     827        add_post_meta( $p3, 'foo2', 'bar' );
     828        add_post_meta( $p3, 'foo3', 'bar' );
     829        add_post_meta( $p3, 'foo4', 'bar' );
     830
     831        $query = new WP_Query( array(
     832            'update_post_meta_cache' => false,
     833            'update_term_meta_cache' => false,
     834            'fields' => 'ids',
     835            'meta_query' => array(
     836                'relation' => 'OR',
     837                array(
     838                    'key' => 'foo',
     839                    'value' => 'bar',
     840                ),
     841                array(
     842                    'relation' => 'OR',
     843                    array(
     844                        'key' => 'foo2',
     845                        'value' => 'bar',
     846                    ),
     847                    array(
     848                        'relation' => 'AND',
     849                        array(
     850                            'key' => 'foo3',
     851                            'value' => 'bar',
     852                        ),
     853                        array(
     854                            'key' => 'foo4',
     855                            'value' => 'bar',
     856                        ),
     857                    ),
     858                ),
     859            ),
     860        ) );
     861
     862        $expected = array( $p1, $p3 );
     863        $this->assertEqualSets( $expected, $query->posts );
     864    }
     865
     866    /**
     867     * @group meta
     868     */
    775869    function test_meta_between_not_between() {
    776870        $post_id = $this->factory->post->create();
     
    820914    /**
    821915     * @ticket 16829
     916     * @group meta
    822917     */
    823918    function test_meta_default_compare() {
     
    860955    /**
    861956     * @ticket 17264
     957     * @group meta
    862958     */
    863959    function test_duplicate_posts_when_no_key() {
     
    891987    /**
    892988     * @ticket 15292
     989     * @group meta
    893990     */
    894991    function test_empty_meta_value() {
Note: See TracChangeset for help on using the changeset viewer.