Commit 1dbe696c017 for woocommerce

commit 1dbe696c0170bbb452d0e342ab9e7011a850416a
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Thu Aug 20 17:33:38 2026 +0300

    Restore lookup table join for the out-of-stock products filter (#67871)

    * fix(admin): Restore lookup join for the out-of-stock products filter

    The Products list stock filter has always left the wc_product_meta_lookup
    alias joined whenever stock_status is set, so posts_clauses callbacks
    registered after WooCommerce could reference it. PR #66942 split
    filter_stock_status_post_clauses() into two paths, and the new out-of-stock
    branch builds a self-contained subquery with its own aliases: it never calls
    append_product_sorting_table_join(), while the else branch still does.

    Third-party callbacks that key off $_GET['stock_status'] and reference the
    alias without appending their own join therefore hit "Unknown column
    'wc_product_meta_lookup.*' in 'WHERE'" and render an empty Products list —
    but only for Out of stock. The identical callback keeps working for In stock
    and On backorder, which makes the failure hard to attribute. No in-tree
    consumer breaks; every core filter self-appends via the same helper.

    Call the helper on the out-of-stock path too. It is idempotent, and the join
    is a LEFT JOIN on the lookup table's PRIMARY KEY, so it can neither filter
    nor duplicate rows: the filter's result set is unchanged.

    Refs WOOPLUG-7258

    * refactor(admin): Hoist the stock filter lookup join out of its branches

    The join was restored inside the out-of-stock branch, leaving two call sites
    for the same helper and a comment that claimed to be about every stock status
    while sitting inside a branch covering exactly one. Hoisting the single call
    above the branch makes "a stock filter always joins the lookup table" a
    property of the method, so a status added later cannot reintroduce the
    regression by forgetting it.

    Two robustness gaps surfaced alongside it, both on values that arrive through
    the posts_clauses filter and therefore cannot be trusted:

    - wc_clean() returns an array for a request shaped as stock_status[]=outofstock,
      which fails the strict comparison and silently fell through to the aggregate
      branch, dropping the variation-aware matching. Collapse it to a scalar first.
    - The clause array is produced by other callbacks, so 'join' may be absent or
      not a string. Default it at the call site and coerce inside the helper.

    Tests now drive the behaviour rather than the mechanism: a real query through a
    callback that reads the alias without joining it, plus coverage for duplicate
    joins, array-shaped input, and a missing join clause. All four were confirmed
    revert-sensitive by mutating each guard in turn.

    Refs WOOPLUG-7258

    * fix(admin): Join the lookup table before normalising the stock status

    The previous commit normalised the request value first and returned early when
    nothing usable was left, which put the early return above the join. The filter
    is registered on a non-empty *raw* stock_status, so values that pass that check
    and then sanitise away — ' ', '<b>', 'stock_status[]=', 'stock_status[][]=x' —
    reached the early return and left without the join.

    That is the same failure this branch exists to fix, reintroduced for a narrower
    set of inputs: with a callback that reads the alias, those requests produced
    'Unknown column wc_product_meta_lookup.*' again, and without one the list went
    from matching no product to showing the whole catalogue. Verified against
    32d1511ea2: all four values joined and filtered to stock_status='' there.

    Join first, then normalise, so the alias is present for every request the
    filter is registered for. A value that is still not a string becomes '', which
    matches no product, exactly as before.

    Two related adjustments:

    - Multi-value requests now honour the first value instead of matching the whole
      catalogue, which is what happened when wpdb::prepare() rejected the surplus
      argument and returned no clause. Recorded in a comment as deliberate.
    - The helper now keeps scalar and __toString values instead of discarding them.
      Throwing away a join while the WHERE that depends on it survives would leave
      a broken query rather than an unfiltered one.

    The join clause is also defaulted at the other ten helper call sites. The
    undefined-key notice fires while evaluating the argument, so it cannot be
    handled inside the helper.

    Refs WOOPLUG-7258

diff --git a/plugins/woocommerce/changelog/fix-wooplug-7258-outofstock-lookup-join b/plugins/woocommerce/changelog/fix-wooplug-7258-outofstock-lookup-join
new file mode 100644
index 00000000000..6261381633f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-7258-outofstock-lookup-join
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Restore the wc_product_meta_lookup join when the Products list is filtered by "Out of stock", so posts_clauses callbacks registered after WooCommerce can still reference the alias.
diff --git a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php
index 3eda6f2a074..f2b226dd017 100644
--- a/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php
+++ b/plugins/woocommerce/includes/admin/list-tables/class-wc-admin-list-table-products.php
@@ -928,7 +928,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function order_by_price_asc_post_clauses( $args ) {
-		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']    = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['orderby'] = ' wc_product_meta_lookup.min_price ASC, wc_product_meta_lookup.product_id ASC ';
 		return $args;
 	}
@@ -940,7 +940,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function order_by_price_desc_post_clauses( $args ) {
-		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']    = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['orderby'] = ' wc_product_meta_lookup.max_price DESC, wc_product_meta_lookup.product_id DESC ';
 		return $args;
 	}
@@ -952,7 +952,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function order_by_sku_asc_post_clauses( $args ) {
-		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']    = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['orderby'] = ' wc_product_meta_lookup.sku ASC, wc_product_meta_lookup.product_id ASC ';
 		return $args;
 	}
@@ -964,7 +964,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function order_by_sku_desc_post_clauses( $args ) {
-		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']    = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['orderby'] = ' wc_product_meta_lookup.sku DESC, wc_product_meta_lookup.product_id DESC ';
 		return $args;
 	}
@@ -976,7 +976,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function order_by_cogs_value_asc_post_clauses( $args ) {
-		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']    = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['orderby'] = ' wc_product_meta_lookup.cogs_total_value ASC, wc_product_meta_lookup.product_id ASC ';
 		return $args;
 	}
@@ -988,7 +988,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function order_by_cogs_value_desc_post_clauses( $args ) {
-		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']    = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['orderby'] = ' wc_product_meta_lookup.cogs_total_value DESC, wc_product_meta_lookup.product_id DESC ';
 		return $args;
 	}
@@ -1000,7 +1000,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function order_by_global_unique_id_asc_post_clauses( $args ) {
-		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']    = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['orderby'] = ' wc_product_meta_lookup.global_unique_id ASC, wc_product_meta_lookup.product_id ASC ';
 		return $args;
 	}
@@ -1012,7 +1012,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function order_by_global_unique_id_desc_post_clauses( $args ) {
-		$args['join']    = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']    = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['orderby'] = ' wc_product_meta_lookup.global_unique_id DESC, wc_product_meta_lookup.product_id DESC ';
 		return $args;
 	}
@@ -1024,7 +1024,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function filter_downloadable_post_clauses( $args ) {
-		$args['join']   = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']   = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['where'] .= ' AND wc_product_meta_lookup.downloadable=1 ';
 		return $args;
 	}
@@ -1036,7 +1036,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	 * @return array
 	 */
 	public function filter_virtual_post_clauses( $args ) {
-		$args['join']   = $this->append_product_sorting_table_join( $args['join'] );
+		$args['join']   = $this->append_product_sorting_table_join( $args['join'] ?? '' );
 		$args['where'] .= ' AND wc_product_meta_lookup.virtual=1 ';
 		return $args;
 	}
@@ -1050,8 +1050,27 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	public function filter_stock_status_post_clauses( $args ) {
 		global $wpdb;
 		if ( ! empty( $_GET['stock_status'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+			// Join before looking at the value at all. This filter is registered on a non-empty raw
+			// stock_status, and has joined the lookup table for every such request since the feature
+			// shipped; callbacks running after it rely on the alias existing. Values that normalise to
+			// nothing ( ' ', '<b>', 'stock_status[]=' ) still reach here, so deciding the join after
+			// normalising would drop it for exactly the requests that used to keep it.
+			$args['join'] = $this->append_product_sorting_table_join( $args['join'] ?? '' );
+
 			$stock_status = wc_clean( wp_unslash( $_GET['stock_status'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended

+			// The request can shape this as an array, but the filter only ever describes a single
+			// status, so the first value wins. That is deliberate: multi-value requests previously
+			// matched the whole catalogue, because wpdb::prepare() rejects the surplus argument and
+			// returns no clause at all. Anything that is still not a string normalises to '', which
+			// matches no product -- the behaviour those requests have always had.
+			if ( is_array( $stock_status ) ) {
+				$stock_status = reset( $stock_status );
+			}
+			if ( ! is_string( $stock_status ) ) {
+				$stock_status = '';
+			}
+
 			if ( ProductStockStatus::OUT_OF_STOCK === $stock_status ) {
 				// Only published variations qualify their parent for this discoverability filter.
 				// Other statuses retain normal aggregate-parent behavior.
@@ -1081,7 +1100,6 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 					$stock_status
 				);
 			} else {
-				$args['join']   = $this->append_product_sorting_table_join( $args['join'] );
 				$args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.stock_status=%s ', $stock_status );
 			}
 		}
@@ -1097,6 +1115,14 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
 	private function append_product_sorting_table_join( $sql ) {
 		global $wpdb;

+		// Another posts_clauses callback produced this clause, so it is not guaranteed to be a string.
+		// Keep anything that can become one: discarding a join while the WHERE that depends on it
+		// survives would leave a broken query rather than a merely unfiltered one.
+		if ( ! is_string( $sql ) ) {
+			$stringable = is_scalar( $sql ) || ( is_object( $sql ) && method_exists( $sql, '__toString' ) );
+			$sql        = $stringable ? (string) $sql : '';
+		}
+
 		if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
 			$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
 		}
diff --git a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-products-test.php b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-products-test.php
index 93e047bc5ba..8c43b655850 100644
--- a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-products-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-products-test.php
@@ -716,6 +716,241 @@ class WC_Admin_List_Table_Products_Test extends WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * Every stock status leaves the lookup table joined for later posts_clauses callbacks.
+	 *
+	 * The out-of-stock branch builds a self-contained subquery and does not read the alias itself, but
+	 * callbacks registered after this one have always been able to rely on it being joined whenever a
+	 * stock filter is active. Dropping it for one status only breaks those callbacks asymmetrically.
+	 *
+	 * @testdox Every stock status leaves the lookup table joined for later posts_clauses callbacks.
+	 *
+	 * @dataProvider stock_status_provider
+	 *
+	 * @param string $stock_status Stock status the products list is filtered by.
+	 */
+	public function test_stock_status_filter_always_joins_the_product_meta_lookup_table( string $stock_status ): void {
+		global $wpdb;
+
+		$args = $this->filter_clauses_for_stock_status( $stock_status );
+
+		$this->assertStringContainsString(
+			"LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup",
+			$args['join'],
+			"The {$stock_status} filter must leave the wc_product_meta_lookup alias joined so later posts_clauses callbacks can reference it."
+		);
+	}
+
+	/**
+	 * A join an earlier callback already added is left alone.
+	 *
+	 * Joining unconditionally is only safe because of this: extensions that append their own join must
+	 * not end up with a duplicate, which the database rejects outright.
+	 *
+	 * @testdox A lookup table join added by an earlier callback is not duplicated.
+	 *
+	 * @dataProvider stock_status_provider
+	 *
+	 * @param string $stock_status Stock status the products list is filtered by.
+	 */
+	public function test_stock_status_filter_does_not_duplicate_an_existing_lookup_join( string $stock_status ): void {
+		global $wpdb;
+
+		$existing_join = " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON {$wpdb->posts}.ID = wc_product_meta_lookup.product_id ";
+
+		$args = $this->filter_clauses_for_stock_status(
+			$stock_status,
+			array(
+				'join'  => $existing_join,
+				'where' => '',
+			)
+		);
+
+		$this->assertSame(
+			1,
+			substr_count( $args['join'], 'wc_product_meta_lookup ON' ),
+			"The {$stock_status} filter must not join wc_product_meta_lookup a second time; a duplicate alias is a hard SQL error."
+		);
+	}
+
+	/**
+	 * An array-shaped stock_status request still selects the matching branch.
+	 *
+	 * @testdox An array-shaped stock_status request still uses the variation-aware out-of-stock branch.
+	 */
+	public function test_array_shaped_stock_status_uses_the_matching_branch(): void {
+		$args = $this->filter_clauses_for_stock_status( array( ProductStockStatus::OUT_OF_STOCK ) );
+
+		$this->assertStringContainsString(
+			'stock_status_products',
+			$args['where'],
+			'An array-shaped out-of-stock request should take the same branch as the scalar form.'
+		);
+	}
+
+	/**
+	 * Values that normalise to nothing still join the lookup table.
+	 *
+	 * The filter is registered on a non-empty raw request value, so anything that survives that check
+	 * but empties out during sanitisation still reaches the filter. Those requests have always been
+	 * joined and have always matched no product; deciding the join after normalising would silently
+	 * drop it for exactly them.
+	 *
+	 * @testdox A stock_status that normalises to nothing still joins the lookup table and matches no product.
+	 *
+	 * @dataProvider degenerate_stock_status_provider
+	 *
+	 * @param string|array $stock_status Value of the stock_status request parameter.
+	 */
+	public function test_degenerate_stock_status_still_joins_the_lookup_table( $stock_status ): void {
+		global $wpdb;
+
+		$args = $this->filter_clauses_for_stock_status( $stock_status );
+
+		$this->assertStringContainsString(
+			"LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup",
+			$args['join'],
+			'A stock_status that sanitises away must still leave the lookup table joined for later callbacks.'
+		);
+		$this->assertStringContainsString(
+			"wc_product_meta_lookup.stock_status=''",
+			$args['where'],
+			'A stock_status that sanitises away should keep matching no product rather than dropping the filter.'
+		);
+	}
+
+	/**
+	 * A multi-value stock_status honours the first value.
+	 *
+	 * @testdox A multi-value stock_status request filters on the first value.
+	 */
+	public function test_multi_value_stock_status_uses_the_first_value(): void {
+		$args = $this->filter_clauses_for_stock_status(
+			array( ProductStockStatus::OUT_OF_STOCK, ProductStockStatus::IN_STOCK )
+		);
+
+		$this->assertStringContainsString(
+			'stock_status_products',
+			$args['where'],
+			'A multi-value request should filter on its first value rather than matching the whole catalogue.'
+		);
+	}
+
+	/**
+	 * Request values that survive the non-empty check but sanitise away.
+	 *
+	 * @return array<string, array<mixed>>
+	 */
+	public function degenerate_stock_status_provider(): array {
+		return array(
+			'whitespace only'       => array( ' ' ),
+			'markup only'           => array( '<b>' ),
+			'array with empty item' => array( array( '' ) ),
+			'nested array'          => array( array( array( 'x' ) ) ),
+		);
+	}
+
+	/**
+	 * Clauses arriving without a usable join string are tolerated.
+	 *
+	 * @testdox Clauses arriving without a join string do not break the stock filter.
+	 */
+	public function test_stock_status_filter_tolerates_missing_join_clause(): void {
+		global $wpdb;
+
+		$args = $this->filter_clauses_for_stock_status( ProductStockStatus::OUT_OF_STOCK, array( 'where' => '' ) );
+
+		$this->assertStringContainsString(
+			"LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup",
+			$args['join'],
+			'A callback that drops the join clause should not stop the filter from joining the lookup table.'
+		);
+	}
+
+	/**
+	 * Later callbacks can read the lookup alias without joining it themselves.
+	 *
+	 * This is the behaviour the join exists for. Asserting the join string alone cannot catch a change
+	 * that leaves the alias unusable in the assembled query, so drive a real query through it.
+	 *
+	 * @testdox Out-of-stock filtering works for callbacks that read the lookup alias without joining it.
+	 */
+	public function test_out_of_stock_filter_supports_callbacks_relying_on_the_lookup_join(): void {
+		update_option( 'woocommerce_manage_stock', 'no' );
+
+		$out_of_stock = WC_Helper_Product::create_simple_product();
+		$out_of_stock->set_manage_stock( false );
+		$out_of_stock->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
+		$out_of_stock->save();
+
+		// Stands in for an extension: reads the alias, appends no join of its own.
+		$consumer = static function ( $clauses ) {
+			$clauses['where'] .= ' AND wc_product_meta_lookup.virtual = 0 ';
+			return $clauses;
+		};
+
+		$ids = $this->query_product_ids_for_stock_status( ProductStockStatus::OUT_OF_STOCK, $consumer );
+
+		$this->assertContains(
+			$out_of_stock->get_id(),
+			$ids,
+			'A posts_clauses callback that references wc_product_meta_lookup without joining it should still produce a valid query.'
+		);
+	}
+
+	/**
+	 * Stock statuses offered by the products list filter.
+	 *
+	 * @return array<string, array<string>>
+	 */
+	public function stock_status_provider(): array {
+		return array(
+			'in stock'     => array( ProductStockStatus::IN_STOCK ),
+			'out of stock' => array( ProductStockStatus::OUT_OF_STOCK ),
+			'on backorder' => array( ProductStockStatus::ON_BACKORDER ),
+		);
+	}
+
+	/**
+	 * Run the stock status clause filter for a given request value.
+	 *
+	 * @param string|array $stock_status Value of the stock_status request parameter.
+	 * @param array|null   $clauses      Clause array to filter; defaults to an empty join and where.
+	 * @return array
+	 */
+	private function filter_clauses_for_stock_status( $stock_status, ?array $clauses = null ): array {
+		$clauses = $clauses ?? array(
+			'join'  => '',
+			'where' => '',
+		);
+
+		return $this->with_stock_status(
+			$stock_status,
+			function () use ( $clauses ) {
+				return $this->sut->filter_stock_status_post_clauses( $clauses );
+			}
+		);
+	}
+
+	/**
+	 * Run a callback with the stock_status request parameter set, restoring $_GET afterwards.
+	 *
+	 * @param string|array $stock_status Value of the stock_status request parameter.
+	 * @param callable     $callback     Callback to run.
+	 * @return mixed
+	 */
+	private function with_stock_status( $stock_status, callable $callback ) {
+		$original_get = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Test cleanup restores the raw original request data.
+
+		$_GET['stock_status'] = $stock_status;
+
+		try {
+			return $callback();
+		} finally {
+			$_GET = $original_get;
+		}
+	}
+
 	/**
 	 * Create title and content-only search matches.
 	 *
@@ -797,39 +1032,44 @@ class WC_Admin_List_Table_Products_Test extends WC_Unit_Test_Case {
 	/**
 	 * Query product IDs through the products list table stock-status filter.
 	 *
-	 * @param string      $stock_status      Stock status to query.
-	 * @param string|null $additional_filter Optional clause filter to register after the stock filter.
+	 * @param string               $stock_status      Stock status to query.
+	 * @param string|callable|null $additional_filter Optional clause filter to register after the stock
+	 *                                                filter: a method name on the list table, or any callable.
 	 * @return array
 	 */
 	private function query_product_ids_for_stock_status( $stock_status, $additional_filter = null ) {
-		$sut          = ( new ReflectionClass( WC_Admin_List_Table_Products::class ) )->newInstanceWithoutConstructor();
-		$original_get = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Test cleanup restores the raw original request data.
-
-		$_GET['stock_status'] = $stock_status;
-		add_filter( 'posts_clauses', array( $sut, 'filter_stock_status_post_clauses' ) );
-		if ( $additional_filter ) {
-			add_filter( 'posts_clauses', array( $sut, $additional_filter ) );
-		}
-
-		try {
-			$query = new WP_Query(
-				array(
-					'fields'         => 'ids',
-					'orderby'        => 'ID',
-					'order'          => 'ASC',
-					'post_status'    => 'publish',
-					'post_type'      => 'product',
-					'posts_per_page' => -1,
-				)
-			);
-
-			return array_map( 'intval', $query->posts );
-		} finally {
-			remove_filter( 'posts_clauses', array( $sut, 'filter_stock_status_post_clauses' ) );
-			if ( $additional_filter ) {
-				remove_filter( 'posts_clauses', array( $sut, $additional_filter ) );
+		$sut = ( new ReflectionClass( WC_Admin_List_Table_Products::class ) )->newInstanceWithoutConstructor();
+
+		return $this->with_stock_status(
+			$stock_status,
+			function () use ( $sut, $additional_filter ) {
+				$extra = is_string( $additional_filter ) ? array( $sut, $additional_filter ) : $additional_filter;
+
+				add_filter( 'posts_clauses', array( $sut, 'filter_stock_status_post_clauses' ) );
+				if ( $extra ) {
+					add_filter( 'posts_clauses', $extra );
+				}
+
+				try {
+					$query = new WP_Query(
+						array(
+							'fields'         => 'ids',
+							'orderby'        => 'ID',
+							'order'          => 'ASC',
+							'post_status'    => 'publish',
+							'post_type'      => 'product',
+							'posts_per_page' => -1,
+						)
+					);
+
+					return array_map( 'intval', $query->posts );
+				} finally {
+					remove_filter( 'posts_clauses', array( $sut, 'filter_stock_status_post_clauses' ) );
+					if ( $extra ) {
+						remove_filter( 'posts_clauses', $extra );
+					}
+				}
 			}
-			$_GET = $original_get;
-		}
+		);
 	}
 }