+/**
+ * Given a taxonomy query, generates SQL to be appended to a main query.
+ *
+ * @since 3.1.0
+ *
+ * @see WP_Tax_Query
+ *
+ * @param array $tax_query A compact tax query
+ * @param string $primary_table
+ * @param string $primary_id_column
+ * @return array
+ */
+function get_tax_sql( $tax_query, $primary_table, $primary_id_column ) {
+ $tax_query_obj = new WP_Tax_Query( $tax_query );
+ return $tax_query_obj->get_sql( $primary_table, $primary_id_column );
+}
+
+/**
+ * Container class for a multiple taxonomy query.
+ *
+ * @since 3.1.0
+ */
+class WP_Tax_Query {
+
+ /**
+ * List of taxonomy queries. A single taxonomy query is an associative array:
+ * - 'taxonomy' string The taxonomy being queried
+ * - 'terms' string|array The list of terms
+ * - 'field' string (optional) Which term field is being used.
+ * Possible values: 'term_id', 'slug' or 'name'
+ * Default: 'term_id'
+ * - 'operator' string (optional)
+ * Possible values: 'AND', 'IN' or 'NOT IN'.
+ * Default: 'IN'
+ * - 'include_children' bool (optional) Whether to include child terms.
+ * Default: true
+ *
+ * @since 3.1.0
+ * @access public
+ * @var array
+ */
+ public $queries = array();
+
+ /**
+ * The relation between the queries. Can be one of 'AND' or 'OR'.
+ *
+ * @since 3.1.0
+ * @access public
+ * @var string
+ */
+ public $relation;
+
+ /**
+ * Standard response when the query should not return any rows.
+ *
+ * @since 3.2.0
+ * @access private
+ * @var string
+ */
+ private static $no_results = array( 'join' => '', 'where' => ' AND 0 = 1' );
+
+ /**
+ * Constructor.
+ *
+ * Parses a compact tax query and sets defaults.
+ *
+ * @since 3.1.0
+ * @access public
+ *
+ * @param array $tax_query A compact tax query:
+ * array(
+ * 'relation' => 'OR',
+ * array(
+ * 'taxonomy' => 'tax1',
+ * 'terms' => array( 'term1', 'term2' ),
+ * 'field' => 'slug',
+ * ),
+ * array(
+ * 'taxonomy' => 'tax2',
+ * 'terms' => array( 'term-a', 'term-b' ),
+ * 'field' => 'slug',
+ * ),
+ * )
+ */
+ public function __construct( $tax_query ) {
+ if ( isset( $tax_query['relation'] ) && strtoupper( $tax_query['relation'] ) == 'OR' ) {
+ $this->relation = 'OR';
+ } else {
+ $this->relation = 'AND';
+ }
+
+ $defaults = array(
+ 'taxonomy' => '',
+ 'terms' => array(),
+ 'include_children' => true,
+ 'field' => 'term_id',
+ 'operator' => 'IN',
+ );
+
+ foreach ( $tax_query as $query ) {
+ if ( ! is_array( $query ) )
+ continue;
+
+ $query = array_merge( $defaults, $query );
+
+ $query['terms'] = (array) $query['terms'];
+
+ $this->queries[] = $query;
+ }
+ }
+
+ /**
+ * Generates SQL clauses to be appended to a main query.
+ *
+ * @since 3.1.0
+ * @access public
+ *
+ * @param string $primary_table
+ * @param string $primary_id_column
+ * @return array
+ */
+ public function get_sql( $primary_table, $primary_id_column ) {
+ global $wpdb;
+
+ $join = '';
+ $where = array();
+ $i = 0;
+ $count = count( $this->queries );
+
+ foreach ( $this->queries as $index => $query ) {
+ $this->clean_query( $query );
+
+ if ( is_wp_error( $query ) )
+ return self::$no_results;
+
+ extract( $query );
+
+ if ( 'IN' == $operator ) {
+
+ if ( empty( $terms ) ) {
+ if ( 'OR' == $this->relation ) {
+ if ( ( $index + 1 === $count ) && empty( $where ) )
+ return self::$no_results;
+ continue;
+ } else {
+ return self::$no_results;
+ }
+ }
+
+ $terms = implode( ',', $terms );
+
+ $alias = $i ? 'tt' . $i : $wpdb->term_relationships;
+
+ $join .= " INNER JOIN $wpdb->term_relationships";
+ $join .= $i ? " AS $alias" : '';
+ $join .= " ON ($primary_table.$primary_id_column = $alias.object_id)";
+
+ $where[] = "$alias.term_taxonomy_id $operator ($terms)";
+ } elseif ( 'NOT IN' == $operator ) {
+
+ if ( empty( $terms ) )
+ continue;
+
+ $terms = implode( ',', $terms );
+
+ $where[] = "$primary_table.$primary_id_column NOT IN (
+ SELECT object_id
+ FROM $wpdb->term_relationships
+ WHERE term_taxonomy_id IN ($terms)
+ )";
+ } elseif ( 'AND' == $operator ) {
+
+ if ( empty( $terms ) )
+ continue;
+
+ $num_terms = count( $terms );
+
+ $terms = implode( ',', $terms );
+
+ $where[] = "(
+ SELECT COUNT(1)
+ FROM $wpdb->term_relationships
+ WHERE term_taxonomy_id IN ($terms)
+ AND object_id = $primary_table.$primary_id_column
+ ) = $num_terms";
+ }
+
+ $i++;
+ }
+
+ if ( ! empty( $where ) )
+ $where = ' AND ( ' . implode( " $this->relation ", $where ) . ' )';
+ else
+ $where = '';
+
+ return compact( 'join', 'where' );
+ }
+
+ /**
+ * Validates a single query.
+ *
+ * @since 3.2.0
+ * @access private
+ *
+ * @param array &$query The single query
+ */
+ private function clean_query( &$query ) {
+ if ( ! taxonomy_exists( $query['taxonomy'] ) ) {
+ $query = new WP_Error( 'Invalid taxonomy' );
+ return;
+ }
+
+ $query['terms'] = array_unique( (array) $query['terms'] );
+
+ if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) {
+ $this->transform_query( $query, 'term_id' );
+
+ if ( is_wp_error( $query ) )
+ return;
+
+ $children = array();
+ foreach ( $query['terms'] as $term ) {
+ $children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) );
+ $children[] = $term;
+ }
+ $query['terms'] = $children;
+ }
+
+ $this->transform_query( $query, 'term_taxonomy_id' );
+ }
+
+ /**
+ * Transforms a single query, from one field to another.
+ *
+ * @since 3.2.0
+ *
+ * @param array &$query The single query
+ * @param string $resulting_field The resulting field
+ */
+ public function transform_query( &$query, $resulting_field ) {
+ global $wpdb;
+
+ if ( empty( $query['terms'] ) )
+ return;
+
+ if ( $query['field'] == $resulting_field )
+ return;
+
+ $resulting_field = esc_sql( $resulting_field );
+
+ switch ( $query['field'] ) {
+ case 'slug':
+ case 'name':
+ $terms = "'" . implode( "','", array_map( 'sanitize_title_for_query', $query['terms'] ) ) . "'";
+ $terms = $wpdb->get_col( "
+ SELECT $wpdb->term_taxonomy.$resulting_field
+ FROM $wpdb->term_taxonomy
+ INNER JOIN $wpdb->terms USING (term_id)
+ WHERE taxonomy = '{$query['taxonomy']}'
+ AND $wpdb->terms.{$query['field']} IN ($terms)
+ " );
+ break;
+ case 'term_taxonomy_id':
+ $terms = implode( ',', array_map( 'intval', $query['terms'] ) );
+ $terms = $wpdb->get_col( "
+ SELECT $resulting_field
+ FROM $wpdb->term_taxonomy
+ WHERE term_taxonomy_id IN ($terms)
+ " );
+ break;
+ default:
+ $terms = implode( ',', array_map( 'intval', $query['terms'] ) );
+ $terms = $wpdb->get_col( "
+ SELECT $resulting_field
+ FROM $wpdb->term_taxonomy
+ WHERE taxonomy = '{$query['taxonomy']}'
+ AND term_id IN ($terms)
+ " );
+ }
+
+ if ( 'AND' == $query['operator'] && count( $terms ) < count( $query['terms'] ) ) {
+ $query = new WP_Error( 'Inexistent terms' );
+ return;
+ }
+
+ $query['terms'] = $terms;
+ $query['field'] = $resulting_field;
+ }
+}
+