Commit d18ed8d5a90 for woocommerce

commit d18ed8d5a90e451cd14e909d9e29764adaf5df40
Author: Tung Du <dinhtungdu@gmail.com>
Date:   Thu Oct 1 10:45:19 2026 +0700

    Refactor Product Collection QueryBuilder filter queries to use ProductFilters\QueryClauses (#68935)

    * refactor: share Product Collection filter query clauses

    * chore: add Product Collection filter changelog

    * refactor: move rating filtering to shared clauses

    * Fix duplicate Product Collection filter clauses

    * fix: restore Product Collection rating tax query

    * docs: explain Product Collection attribute count scope

    * refactor: preserve Product Filter count behavior

    * refactor: inline Product Collection rating query

    * refactor: group Product Collection filters

    * refactor: preserve Product Collection rating behavior

    * refactor: split attribute clause strategies

    * fix: preserve Product Filter attribute behavior

    * fix: preserve Product Filter taxonomy behavior

    * fix: remove obsolete Product Filters PHPStan baseline entry

diff --git a/plugins/woocommerce/changelog/fix-product-collection-query-clauses b/plugins/woocommerce/changelog/fix-product-collection-query-clauses
new file mode 100644
index 00000000000..235581b399c
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-collection-query-clauses
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Migrate Product Collection filtering to the shared Product Filters QueryClauses implementation.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index e2d39619fec..9a3e882d3c8 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -62448,12 +62448,6 @@ parameters:
 			count: 1
 			path: src/Internal/ProductFilters/QueryClauses.php

-		-
-			message: '#^Variable \$chosen_attributes in empty\(\) always exists and is not falsy\.$#'
-			identifier: empty.variable
-			count: 1
-			path: src/Internal/ProductFilters/QueryClauses.php
-
 		-
 			message: '#^Variable \$stock_statuses in empty\(\) always exists and is not falsy\.$#'
 			identifier: empty.variable
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index 03240cc519a..f5cd17a0ec8 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -4,9 +4,9 @@ declare(strict_types=1);
 namespace Automattic\WooCommerce\Blocks\BlockTypes\ProductCollection;

 use Automattic\WooCommerce\Blocks\BlockTypes\AttributeFilter;
-use Automattic\WooCommerce\Blocks\BlockTypes\PriceFilter;
 use Automattic\WooCommerce\Blocks\BlockTypes\RatingFilter;
-use Automattic\WooCommerce\Blocks\BlockTypes\StockFilter;
+use Automattic\WooCommerce\Internal\ProductFilters\Params;
+use Automattic\WooCommerce\Internal\ProductFilters\QueryClauses;
 use WP_Query;
 use WC_Tax;
 use Automattic\WooCommerce\Enums\CatalogSortOrder;
@@ -54,6 +54,13 @@ class QueryBuilder {
 	public function __construct() {
 		$this->valid_query_vars = $this->get_valid_query_vars();
 		add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
+		static $product_filter_query_clauses_callback;
+		if ( null === $product_filter_query_clauses_callback ) {
+			$product_filter_query_clauses_callback = static function ( $clauses, $query ) {
+				return self::apply_product_filter_query_clauses( $clauses, $query );
+			};
+		}
+		add_filter( 'posts_clauses', $product_filter_query_clauses_callback, 10, 2 );
 	}

 	/**
@@ -276,9 +283,6 @@ class QueryBuilder {
 		$price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
 		$handpicked_query = $this->get_handpicked_query( $query['handpicked_products'] ?? false );

-		// We exclude applied filters to generate product ids for the filter blocks.
-		$applied_filters_query = $is_exclude_applied_filters ? array() : $this->get_queries_by_applied_filters();
-
 		// Allow collections to provide their own query parameters.
 		$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
 		if ( isset( $handlers['build_query'] ) ) {
@@ -293,18 +297,27 @@ class QueryBuilder {
 			$collection_query = array();
 		}

-		return $this->merge_queries(
+		$final_query = $this->merge_queries(
 			$common_query_values,
 			$orderby_query,
 			$on_sale_query,
 			$stock_query,
 			$tax_query,
-			$applied_filters_query,
 			$date_query,
 			$price_query_args,
 			$handpicked_query,
 			$collection_query
 		);
+
+		$final_query['isProductCollection'] = true;
+
+		if ( ! $is_exclude_applied_filters ) {
+			// Rating must be a tax query; the remaining filters are query vars consumed by QueryClauses.
+			$final_query = $this->merge_queries( $final_query, $this->get_filter_by_rating_query() );
+			$final_query = array_merge( $final_query, $this->get_applied_filter_query_vars() );
+		}
+
+		return $final_query;
 	}

 	/**
@@ -459,96 +472,6 @@ class QueryBuilder {
 		);
 	}

-	/**
-	 * Return a query that filters products by price.
-	 *
-	 * @return array
-	 */
-	private function get_filter_by_price_query() {
-		$min_price = get_query_var( PriceFilter::MIN_PRICE_QUERY_VAR );
-		$max_price = get_query_var( PriceFilter::MAX_PRICE_QUERY_VAR );
-
-		$max_price_query = empty( $max_price ) ? array() : array(
-			'key'     => '_price',
-			'value'   => $max_price,
-			'compare' => '<=',
-			'type'    => 'numeric',
-		);
-
-		$min_price_query = empty( $min_price ) ? array() : array(
-			'key'     => '_price',
-			'value'   => $min_price,
-			'compare' => '>=',
-			'type'    => 'numeric',
-		);
-
-		if ( empty( $min_price_query ) && empty( $max_price_query ) ) {
-			return array();
-		}
-
-		return array(
-			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
-			'meta_query' => array(
-				array(
-					'relation' => 'AND',
-					$max_price_query,
-					$min_price_query,
-				),
-			),
-		);
-	}
-
-	/**
-	 * Return a query that filters products by attributes.
-	 *
-	 * @return array
-	 */
-	private function get_filter_by_attributes_query() {
-		$attributes_filter_query_args = $this->get_filter_by_attributes_query_vars();
-
-		$queries = array_reduce(
-			$attributes_filter_query_args,
-			function ( $acc, $query_args ) {
-				$attribute_name       = $query_args['filter'];
-				$attribute_query_type = $query_args['query_type'];
-
-				$attribute_value = get_query_var( $attribute_name );
-				$attribute_query = get_query_var( $attribute_query_type );
-
-				if ( empty( $attribute_value ) ) {
-					return $acc;
-				}
-
-				// It is necessary explode the value because $attribute_value can be a string with multiple values (e.g. "red,blue").
-				$attribute_value = explode( ',', $attribute_value );
-
-				$acc[] = array(
-					'taxonomy' => str_replace( AttributeFilter::FILTER_QUERY_VAR_PREFIX, 'pa_', $attribute_name ),
-					'field'    => 'slug',
-					'terms'    => $attribute_value,
-					'operator' => 'and' === $attribute_query ? 'AND' : 'IN',
-				);
-
-				return $acc;
-			},
-			array()
-		);
-
-		if ( empty( $queries ) ) {
-			return array();
-		}
-
-		return array(
-			// phpcs:ignore WordPress.DB.SlowDBQuery
-			'tax_query' => array(
-				array(
-					'relation' => 'AND',
-					$queries,
-				),
-			),
-		);
-	}
-
 	/**
 	 * Get all the query args related to the filter by attributes block.
 	 *
@@ -586,42 +509,6 @@ class QueryBuilder {
 		return $this->attributes_filter_query_args;
 	}

-	/**
-	 * Return a query that filters products by stock status.
-	 *
-	 * @return array
-	 */
-	private function get_filter_by_stock_status_query() {
-		$filter_stock_status_values = get_query_var( StockFilter::STOCK_STATUS_QUERY_VAR );
-
-		if ( empty( $filter_stock_status_values ) ) {
-			return array();
-		}
-
-		$filtered_stock_status_values = array_filter(
-			explode( ',', $filter_stock_status_values ),
-			function ( $stock_status ) {
-				return in_array( $stock_status, StockFilter::get_stock_status_query_var_values(), true );
-			}
-		);
-
-		if ( empty( $filtered_stock_status_values ) ) {
-			return array();
-		}
-
-		return array(
-			// Ignoring the warning of not using meta queries.
-			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
-			'meta_query' => array(
-				array(
-					'key'      => '_stock_status',
-					'value'    => $filtered_stock_status_values,
-					'operator' => 'IN',
-				),
-			),
-		);
-	}
-
 	/**
 	 * Return a query that filters products by rating.
 	 *
@@ -661,66 +548,6 @@ class QueryBuilder {
 		);
 	}

-	/**
-	 * Return a query that filters products by taxonomy terms.
-	 *
-	 * @since 10.6.0
-	 *
-	 * @return array
-	 */
-	private function get_filter_by_taxonomy_query() {
-
-		$container       = wc_get_container();
-		$params_handler  = $container->get( \Automattic\WooCommerce\Internal\ProductFilters\Params::class );
-		$taxonomy_params = $params_handler->get_param( 'taxonomy' );
-
-		if ( empty( $taxonomy_params ) ) {
-			return array();
-		}
-
-		$tax_queries = array();
-
-		foreach ( $taxonomy_params as $taxonomy_slug => $param_key ) {
-			$param_value = get_query_var( $param_key );
-
-			// Adding is_string check to avoid invalid query parameters for the taxonomy.
-			if ( ! is_string( $param_value ) || empty( $param_value ) ) {
-				continue;
-			}
-
-			// Define $term_values by exploding the string.
-			$term_values = explode( ',', $param_value );
-
-			// Sanitize and filter (removes empty strings).
-			$term_slugs = array_values( array_filter( array_map( 'sanitize_title', $term_values ) ) );
-
-			if ( empty( $term_slugs ) ) {
-				continue;
-			}
-
-			$tax_queries[] = array(
-				'taxonomy' => $taxonomy_slug,
-				'field'    => 'slug',
-				'terms'    => $term_slugs,
-				'operator' => 'IN',
-			);
-		}
-
-		if ( empty( $tax_queries ) ) {
-			return array();
-		}
-
-		return array(
-			// phpcs:ignore WordPress.DB.SlowDBQuery
-			'tax_query' => array(
-				array(
-					'relation' => 'AND',
-					...$tax_queries,
-				),
-			),
-		);
-	}
-
 	/**
 	 * Merge two array recursively but replace the non-array values instead of
 	 * merging them. The merging strategy:
@@ -787,18 +614,52 @@ class QueryBuilder {
 	}

 	/**
-	 * Return queries that are generated by query args.
+	 * Get the active Product Filter URL query variables.
 	 *
 	 * @return array
 	 */
-	private function get_queries_by_applied_filters() {
-		return array(
-			'price_filter'        => $this->get_filter_by_price_query(),
-			'attributes_filter'   => $this->get_filter_by_attributes_query(),
-			'stock_status_filter' => $this->get_filter_by_stock_status_query(),
-			'rating_filter'       => $this->get_filter_by_rating_query(),
-			'taxonomy_filter'     => $this->get_filter_by_taxonomy_query(),
+	private function get_applied_filter_query_vars(): array {
+		$params                          = wc_get_container()->get( Params::class );
+		$filter_query_vars               = array();
+		$registered_attribute_params     = $params->get_param( 'attribute' );
+		$registered_query_type_params    = array_map(
+			function ( $attribute_name ) {
+				return AttributeFilter::QUERY_TYPE_QUERY_VAR_PREFIX . $attribute_name;
+			},
+			array_keys( $registered_attribute_params )
 		);
+		$registered_attribute_param_keys = array_merge( array_values( $registered_attribute_params ), $registered_query_type_params );
+
+		foreach ( $params->get_param_keys() as $param_key ) {
+			if ( RatingFilter::RATING_QUERY_VAR === $param_key || in_array( $param_key, $registered_attribute_param_keys, true ) ) {
+				continue;
+			}
+
+			$value = get_query_var( $param_key );
+			if ( ( is_string( $value ) || is_numeric( $value ) ) && '' !== (string) $value ) {
+				$filter_query_vars[ $param_key ] = (string) $value;
+			}
+		}
+
+		foreach ( $this->get_filter_by_attributes_query_vars() as $attribute_name => $query_args ) {
+			$filter_value = get_query_var( $query_args['filter'] );
+			if ( ( ! is_string( $filter_value ) && ! is_numeric( $filter_value ) ) || '' === (string) $filter_value ) {
+				continue;
+			}
+
+			if ( ! is_string( $attribute_name ) ) {
+				$attribute_name = preg_replace( '/^(filter_|pa_)/', '', $query_args['filter'] );
+			}
+			$attribute_name = wc_sanitize_taxonomy_name( $attribute_name );
+			$query_type     = get_query_var( $query_args['query_type'] );
+
+			$filter_query_vars[ AttributeFilter::FILTER_QUERY_VAR_PREFIX . $attribute_name ]     = (string) $filter_value;
+			$filter_query_vars[ AttributeFilter::QUERY_TYPE_QUERY_VAR_PREFIX . $attribute_name ] = is_string( $query_type ) && in_array( $query_type, array( 'and', 'or' ), true )
+				? $query_type
+				: 'or';
+		}
+
+		return $filter_query_vars;
 	}

 	/**
@@ -931,6 +792,26 @@ class QueryBuilder {
 		return $clauses;
 	}

+	/**
+	 * Apply Product Filter clauses to Product Collection queries.
+	 *
+	 * @param array    $clauses The query clauses.
+	 * @param WP_Query $query   The WP_Query instance.
+	 * @return array
+	 */
+	private static function apply_product_filter_query_clauses( $clauses, $query ) {
+		if ( ! ( $query->query_vars['isProductCollection'] ?? false ) ) {
+			return $clauses;
+		}
+
+		$query_clauses = wc_get_container()->get( QueryClauses::class );
+		if ( false !== has_filter( 'posts_clauses', array( $query_clauses, 'add_query_clauses' ) ) ) {
+			return $clauses;
+		}
+
+		return $query_clauses->add_query_clauses( $clauses, $query );
+	}
+
 	/**
 	 * Get query for price filters when dealing with displayed taxes.
 	 *
diff --git a/plugins/woocommerce/src/Internal/ProductFilters/QueryClauses.php b/plugins/woocommerce/src/Internal/ProductFilters/QueryClauses.php
index d327b4703c9..04164920a78 100644
--- a/plugins/woocommerce/src/Internal/ProductFilters/QueryClauses.php
+++ b/plugins/woocommerce/src/Internal/ProductFilters/QueryClauses.php
@@ -168,7 +168,7 @@ class QueryClauses implements QueryClausesGenerator, MainQueryClausesGenerator {
 		$args['join']     = $this->append_product_sorting_table_join( $args['join'] );

 		if ( isset( $price_range['min_price'] ) ) {
-			$min_price_filter = intval( $price_range['min_price'] );
+			$min_price_filter = (float) wc_format_decimal( $price_range['min_price'] );

 			if ( $adjust_for_taxes ) {
 				$args['where'] .= $this->get_price_filter_query_for_displayed_taxes( $min_price_filter, 'max_price', '>=' );
@@ -178,7 +178,7 @@ class QueryClauses implements QueryClausesGenerator, MainQueryClausesGenerator {
 		}

 		if ( isset( $price_range['max_price'] ) ) {
-			$max_price_filter = intval( $price_range['max_price'] );
+			$max_price_filter = (float) wc_format_decimal( $price_range['max_price'] );

 			if ( $adjust_for_taxes ) {
 				$args['where'] .= $this->get_price_filter_query_for_displayed_taxes( $max_price_filter, 'min_price', '<=' );
@@ -210,6 +210,21 @@ class QueryClauses implements QueryClausesGenerator, MainQueryClausesGenerator {
 			return $args;
 		}

+		if ( 'yes' !== get_option( 'woocommerce_attribute_lookup_enabled' ) ) {
+			return $this->add_attribute_taxonomy_clauses( $args, $chosen_attributes );
+		}
+
+		return $this->add_attribute_lookup_table_clauses( $args, $chosen_attributes );
+	}
+
+	/**
+	 * Add attribute clauses using the product attributes lookup table.
+	 *
+	 * @param array $args              Query args.
+	 * @param array $chosen_attributes Chosen attributes.
+	 * @return array
+	 */
+	private function add_attribute_lookup_table_clauses( array $args, array $chosen_attributes ): array {
 		global $wpdb;

 		// The extra derived table ("SELECT product_or_parent_id FROM") is needed for performance
@@ -293,6 +308,8 @@ class QueryClauses implements QueryClausesGenerator, MainQueryClausesGenerator {
 				WHERE is_variation_attribute=1
 				{$in_stock_clause}
 				AND term_id in {$term_ids_to_filter_by_list}
+				GROUP BY product_or_parent_id
+				HAVING COUNT(DISTINCT term_id)={$count}
 			)";
 		}

@@ -306,6 +323,93 @@ class QueryClauses implements QueryClausesGenerator, MainQueryClausesGenerator {
 		return $args;
 	}

+	/**
+	 * Add attribute clauses using WordPress taxonomy queries.
+	 *
+	 * @param array $args              Query args.
+	 * @param array $chosen_attributes Chosen attributes.
+	 * @return array
+	 */
+	private function add_attribute_taxonomy_clauses( array $args, array $chosen_attributes ): array {
+		global $wpdb;
+
+		$all_term_slugs = array();
+		foreach ( $chosen_attributes as $data ) {
+			if ( ! empty( $data['terms'] ) && is_array( $data['terms'] ) ) {
+				$all_term_slugs = array_merge( $all_term_slugs, $data['terms'] );
+			}
+		}
+
+		if ( empty( $all_term_slugs ) ) {
+			$args['where'] .= ' AND 1=0';
+			return $args;
+		}
+
+		$all_terms = get_terms(
+			array(
+				'taxonomy'   => array_keys( $chosen_attributes ),
+				'slug'       => $all_term_slugs,
+				'hide_empty' => false,
+			)
+		);
+
+		if ( is_wp_error( $all_terms ) ) {
+			$args['where'] .= ' AND 1=0';
+			return $args;
+		}
+
+		$term_ids_by_taxonomy = array();
+		foreach ( $all_terms as $term ) {
+			$term_ids_by_taxonomy[ $term->taxonomy ][ $term->slug ] = (int) $term->term_id;
+		}
+
+		$clauses = array();
+		foreach ( $chosen_attributes as $taxonomy => $data ) {
+			$term_ids     = array_values( array_intersect_key( $term_ids_by_taxonomy[ $taxonomy ] ?? array(), array_flip( $data['terms'] ) ) );
+			$is_and       = 'and' === strtolower( $data['query_type'] );
+			$unique_terms = array_unique( $data['terms'] );
+
+			if ( empty( $term_ids ) || ( $is_and && count( $term_ids ) !== count( $unique_terms ) ) ) {
+				$args['where'] .= ' AND 1=0';
+				return $args;
+			}
+
+			$term_ids_list = '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')';
+
+			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
+			if ( $is_and && count( $term_ids ) > 1 ) {
+				$clauses[] = $wpdb->prepare(
+					"{$wpdb->posts}.ID IN (
+						SELECT tr.object_id FROM {$wpdb->term_relationships} tr
+						INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
+						WHERE tt.taxonomy = %s
+						AND tt.term_id IN {$term_ids_list}
+						GROUP BY tr.object_id
+						HAVING COUNT(DISTINCT tt.term_id) = %d
+					)",
+					$taxonomy,
+					count( $term_ids )
+				);
+			} else {
+				$clauses[] = $wpdb->prepare(
+					"EXISTS (
+						SELECT 1 FROM {$wpdb->term_relationships} tr
+						INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
+						WHERE tr.object_id = {$wpdb->posts}.ID
+						AND tt.taxonomy = %s
+						AND tt.term_id IN {$term_ids_list}
+					)",
+					$taxonomy
+				);
+			}
+			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
+		}
+
+		$args['where'] .= ' AND (' . implode( ' AND ', $clauses ) . ')';
+
+		return $args;
+	}
+
 	/**
 	 * Add query clauses for taxonomy filter (e.g., product_cat, product_tag).
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index 301ab506c2d..642e635be18 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -9,6 +9,7 @@ use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
 use Automattic\WooCommerce\Tests\Blocks\BlockTypes\ProductCollection\Utils;
 use Automattic\WooCommerce\Tests\Blocks\Mocks\ProductCollectionMock;
 use Automattic\WooCommerce\Enums\ProductStockStatus;
+use Automattic\WooCommerce\Internal\ProductFilters\QueryClauses;
 use WC_Helper_Product;
 use WP_Query;

@@ -248,101 +249,59 @@ class QueryBuilder extends \WP_UnitTestCase {
 	}

 	/**
-	 * Test merging filter by max price queries.
+	 * @testdox Should pass the maximum price filter to shared query clauses.
 	 */
-	public function test_merging_filter_by_max_price_queries() {
+	public function test_merging_filter_by_max_price_queries(): void {
 		set_query_var( 'max_price', 100 );

 		$merged_query = Utils::initialize_merged_query( $this->block_instance );

-		$this->assertContainsEquals(
-			array(
-				array(
-					'key'     => '_price',
-					'value'   => 100,
-					'compare' => '<=',
-					'type'    => 'numeric',
-				),
-				array(),
-				'relation' => 'AND',
-			),
-			$merged_query['meta_query']
-		);
+		$this->assertSame( '100', $merged_query['max_price'] );
+		$this->assertTrue( $merged_query['isProductCollection'] );
+		$this->assertEmpty( $merged_query['meta_query'] );
 		set_query_var( 'max_price', '' );
 	}

 	/**
-	 * Test merging filter by min price queries.
+	 * @testdox Should pass the minimum price filter to shared query clauses.
 	 */
-	public function test_merging_filter_by_min_price_queries() {
+	public function test_merging_filter_by_min_price_queries(): void {
 		set_query_var( 'min_price', 20 );

 		$merged_query = Utils::initialize_merged_query( $this->block_instance );

-		$this->assertContainsEquals(
-			array(
-				array(),
-				array(
-					'key'     => '_price',
-					'value'   => 20,
-					'compare' => '>=',
-					'type'    => 'numeric',
-				),
-				'relation' => 'AND',
-			),
-			$merged_query['meta_query']
-		);
+		$this->assertSame( '20', $merged_query['min_price'] );
+		$this->assertEmpty( $merged_query['meta_query'] );
 		set_query_var( 'min_price', '' );
 	}

 	/**
-	 * Test merging filter by min and max price queries.
+	 * @testdox Should pass both price filters to shared query clauses.
 	 */
-	public function test_merging_filter_by_min_and_max_price_queries() {
+	public function test_merging_filter_by_min_and_max_price_queries(): void {
 		set_query_var( 'max_price', 100 );
 		set_query_var( 'min_price', 20 );

 		$merged_query = Utils::initialize_merged_query( $this->block_instance );

-		$this->assertContainsEquals(
-			array(
-				array(
-					'key'     => '_price',
-					'value'   => 100,
-					'compare' => '<=',
-					'type'    => 'numeric',
-				),
-				array(
-					'key'     => '_price',
-					'value'   => 20,
-					'compare' => '>=',
-					'type'    => 'numeric',
-				),
-				'relation' => 'AND',
-			),
-			$merged_query['meta_query']
-		);
+		$this->assertSame( '100', $merged_query['max_price'] );
+		$this->assertSame( '20', $merged_query['min_price'] );
+		$this->assertEmpty( $merged_query['meta_query'] );

 		set_query_var( 'max_price', '' );
 		set_query_var( 'min_price', '' );
 	}

 	/**
-	 * Test merging filter by stock status queries.
+	 * @testdox Should pass stock filters to shared query clauses.
 	 */
-	public function test_merging_filter_by_stock_status_queries() {
+	public function test_merging_filter_by_stock_status_queries(): void {
 		set_query_var( 'filter_stock_status', ProductStockStatus::IN_STOCK );

 		$merged_query = Utils::initialize_merged_query( $this->block_instance );

-		$this->assertContainsEquals(
-			array(
-				'operator' => 'IN',
-				'key'      => '_stock_status',
-				'value'    => array( ProductStockStatus::IN_STOCK ),
-			),
-			$merged_query['meta_query']
-		);
+		$this->assertSame( ProductStockStatus::IN_STOCK, $merged_query['filter_stock_status'] );
+		$this->assertEmpty( $merged_query['meta_query'] );

 		set_query_var( 'filter_stock_status', '' );
 	}
@@ -406,12 +365,61 @@ class QueryBuilder extends \WP_UnitTestCase {

 		set_query_var( 'filter_stock_status', ProductStockStatus::OUT_OF_STOCK );

-		$merged_query      = Utils::initialize_merged_query( $this->block_instance );
+		$merged_query             = Utils::initialize_merged_query( $this->block_instance );
+		$additional_query_builder = new \Automattic\WooCommerce\Blocks\BlockTypes\ProductCollection\QueryBuilder();
+		unset( $additional_query_builder );
+
 		$query             = new WP_Query( $merged_query );
 		$found_product_ids = wp_list_pluck( $query->posts, 'ID' );

 		$this->assertContains( $matching_product->get_id(), $found_product_ids, 'The out-of-stock target should be returned.' );
 		$this->assertNotContains( $non_matching_product->get_id(), $found_product_ids, 'The in-stock distractor should be excluded.' );
+		$this->assertSame( 1, substr_count( $query->request, 'wc_product_meta_lookup.stock_status IN' ), 'Multiple QueryBuilder instances should apply shared clauses once.' );
+	}
+
+	/**
+	 * @testdox Decimal price filters are preserved by shared query clauses.
+	 */
+	public function test_filter_by_decimal_price_returns_exact_products(): void {
+		$fixtures = new FixtureData();
+
+		$matching_product     = $fixtures->get_simple_product(
+			array(
+				'name'          => 'Decimal price target',
+				'regular_price' => '10.25',
+				'status'        => 'publish',
+			)
+		);
+		$non_matching_product = $fixtures->get_simple_product(
+			array(
+				'name'          => 'Decimal price distractor',
+				'regular_price' => '10.75',
+				'status'        => 'publish',
+			)
+		);
+
+		set_query_var( 'max_price', '10.50' );
+
+		$merged_query      = Utils::initialize_merged_query( $this->block_instance );
+		$query             = new WP_Query( $merged_query );
+		$found_product_ids = wp_list_pluck( $query->posts, 'ID' );
+
+		$this->assertContains( $matching_product->get_id(), $found_product_ids, 'The product below the decimal maximum should be returned.' );
+		$this->assertNotContains( $non_matching_product->get_id(), $found_product_ids, 'The product above the decimal maximum should be excluded.' );
+		set_query_var( 'max_price', '' );
+	}
+
+	/**
+	 * @testdox Invalid stock filters return no products through shared query clauses.
+	 */
+	public function test_invalid_stock_filter_returns_no_products(): void {
+		set_query_var( 'filter_stock_status', 'invalid' );
+
+		$merged_query = Utils::initialize_merged_query( $this->block_instance );
+		$query        = new WP_Query( $merged_query );
+
+		$this->assertEmpty( $query->posts );
+		set_query_var( 'filter_stock_status', '' );
 	}

 	/**
@@ -463,10 +471,9 @@ class QueryBuilder extends \WP_UnitTestCase {
 	}

 	/**
-	 * Test merging filter by stock status queries.
+	 * @testdox Should normalize attribute filters for shared query clauses and default to OR.
 	 */
-	public function test_merging_filter_by_attribute_queries() {
-		// Mock the attribute data.
+	public function test_merging_filter_by_attribute_queries(): void {
 		$this->block_instance->set_attributes_filter_query_args(
 			array(
 				array(
@@ -481,97 +488,134 @@ class QueryBuilder extends \WP_UnitTestCase {
 		);

 		set_query_var( 'filter_color', 'blue' );
-		set_query_var( 'query_type_color', 'or' );
 		set_query_var( 'filter_size', 'xl,xxl' );
 		set_query_var( 'query_type_size', 'and' );

 		$merged_query = Utils::initialize_merged_query( $this->block_instance );
-		$tax_queries  = $merged_query['tax_query'];
-
-		$and_query = array();
-		foreach ( $tax_queries as $tax_query ) {
-			if ( isset( $tax_query['relation'] ) && 'AND' === $tax_query['relation'] ) {
-				$and_query = $tax_query;
-			}
-		}

-		// Check if the AND query is an array.
-		$this->assertIsArray( $and_query );
+		$this->assertSame( 'blue', $merged_query['filter_color'] );
+		$this->assertSame( 'or', $merged_query['query_type_color'] );
+		$this->assertSame( 'xl,xxl', $merged_query['filter_size'] );
+		$this->assertSame( 'and', $merged_query['query_type_size'] );

-		$attribute_queries = array();
-		foreach ( $and_query as $and_query_item ) {
-			if ( is_array( $and_query_item ) ) {
-				$attribute_queries = $and_query_item;
-			}
-		}
+		set_query_var( 'filter_color', '' );
+		set_query_var( 'filter_size', '' );
+		set_query_var( 'query_type_size', '' );
+	}

-		$this->assertContainsEquals(
+	/**
+	 * @testdox Should normalize custom attribute parameter names for shared query clauses.
+	 */
+	public function test_custom_attribute_query_vars_are_normalized(): void {
+		$this->block_instance->set_attributes_filter_query_args(
 			array(
-				'taxonomy' => 'pa_color',
-				'field'    => 'slug',
-				'terms'    => array( 'blue' ),
-				'operator' => 'IN',
-			),
-			$attribute_queries
+				'color' => array(
+					'filter'     => 'pa_color',
+					'query_type' => 'custom_color_mode',
+				),
+			)
 		);

-		$this->assertContainsEquals(
-			array(
-				'taxonomy' => 'pa_size',
-				'field'    => 'slug',
-				'terms'    => array( 'xl', 'xxl' ),
-				'operator' => 'AND',
-			),
-			$attribute_queries
-		);
+		set_query_var( 'pa_color', 'blue' );
+		set_query_var( 'custom_color_mode', 'and' );

-		set_query_var( 'filter_color', '' );
-		set_query_var( 'query_type_color', '' );
-		set_query_var( 'filter_size', '' );
-		set_query_var( 'query_type_size', '' );
+		$merged_query = Utils::initialize_merged_query( $this->block_instance );
+
+		$this->assertSame( 'blue', $merged_query['filter_color'] );
+		$this->assertSame( 'and', $merged_query['query_type_color'] );
+		$this->assertArrayNotHasKey( 'pa_color', $merged_query );
+
+		set_query_var( 'pa_color', '' );
+		set_query_var( 'custom_color_mode', '' );
 	}

 	/**
-	 * Test merging multiple filter queries.
+	 * @testdox Should pass multiple shopper filters without building local SQL queries.
 	 */
-	public function test_merging_multiple_filter_queries() {
+	public function test_merging_multiple_filter_queries(): void {
 		set_query_var( 'max_price', 100 );
 		set_query_var( 'min_price', 20 );
 		set_query_var( 'filter_stock_status', ProductStockStatus::IN_STOCK );

 		$merged_query = Utils::initialize_merged_query( $this->block_instance );

-		$this->assertContainsEquals(
+		$this->assertSame( '100', $merged_query['max_price'] );
+		$this->assertSame( '20', $merged_query['min_price'] );
+		$this->assertSame( ProductStockStatus::IN_STOCK, $merged_query['filter_stock_status'] );
+		$this->assertEmpty( $merged_query['meta_query'] );
+
+		set_query_var( 'max_price', '' );
+		set_query_var( 'min_price', '' );
+		set_query_var( 'filter_stock_status', '' );
+	}
+
+	/**
+	 * @testdox Filter count queries omit shopper filters but keep collection constraints.
+	 */
+	public function test_filter_count_query_excludes_applied_filters(): void {
+		$query_builder = new \Automattic\WooCommerce\Blocks\BlockTypes\ProductCollection\QueryBuilder();
+		$query_builder->set_attributes_filter_query_args(
 			array(
-				'operator' => 'IN',
-				'key'      => '_stock_status',
-				'value'    => array( ProductStockStatus::IN_STOCK ),
-			),
-			$merged_query['meta_query']
+				'color' => array(
+					'filter'     => 'filter_color',
+					'query_type' => 'query_type_color',
+				),
+			)
 		);

-		$this->assertContainsEquals(
+		set_query_var( 'max_price', '100' );
+		set_query_var( 'filter_stock_status', ProductStockStatus::IN_STOCK );
+		set_query_var( 'filter_color', 'blue' );
+		set_query_var( 'categories', 'accessories' );
+
+		$final_query = $query_builder->get_final_query_args(
+			array( 'name' => '' ),
 			array(
-				array(
-					'key'     => '_price',
-					'value'   => 100,
-					'compare' => '<=',
-					'type'    => 'numeric',
-				),
-				array(
-					'key'     => '_price',
-					'value'   => 20,
-					'compare' => '>=',
-					'type'    => 'numeric',
-				),
-				'relation' => 'AND',
+				'meta_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+				'post__in'   => array(),
+				'tax_query'  => array(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
 			),
-			$merged_query['meta_query']
+			array(
+				'handpicked_products' => false,
+				'on_sale'             => false,
+				'orderby'             => '',
+				'product_attributes'  => array(),
+				'queryId'             => null,
+				'stock_status'        => array( ProductStockStatus::OUT_OF_STOCK ),
+			),
+			true
 		);

+		$this->assertTrue( $final_query['isProductCollection'] );
+		$this->assertNotEmpty( $final_query['meta_query'], 'Saved collection stock constraints should remain.' );
+		$this->assertArrayNotHasKey( 'max_price', $final_query );
+		$this->assertArrayNotHasKey( 'filter_stock_status', $final_query );
+		$this->assertArrayNotHasKey( 'filter_color', $final_query );
+		$this->assertArrayNotHasKey( 'categories', $final_query );
+
 		set_query_var( 'max_price', '' );
-		set_query_var( 'min_price', '' );
 		set_query_var( 'filter_stock_status', '' );
+		set_query_var( 'filter_color', '' );
+		set_query_var( 'categories', '' );
+	}
+
+	/**
+	 * @testdox Product Filter count queries apply shared clauses only once.
+	 */
+	public function test_filter_count_query_applies_shared_clauses_once(): void {
+		set_query_var( 'filter_stock_status', ProductStockStatus::IN_STOCK );
+		$query_args    = Utils::initialize_merged_query( $this->block_instance );
+		$query_clauses = wc_get_container()->get( QueryClauses::class );
+
+		add_filter( 'posts_clauses', array( $query_clauses, 'add_query_clauses' ), 10, 2 );
+		try {
+			$query = new WP_Query( $query_args );
+		} finally {
+			remove_filter( 'posts_clauses', array( $query_clauses, 'add_query_clauses' ), 10 );
+			set_query_var( 'filter_stock_status', '' );
+		}
+
+		$this->assertSame( 1, substr_count( $query->request, 'wc_product_meta_lookup.stock_status IN' ) );
 	}

 	/**
@@ -1193,177 +1237,60 @@ class QueryBuilder extends \WP_UnitTestCase {
 	}

 	/**
-	 * Test merging filter queries by Category Slug (e.g. ?categories=accessories).
+	 * @testdox Should pass category filters to shared query clauses.
 	 */
-	public function test_merging_filter_by_category_slug() {
-		// Set the URL query variables.
+	public function test_merging_filter_by_category_slug(): void {
 		set_query_var( 'categories', 'accessories' );

-		// Execute the query builder.
-		$merged_query   = Utils::initialize_merged_query( $this->block_instance );
-		$filter_clauses = $this->extract_filter_clauses( $merged_query['tax_query'] );
-
-		// Assertions.
-		$this->assertContainsEquals(
-			array(
-				'taxonomy' => 'product_cat',
-				'field'    => 'slug',
-				'terms'    => array( 'accessories' ),
-				'operator' => 'IN',
-			),
-			$filter_clauses,
-			'Should contain correct product_cat tax query using slug.'
-		);
+		$merged_query = Utils::initialize_merged_query( $this->block_instance );

-		// Clean up.
+		$this->assertSame( 'accessories', $merged_query['categories'] );
 		set_query_var( 'categories', '' );
 	}

 	/**
-	 * Test merging filter queries specifically for Tags.
-	 * Scenario: ?tags=tag-new (Slug)
+	 * @testdox Should pass tag filters to shared query clauses.
 	 */
-	public function test_merging_filter_by_tags() {
-		// Set the URL query variables.
+	public function test_merging_filter_by_tags(): void {
 		set_query_var( 'tags', 'tag-new' );

-		// Execute the query builder.
-		$merged_query   = Utils::initialize_merged_query( $this->block_instance );
-		$filter_clauses = $this->extract_filter_clauses( $merged_query['tax_query'] );
-
-		// Assertions.
-		$this->assertContainsEquals(
-			array(
-				'taxonomy' => 'product_tag',
-				'field'    => 'slug',
-				'terms'    => array( 'tag-new' ),
-				'operator' => 'IN',
-			),
-			$filter_clauses,
-			'Should contain correct product_tag tax query with IN operator.'
-		);
+		$merged_query = Utils::initialize_merged_query( $this->block_instance );

-		// Clean up.
+		$this->assertSame( 'tag-new', $merged_query['tags'] );
 		set_query_var( 'tags', '' );
 	}

 	/**
-	 * Test merging filter queries for Categories, Tags, and Brands simultaneously.
-	 * Scenario: ?categories=accessories&tags=tag-new&brands=nike
+	 * @testdox Should pass category, tag, and brand filters to shared query clauses.
 	 */
-	public function test_merging_filter_by_all_taxonomies_together() {
-		// Set the URL query variables.
+	public function test_merging_filter_by_all_taxonomies_together(): void {
 		set_query_var( 'categories', 'accessories' );
 		set_query_var( 'tags', 'tag-new' );
 		set_query_var( 'brands', 'nike' );

-		// Execute the query builder.
-		$merged_query   = Utils::initialize_merged_query( $this->block_instance );
-		$filter_clauses = $this->extract_filter_clauses( $merged_query['tax_query'] );
-
-		// Assertions.
-		// Verify Category.
-		$this->assertContainsEquals(
-			array(
-				'taxonomy' => 'product_cat',
-				'field'    => 'slug',
-				'terms'    => array( 'accessories' ),
-				'operator' => 'IN',
-			),
-			$filter_clauses,
-			'Should contain correct product_cat tax query.'
-		);
-
-		// Verify Tag.
-		$this->assertContainsEquals(
-			array(
-				'taxonomy' => 'product_tag',
-				'field'    => 'slug',
-				'terms'    => array( 'tag-new' ),
-				'operator' => 'IN',
-			),
-			$filter_clauses,
-			'Should contain correct product_tag tax query.'
-		);
+		$merged_query = Utils::initialize_merged_query( $this->block_instance );

-		// Verify Brand.
-		$this->assertContainsEquals(
-			array(
-				'taxonomy' => 'product_brand',
-				'field'    => 'slug',
-				'terms'    => array( 'nike' ),
-				'operator' => 'IN',
-			),
-			$filter_clauses,
-			'Should contain correct product_brand tax query.'
-		);
+		$this->assertSame( 'accessories', $merged_query['categories'] );
+		$this->assertSame( 'tag-new', $merged_query['tags'] );
+		$this->assertSame( 'nike', $merged_query['brands'] );

-		// Clean up global state.
 		set_query_var( 'categories', '' );
 		set_query_var( 'tags', '' );
 		set_query_var( 'brands', '' );
 	}

 	/**
-	 * Test that the strictly string-based filter logic works and SAFELY ignores arrays.
-	 * Matches logic: if ( ! is_string($param_value) ) continue;
+	 * @testdox Should ignore non-scalar taxonomy filter values.
 	 */
-	public function test_filter_strict_string_handling() {
-		// Scenario: Array Input (Should be IGNORED).
-		// ?categories[]=hats.
+	public function test_filter_strict_string_handling(): void {
 		set_query_var( 'categories', array( 'hats' ) );

-		// Execute.
-		$merged_query   = Utils::initialize_merged_query( $this->block_instance );
-		$tax_queries    = $merged_query['tax_query'] ?? array();
-		$filter_clauses = $this->extract_filter_clauses( $tax_queries );
-
-		// Assertion: The array input should have been ignored.
-		$this->assertNotContainsEquals(
-			array(
-				'taxonomy' => 'product_cat',
-				'field'    => 'slug',
-				'terms'    => array( 'hats' ),
-				'operator' => 'IN',
-			),
-			$filter_clauses,
-			'Should not contain product_cat tax query because array input should be ignored.'
-		);
+		$merged_query = Utils::initialize_merged_query( $this->block_instance );

-		// Clean up.
+		$this->assertArrayNotHasKey( 'categories', $merged_query );
 		set_query_var( 'categories', '' );
 	}

-	/**
-	 * Helper to extract filter clauses from the tax_query array.
-	 *
-	 * @param array $tax_queries The tax_query array from the merged query.
-	 * @return array The extracted filter clauses.
-	 */
-	private function extract_filter_clauses( array $tax_queries ) {
-
-		$and_query = array();
-
-		// Find the 'AND' relation group where filters are stored.
-		foreach ( $tax_queries as $tax_query ) {
-			if ( isset( $tax_query['relation'] ) && 'AND' === $tax_query['relation'] ) {
-				$and_query = $tax_query;
-				break;
-			}
-		}
-
-		$clauses = array();
-		if ( ! empty( $and_query ) ) {
-			foreach ( $and_query as $item ) {
-				if ( is_array( $item ) ) {
-					$clauses[] = $item;
-				}
-			}
-		}
-
-		return $clauses;
-	}
-
 	/**
 	 * @testdox Empty string values for perPage and offset fall back to defaults.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php
index 34fca6f359b..21a538693a9 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php
@@ -172,6 +172,34 @@ class FilterDataTest extends AbstractProductFiltersTest {
 		);
 	}

+	/**
+	 * @testdox Rating taxonomy queries constrain sibling stock counts.
+	 */
+	public function test_get_stock_status_counts_with_rating_tax_query(): void {
+		$product_visibility_terms = wc_get_product_visibility_term_ids();
+		$wp_query                 = new \WP_Query(
+			array(
+				'post_type' => 'product',
+				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+				'tax_query' => array(
+					array(
+						'field'         => 'term_taxonomy_id',
+						'taxonomy'      => 'product_visibility',
+						'terms'         => array( $product_visibility_terms['rated-5'] ),
+						'rating_filter' => true,
+					),
+				),
+			)
+		);
+
+		$this->test_get_stock_status_counts_with(
+			$wp_query,
+			function ( $product_data ) {
+				return in_array( $product_data['name'], array( 'Product 1', 'Product 4' ), true );
+			}
+		);
+	}
+
 	/**
 	 * @testdox Test rating counts without filter.
 	 */
@@ -256,10 +284,8 @@ class FilterDataTest extends AbstractProductFiltersTest {

 	/**
 	 * @testdox Test attribute count with query_type set to `and`.
-	 * @todo Remove this test once the issue with `and` query type is fixed in https://github.com/woocommerce/woocommerce/pull/44825.
 	 */
 	public function test_get_attribute_counts_with_query_type_and() {
-		$this->markTestSkipped( 'Skipping tests with query_type `and` because there is an issue with Filterer::filter_by_attribute_post_clauses that generate wrong clauses for `and`. We can fix the same issue in FilterClausesGenerator::add_attribute_clauses but doing so will make the attribute counts data doesnt match with current query. A fix for both methods is pending. See https://github.com/woocommerce/woocommerce/pull/44825.' );
 		$wp_query = new \WP_Query( array( 'post_type' => 'product' ) );
 		$wp_query->set( 'filter_color', 'blue-slug,green-slug' );
 		$wp_query->set( 'query_type_color', 'and' );
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/QueryClausesTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/QueryClausesTest.php
index c190a60942a..0256af2a4a5 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/QueryClausesTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/QueryClausesTest.php
@@ -121,6 +121,31 @@ class QueryClausesTest extends AbstractProductFiltersTest {
 		$this->assertEqualsCanonicalizing( $expected_products_name, $received_products_name );
 	}

+	/**
+	 * @testdox Decimal price boundaries are not truncated.
+	 */
+	public function test_decimal_price_boundaries_are_not_truncated(): void {
+		$product         = $this->fixture_data->get_simple_product(
+			array(
+				'name'          => 'Decimal price product',
+				'regular_price' => 10.5,
+			)
+		);
+		$price_range     = array(
+			'min_price' => 10.5,
+			'max_price' => 10.5,
+		);
+		$filter_callback = function ( $args ) use ( $price_range ) {
+			return $this->sut->add_price_clauses( $args, $price_range );
+		};
+
+		add_filter( 'posts_clauses', $filter_callback );
+		$received_products = wc_get_products( array() );
+		remove_filter( 'posts_clauses', $filter_callback );
+
+		$this->assertSame( array( $product->get_name() ), $this->get_data_from_products_array( $received_products ) );
+	}
+
 	/**
 	 * @testdox Test the product query with post clauses containing stock clauses.
 	 *
@@ -161,14 +186,19 @@ class QueryClausesTest extends AbstractProductFiltersTest {
 	 *           ["pa_color",["red-slug"],"or"]
 	 *           ["pa_color",["red-slug","not-exist-slug"],"or"]
 	 *           ["pa_color",["red-slug","green-slug"],"or"]
+	 *           ["pa_color",["red-slug","green-slug"],"and"]
+	 *           ["pa_color",["red-slug","blue-slug"],"and"]
+	 *           ["pa_color",["red-slug"],"or",false]
+	 *           ["pa_color",["red-slug","green-slug"],"and",false]
 	 *
-	 * @todo Add tests for `and` query type once https://github.com/woocommerce/woocommerce/pull/44825 is merged.
-	 *
-	 * @param string   $taxonomy   Attribute taxonomy name.
-	 * @param string[] $terms      Chosen terms' slug.
-	 * @param string   $query_type Query type. Accepts 'and' or 'or'.
+	 * @param string   $taxonomy      Attribute taxonomy name.
+	 * @param string[] $terms         Chosen terms' slug.
+	 * @param string   $query_type    Query type. Accepts 'and' or 'or'.
+	 * @param bool     $lookup_enabled Whether to use the product attributes lookup table.
 	 */
-	public function test_attribute_clauses_with( $taxonomy, $terms, $query_type ) {
+	public function test_attribute_clauses_with( $taxonomy, $terms, $query_type, $lookup_enabled = true ) {
+		update_option( 'woocommerce_attribute_lookup_enabled', $lookup_enabled ? 'yes' : 'no' );
+
 		$chosen_attributes = array(
 			$taxonomy => array(
 				'terms'      => $terms,
@@ -218,6 +248,27 @@ class QueryClausesTest extends AbstractProductFiltersTest {
 		$this->assertEqualsCanonicalizing( $expected_products_name, $received_products_name );
 	}

+	/**
+	 * @testdox Attribute lookup filtering ignores unknown terms in an AND query.
+	 */
+	public function test_attribute_lookup_clauses_ignore_unknown_and_terms(): void {
+		$chosen_attributes = array(
+			'pa_color' => array(
+				'terms'      => array( 'red-slug', 'not-exist-slug' ),
+				'query_type' => 'and',
+			),
+		);
+		$filter_callback   = function ( $args ) use ( $chosen_attributes ) {
+			return $this->sut->add_attribute_clauses( $args, $chosen_attributes );
+		};
+
+		add_filter( 'posts_clauses', $filter_callback );
+		$received_products = $this->get_data_from_products_array( wc_get_products( array() ) );
+		remove_filter( 'posts_clauses', $filter_callback );
+
+		$this->assertSame( array( 'Product 5' ), $received_products );
+	}
+
 	/**
 	 * Test the product query with post clauses containing taxonomy clauses.
 	 *
@@ -268,6 +319,25 @@ class QueryClausesTest extends AbstractProductFiltersTest {
 		$this->assertEqualsCanonicalizing( $expected_products_name, $received_products_name );
 	}

+	/**
+	 * @testdox Taxonomy filtering ignores selected taxonomies with no matching term.
+	 */
+	public function test_taxonomy_clauses_ignore_taxonomies_without_matching_terms(): void {
+		$chosen_taxonomies = array(
+			'product_cat' => array( 'cat-1' ),
+			'product_tag' => array( 'not-exist-slug' ),
+		);
+		$filter_callback   = function ( $args ) use ( $chosen_taxonomies ) {
+			return $this->sut->add_taxonomy_clauses( $args, $chosen_taxonomies );
+		};
+
+		add_filter( 'posts_clauses', $filter_callback );
+		$received_products = $this->get_data_from_products_array( wc_get_products( array() ) );
+		remove_filter( 'posts_clauses', $filter_callback );
+
+		$this->assertEqualsCanonicalizing( array( 'Product 1', 'Product 4', 'Product 5', 'Product 6' ), $received_products );
+	}
+
 	/**
 	 * @testdox Non-string taxonomy filter values are ignored.
 	 */