From 2796b6969f2f46d8c9691f7e3ab2a7919a206510 Mon Sep 17 00:00:00 2001 From: Boone Gorges Date: Wed, 15 Oct 2014 00:53:22 +0000 Subject: [PATCH] Support 'EXISTS' and 'NOT EXISTS' in `WP_Tax_Query`. These new values for the 'operator' parameter make it possible to filter items that have no term from a given taxonomy, or any term from a given taxonomy. Includes unit tests. Fixes #29181. git-svn-id: https://develop.svn.wordpress.org/trunk@29896 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/taxonomy.php | 18 +++- tests/phpunit/tests/post/query.php | 128 +++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 713898b307..91f47f17c7 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -691,10 +691,11 @@ class WP_Tax_Query { * Constructor. * * @since 3.1.0 + * @since 4.1.0 Added support for $operator 'NOT EXISTS' and 'EXISTS'. * @access public * * @param array $tax_query { - * Array of taxonoy query clauses. + * Array of taxonomy query clauses. * * @type string $relation Optional. The MySQL keyword used to join * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'. @@ -706,7 +707,8 @@ class WP_Tax_Query { * @type string $field Field to match $terms against. Accepts 'term_id', 'slug', * 'name', or 'term_taxonomy_id'. Default: 'term_id'. * @type string $operator MySQL operator to be used with $terms in the WHERE clause. - * Accepts 'AND', 'IN', or 'OR. Default: 'IN'. + * Accepts 'AND', 'IN', 'NOT IN', 'EXISTS', 'NOT EXISTS'. + * Default: 'IN'. * @type bool $include_children Optional. Whether to include child terms. * Requires a $taxonomy. Default: true. * } @@ -1026,6 +1028,18 @@ class WP_Tax_Query { WHERE term_taxonomy_id IN ($terms) AND object_id = $this->primary_table.$this->primary_id_column ) = $num_terms"; + + } elseif ( 'NOT EXISTS' === $operator || 'EXISTS' === $operator ) { + + $where = $wpdb->prepare( "$operator ( + SELECT 1 + FROM $wpdb->term_relationships + INNER JOIN $wpdb->term_taxonomy + ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id + WHERE $wpdb->term_taxonomy.taxonomy = %s + AND $wpdb->term_relationships.object_id = $this->primary_table.$this->primary_id_column + )", $clause['taxonomy'] ); + } $sql['join'][] = $join; diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index e3a6f1f33b..e9a081bcd2 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -1412,6 +1412,134 @@ class Tests_Post_Query extends WP_UnitTestCase { $this->assertEquals( array( $p2 ), $q->posts ); } + /** + * @ticket 29181 + */ + public function test_tax_query_operator_not_exists() { + register_taxonomy( 'wptests_tax1', 'post' ); + register_taxonomy( 'wptests_tax2', 'post' ); + + $t1 = $this->factory->term->create( array( 'taxonomy' => 'wptests_tax1' ) ); + $t2 = $this->factory->term->create( array( 'taxonomy' => 'wptests_tax2' ) ); + + $p1 = $this->factory->post->create(); + $p2 = $this->factory->post->create(); + $p3 = $this->factory->post->create(); + + wp_set_object_terms( $p1, array( $t1 ), 'wptests_tax1' ); + wp_set_object_terms( $p2, array( $t2 ), 'wptests_tax2' ); + + $q = new WP_Query( array( + 'fields' => 'ids', + 'orderby' => 'ID', + 'order' => 'ASC', + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_tax2', + 'operator' => 'NOT EXISTS', + ), + ), + ) ); + + $this->assertEqualSets( array( $p1, $p3 ), $q->posts ); + } + + /** + * @ticket 29181 + */ + public function test_tax_query_operator_exists() { + register_taxonomy( 'wptests_tax1', 'post' ); + register_taxonomy( 'wptests_tax2', 'post' ); + + $t1 = $this->factory->term->create( array( 'taxonomy' => 'wptests_tax1' ) ); + $t2 = $this->factory->term->create( array( 'taxonomy' => 'wptests_tax2' ) ); + + $p1 = $this->factory->post->create(); + $p2 = $this->factory->post->create(); + $p3 = $this->factory->post->create(); + + wp_set_object_terms( $p1, array( $t1 ), 'wptests_tax1' ); + wp_set_object_terms( $p2, array( $t2 ), 'wptests_tax2' ); + + $q = new WP_Query( array( + 'fields' => 'ids', + 'orderby' => 'ID', + 'order' => 'ASC', + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_tax2', + 'operator' => 'EXISTS', + ), + ), + ) ); + + $this->assertEqualSets( array( $p2 ), $q->posts ); + } + + /** + * @ticket 29181 + */ + public function test_tax_query_operator_exists_should_ignore_terms() { + register_taxonomy( 'wptests_tax1', 'post' ); + register_taxonomy( 'wptests_tax2', 'post' ); + + $t1 = $this->factory->term->create( array( 'taxonomy' => 'wptests_tax1' ) ); + $t2 = $this->factory->term->create( array( 'taxonomy' => 'wptests_tax2' ) ); + + $p1 = $this->factory->post->create(); + $p2 = $this->factory->post->create(); + $p3 = $this->factory->post->create(); + + wp_set_object_terms( $p1, array( $t1 ), 'wptests_tax1' ); + wp_set_object_terms( $p2, array( $t2 ), 'wptests_tax2' ); + + $q = new WP_Query( array( + 'fields' => 'ids', + 'orderby' => 'ID', + 'order' => 'ASC', + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_tax2', + 'operator' => 'EXISTS', + 'terms' => array( 'foo', 'bar' ), + ), + ), + ) ); + + $this->assertEqualSets( array( $p2 ), $q->posts ); + } + + /** + * @ticket 29181 + */ + public function test_tax_query_operator_exists_with_no_taxonomy() { + register_taxonomy( 'wptests_tax1', 'post' ); + register_taxonomy( 'wptests_tax2', 'post' ); + + $t1 = $this->factory->term->create( array( 'taxonomy' => 'wptests_tax1' ) ); + $t2 = $this->factory->term->create( array( 'taxonomy' => 'wptests_tax2' ) ); + + $p1 = $this->factory->post->create(); + $p2 = $this->factory->post->create(); + $p3 = $this->factory->post->create(); + + wp_set_object_terms( $p1, array( $t1 ), 'wptests_tax1' ); + wp_set_object_terms( $p2, array( $t2 ), 'wptests_tax2' ); + + $q = new WP_Query( array( + 'fields' => 'ids', + 'orderby' => 'ID', + 'order' => 'ASC', + 'tax_query' => array( + array( + 'operator' => 'EXISTS', + ), + ), + ) ); + + $this->assertEmpty( $q->posts ); + } + /** * @group taxonomy */