Repository: wp-graphql/wp-graphql (language: php)

Issue:
Acquiring the WP_Query instance within a wp-graphql filter causes post hooks to fire again
### Description

When acquiring the WP_Query instance inside of wp-graphql filters, it causes post hooks such as `posts_pre_query` to fire a second time within the wp-graphql request lifecyle.


**Example**
```php
function filter_graphql_connection_page_info(
  $page_info,
  AbstractConnectionResolver $resolver,
) {
  $query = $resolver->get_query();
  return $query->query_args;
}

add_filter(
  'graphql_connection_page_info',
  ['filter_graphql_connection_page_info'],
  10,
  2
);
```
**Explanation**

In the case the `AbstractConnectionResolver` is an instance of `PostObjectConnectionResolver `
we can see that the `WP_Query` class is instantiated

https://github.com/wp-graphql/wp-graphql/blob/9ee971695cdb848d7b51afdd2df837bdf111df76/src/Data/Connection/PostObjectConnectionResolver.php#L94-L107

Digging into WordPress core we can see the side-effect of instantiating a new instance of `WP_Query` calls `$this->query( $query )` in its constructor and then calls `$this->get_posts()`

https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-query.php#L3787

This results in applying the `posts_pre_query` and other hooks https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-query.php#L3136

### Steps to reproduce

Define hooks:
```php
function filter_graphql_connection_page_info(
  $page_info,
  AbstractConnectionResolver $resolver,
) {
  $query = $resolver->get_query();
  return $query->query_args;
}

add_filter(
  'graphql_connection_page_info',
  ['filter_graphql_connection_page_info'],
  10,
  2
);

function filter_posts_pre_query($posts, WP_Query $query) {
   graphql_debug("Triggered posts_pre_query");
   return $posts;
}

add_filter('posts_pre_query', 'filter_posts_pre_query', 10, 2);
```

With wp-graphql debug enabled run the query 

```graphql
{
  posts {
    nodes {
      title
    }
  }
}
```

### Additional context

_No response_

### WPGraphQL Version

1.13.8

### WordPress Version

6.1.1

### PHP Version

7.4 & 8

### Additional enviornment details

Observed in wp-graphql-offset-pagination https://github.com/valu-digital/wp-graphql-offset-pagination/blob/master/src/Loader.php#L128

### Please confirm that you have searched existing issues in the repo.

- [X] Yes

### Please confirm that you have disabled ALL plugins except for WPGraphQL.

- [X] Yes
- [ ] My issue is with compatibility with a specific WordPress plugin, and I have listed all my installed plugins (and version info) above.
Overriding/subclassing the default object connection types
### What problem does this address?

Hi.

I have a situation where we are using paginated queries, but need to apply complex access control logic.

This section from `PostObjectConnectionResolver` illustrates the issue:

```
  /**
   * {@inheritDoc}
   */
  public function get_ids_from_query()
  {
    $ids = !empty($this->query->posts) ? $this->query->posts : [];

    // FIXME: this is where we should filter entries according to security rules
    foreach($ids as $id) {

    }

    // If we're going backwards, we need to reverse the array.
    if (!empty($this->args['last'])) {
      $ids = array_reverse($ids);
    }

    return $ids;
  }
  ```
  
Existing filters let us either filter the `WP_Query` upstream (but our logic can't be expressed as a meta query), or the returned ids _after_ cursors have been applied to them. 

Currently, the `AbstractConnectionResolver` applies cursors immediately to the output of the above function.

### What is your proposed solution?

A filter after ids have been retrieved, but before cursors are applied to them.

### What alternatives have you considered?

I have tried to override the `PostObjectConnectionResolver`:

```
register_graphql_connection(
  [
    'fromType'       => 'RootQuery',
    'toType'         => ucfirst($post_type->graphql_single_name),
    'queryClass'     => 'WP_Query',
    'fromFieldName'  => $post_type->graphql_plural_name,
    'connectionArgs' => PostObjects::get_connection_args(),
    'resolve'        => function ($root, $args, $context, $info) use ($post_type) {
      $resolver = new \WPGraphQL\Data\Connection\CustomPostObjectConnectionResolver($root, $args, $context, $info, $post_type);

      return $resolver->get_connection();
    },
  ]
);
 ```
 
 In:
 
 ```
 graphql_register_types
 ```
 
 But obviously, this results in a complaint about duplicate types.
 
 Is something like this possible currently?
 

### Additional Context

_No response_


Discussion / hints:
Related to https://github.com/wp-graphql/wp-graphql/issues/2696
> I have a situation where we are using paginated queries, but need to apply complex access control logic.

@maurocolella can you provide a bit more information about what logic you need to apply?

If it's logic about individual posts and whether they should be returned or not, the Model layer might be the right place to apply this logic. 

For example, [this filter](https://github.com/wp-graphql/wp-graphql/blob/develop/src/Model/Model.php#L238) could be used to determine whether the node should be considered private or not. 

If you return `true` from that filter, then the node would be considered private, and the node would be removed from the results of the query. 

Here's an example of it in use: https://github.com/wp-graphql/wp-graphql/issues/2316#issuecomment-1085825927

If your complex logic is something else, could you expand a bit further?
Jason Bahl thanks! This might be what I was looking for.

Although we had another case with a complex order by where I did have to
create a custom resolver, regarding node privacy here, we have to apply
access control according to post meta against user meta, while taking
ancestry into account (so user meta A matches post meta A OR any ancestor
of post meta A).

Effectively, this forces us to resolve the ancestry based on the content of
meta. And performing this at runtime looks more maintainable.

I am also mentioning the other case (order by) because it posed some
challenges that might or might not be useful to the WP GraphQL team.

Happy to answer any question.

On Fri, Dec 16, 2022, 4:03 AM Jason Bahl ***@***.***> wrote:

> I have a situation where we are using paginated queries, but need to apply
> complex access control logic.
>
> @maurocolella <https://github.com/maurocolella> can you provide a bit
> more information about what logic you need to apply?
>
> If it's logic about individual posts and whether they should be returned
> or not, the Model layer might be the right place to apply this logic.
>
> For example, this filter
> <https://github.com/wp-graphql/wp-graphql/blob/develop/src/Model/Model.php#L238>
> could be used to determine whether the node should be considered private or
> not.
>
> If you return true from that filter, then the node would be considered
> private, and the node would be removed from the results of the query.
>
> Here's an example of it in use: #2316 (comment)
> <https://github.com/wp-graphql/wp-graphql/issues/2316#issuecomment-1085825927>
>
> If your complex logic is something else, could you expand a bit further?
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/wp-graphql/wp-graphql/issues/2678#issuecomment-1353641099>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAFZ7NHIM5LEUCWFLLGOD6DWNN2PTANCNFSM6AAAAAAS7ZGU34>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

There's _currently_ no good way to overload the abstract connection resolver, ( although it's essential to #2466). 

In addition to @jasonbahl 's suggestion for your initial example, other approaches could be:
1. using the `graphql_wp_connection_type_config` filter to replace the `resolve` callback.
2. Use the `graphql_exclude_connections` or `graphql_register_root_connection` on your CPT registration args to exclude the connection you're regstering manually with `register_graphql_connection()`.

Regardless, [this line in `AbstractConnectionResolver` ](https://github.com/wp-graphql/wp-graphql/blob/6ae12dfe9baa9ad9625f02531a5af8e041a3c8ba/src/Data/Connection/AbstractConnectionResolver.php#L607) seems like the perfect place for a WP filter to give people more control of their pre-cursorised query results without needing to overload the method in a child class. @jasonbahl , thoughts?
Thanks a lot @jasonbahl and @justlevine .

While I am here, allow me to point out an [adjacent issue](https://github.com/wp-graphql/wp-graphql-acf/issues/343). For us, this implementation is in part tied to ACF, but when working with ACF and the [page link field type](https://www.advancedcustomfields.com/resources/page-link/), we've noted that there isn't clear behavior defined for the cases where it's either null, or points to an archive page (and ACF returns a string).

My workaround was to register a union type between `PostObjectType` and `AcfLink`, and to filter the resolution of page link fields to return in the shape of an `AcfLink` when the value resolves to a string.
Quick update: Ive rebased #2749 on v1.21.1, and got some initial buy-in on the premise from @jasonbahl earlier this week, so I should hopefully be able to start work on addressing this in my next sprint.

( Relabeling this as an enhancement, since this is a symptom of general lifecycle issues in the AbstractConnectionResolver ).

Fix patch:
diff --git a/src/Data/Connection/AbstractConnectionResolver.php b/src/Data/Connection/AbstractConnectionResolver.php
index 3dd4f34d9..481f8b923 100644
--- a/src/Data/Connection/AbstractConnectionResolver.php
+++ b/src/Data/Connection/AbstractConnectionResolver.php
@@ -2,8 +2,8 @@
 
 namespace WPGraphQL\Data\Connection;
 
-use Exception;
 use GraphQL\Deferred;
+use GraphQL\Error\InvariantViolation;
 use GraphQL\Error\UserError;
 use GraphQL\Type\Definition\ResolveInfo;
 use WPGraphQL\AppContext;
@@ -15,6 +15,12 @@
  * Individual Connection Resolvers should extend this to make returning data in proper shape for Relay-compliant connections easier, ensure data is passed through consistent filters, etc.
  *
  * @package WPGraphQL\Data\Connection
+ *
+ * The template type `TQueryClass` is used by static analysis tools to correctly typehint the query class used by the Connection Resolver.
+ * Classes that extend `AbstractConnectionResolver` should add `@extends @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\MY_QUERY_CLASS>` to the class dockblock to get proper hinting.
+ * E.g. `@extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_Term_Query>`
+ *
+ * @template TQueryClass
  */
 abstract class AbstractConnectionResolver {
 	/**
@@ -98,7 +104,16 @@ abstract class AbstractConnectionResolver {
 	public $one_to_one = false;
 
 	/**
-	 * The Query class/array/object used to fetch the data.
+	 * The class name of the query to instantiate. Set to `null` if the Connection Resolver does not rely on a query class to fetch data.
+	 *
+	 * Examples `WP_Query`, `WP_Comment_Query`, `WC_Query`, `/My/Namespaced/CustomQuery`, etc.
+	 *
+	 * @var ?class-string<TQueryClass>
+	 */
+	protected $query_class;
+
+	/**
+	 * The instantiated query array/object used to fetch the data.
 	 *
 	 * Examples:
 	 *   return new WP_Query( $this->get_query_args() );
@@ -109,7 +124,9 @@ abstract class AbstractConnectionResolver {
 	 * have context from what was queried and can make adjustments as needed, such
 	 * as exposing `totalCount` in pageInfo, etc.
 	 *
-	 * @var mixed[]|object|mixed
+	 * Filterable by `graphql_connection_pre_get_query` and `graphql_connection_query`.
+	 *
+	 * @var ?TQueryClass
 	 */
 	protected $query;
 
@@ -164,8 +181,6 @@ abstract class AbstractConnectionResolver {
 	 * @param array<string,mixed>                  $args    Array of arguments input in the field as part of the GraphQL query.
 	 * @param \WPGraphQL\AppContext                $context The app context that gets passed down the resolve tree.
 	 * @param \GraphQL\Type\Definition\ResolveInfo $info    Info about fields passed down the resolve tree.
-	 *
-	 * @throws \Exception
 	 */
 	public function __construct( $source, array $args, AppContext $context, ResolveInfo $info ) {
 		// Set the source (the root object), context, resolveInfo, and unfiltered args for the resolver.
@@ -214,6 +229,11 @@ public function __construct( $source, array $args, AppContext $context, ResolveI
 		 * @param array<string,mixed>                                   $unfiltered_args     Array of arguments input in the field as part of the GraphQL query.
 		 */
 		$this->query_args = apply_filters( 'graphql_connection_query_args', $this->get_query_args(), $this, $this->get_unfiltered_args() );
+
+		// Get the query class for the connection.
+		$this->query_class = $this->get_query_class();
+
+		// The rest of the class properties are set when `$this->get_connection()` is called.
 	}
 
 	/**
@@ -239,12 +259,12 @@ protected function loader_name(): string {
 	 *
 	 * @return array<string,mixed>
 	 *
-	 * @throws \Exception If the method is not implemented.
+	 * @throws \GraphQL\Error\InvariantViolation If the method is not implemented.
 	 *
 	 * @codeCoverageIgnore
 	 */
 	protected function prepare_query_args( array $args ): array {
-		throw new Exception(
+		throw new InvariantViolation(
 			sprintf(
 				// translators: %s is the name of the connection resolver class.
 				esc_html__( 'Class %s does not implement a valid method `prepare_query_args()`.', 'wp-graphql' ),
@@ -253,19 +273,6 @@ protected function prepare_query_args( array $args ): array {
 		);
 	}
 
-	/**
-	 * Get_query
-	 *
-	 * The Query used to get items from the database (or even external datasource) are all
-	 * different.
-	 *
-	 * Each connection resolver should be responsible for defining the Query object that
-	 * is used to fetch items.
-	 *
-	 * @return mixed
-	 */
-	abstract public function get_query();
-
 	/**
 	 * Determine whether or not the the offset is valid, i.e the item corresponding to the offset exists.
 	 *
@@ -324,6 +331,58 @@ protected function max_query_amount(): int {
 		return 100;
 	}
 
+	/**
+	 * The default query class to use for the connection.
+	 *
+	 * Should return null if the resolver does not use a query class to fetch the data.
+	 *
+	 * @return ?class-string<TQueryClass>
+	 */
+	protected function query_class(): ?string {
+		return null;
+	}
+
+	/**
+	 * Validates the query class. Will be ignored if the Connection Resolver does not use a query class.
+	 *
+	 * By default this checks if the query class has a `query()` method. If the query class requires the `query()` method to be named something else (e.g. $query_class->get_results()` ) this method should be overloaded.
+	 *
+	 * @param string $query_class The query class to validate.
+	 */
+	protected function is_valid_query_class( string $query_class ): bool {
+		return method_exists( $query_class, 'query' );
+	}
+
+	/**
+	 * Executes the query and returns the results.
+	 *
+	 * Usually, the returned value is an instantiated `$query_class` (e.g. `WP_Query`), but it can be any collection of data. The `get_ids_from_query()` method will be used to extract the IDs from the returned value.
+	 *
+	 * If the resolver does not rely on a query class, this should be overloaded to return the data directly.
+	 *
+	 * @param array<string,mixed> $query_args The query args to use to query the data.
+	 *
+	 * @return TQueryClass
+	 *
+	 * @throws \GraphQL\Error\InvariantViolation If the query class is not valid.
+	 */
+	protected function query( array $query_args ) {
+		// If there is no query class, we need the child class to overload this method.
+		$query_class = $this->get_query_class();
+
+		if ( empty( $query_class ) ) {
+			throw new InvariantViolation(
+				sprintf(
+					// translators: %s is the name of the connection resolver class.
+					esc_html__( 'The %s class does not rely on a query class. Please define a `query()` method to return the data directly.', 'wp-graphql' ),
+					static::class
+				)
+			);
+		}
+
+		return new $query_class( $query_args );
+	}
+
 	/**
 	 * Return an array of ids from the query
 	 *
@@ -336,12 +395,12 @@ protected function max_query_amount(): int {
 	 *
 	 * @since 1.9.0
 	 *
-	 * @throws \Exception If child class forgot to implement this.
+	 * @throws \GraphQL\Error\InvariantViolation If child class forgot to implement this.
 	 *
 	 * @return int[]|string[] the array of IDs.
 	 */
 	public function get_ids_from_query() {
-		throw new Exception(
+		throw new InvariantViolation(
 			sprintf(
 				// translators: %s is the name of the connection resolver class.
 				esc_html__( 'Class %s does not implement a valid method `get_ids_from_query()`.', 'wp-graphql' ),
@@ -437,7 +496,7 @@ public function get_info(): ResolveInfo {
 	 *
 	 * @return string
 	 *
-	 * @throws \Exception
+	 * @throws \GraphQL\Error\InvariantViolation
 	 */
 	public function get_loader_name() {
 		// Only initialize the loader_name property once.
@@ -446,7 +505,7 @@ public function get_loader_name() {
 
 			// This is a b/c check because `loader_name()` is not abstract.
 			if ( empty( $name ) ) {
-				throw new \Exception(
+				throw new InvariantViolation(
 					sprintf(
 						// translators: %s is the name of the connection resolver class.
 						esc_html__( 'Class %s does not implement a valid method `loader_name()`.', 'wp-graphql' ),
@@ -466,7 +525,7 @@ public function get_loader_name() {
 
 			// Bail if the loader name is invalid.
 			if ( empty( $name ) || ! is_string( $name ) ) {
-				throw new \Exception( esc_html__( 'The Connection Resolver needs to define a loader name', 'wp-graphql' ) );
+				throw new InvariantViolation( esc_html__( 'The Connection Resolver needs to define a loader name', 'wp-graphql' ) );
 			}
 
 			$this->loader_name = $name;
@@ -505,7 +564,6 @@ public function get_args(): array {
 	 * If the amount requested is greater than the max query amount, a debug message will be included in the GraphQL response.
 	 *
 	 * @return int
-	 * @throws \Exception
 	 */
 	public function get_query_amount() {
 		if ( ! isset( $this->query_amount ) ) {
@@ -563,6 +621,36 @@ public function get_query_args() {
 		return $this->query_args;
 	}
 
+	/**
+	 * Gets the query class to be instantiated by the `query()` method.
+	 *
+	 * If null, the `query()` method will be overloaded to return the data.
+	 *
+	 * @return ?class-string<TQueryClass>
+	 */
+	public function get_query_class(): ?string {
+		if ( ! isset( $this->query_class ) ) {
+			$default_query_class = $this->query_class();
+
+			// Attempt to get the query class from the context.
+			$context = $this->get_context();
+
+			$query_class = ! empty( $context->queryClass ) ? $context->queryClass : $default_query_class;
+
+			/**
+			 * Filters the `$query_class` that will be used to execute the query.
+			 *
+			 * This is useful for replacing the default query (e.g `WP_Query` ) with a custom one (E.g. `WP_Term_Query` or WooCommerce's `WC_Query`).
+			 *
+			 * @param ?class-string<TQueryClass> $query_class The query class to be used with the executable query to get data. `null` if the AbstractConnectionResolver does not use a query class.
+			 * @param self        $resolver    Instance of the AbstractConnectionResolver
+			 */
+			$this->query_class = apply_filters( 'graphql_connection_query_class', $query_class, $this );
+		}
+
+		return $this->query_class;
+	}
+
 	/**
 	 * Returns whether the connection should execute.
 	 *
@@ -577,6 +665,35 @@ public function get_should_execute(): bool {
 		return $this->should_execute;
 	}
 
+	/**
+	 * Gets the results of the executed query.
+	 *
+	 * @return TQueryClass
+	 */
+	public function get_query() {
+		if ( ! isset( $this->query ) ) {
+			/**
+			 * When this filter returns anything but null, it will be used as the resolved query, and the default query execution will be skipped.
+			 *
+			 * @param null $query               The query to return. Return null to use the default query execution.
+			 * @param self $resolver The connection resolver instance.
+			 */
+			$query = apply_filters( 'graphql_connection_pre_get_query', null, $this );
+
+			if ( null === $query ) {
+
+				// Validates the query class before it is used in the query() method.
+				$this->validate_query_class();
+
+				$query = $this->query( $this->get_query_args() );
+			}
+
+			$this->query = $query;
+		}
+
+		return $this->query;
+	}
+
 	/**
 	 * Returns an array of IDs for the connection.
 	 *
@@ -599,8 +716,6 @@ public function get_ids() {
 	 * @uses AbstractConnectionResolver::get_ids_for_nodes()
 	 *
 	 * @return array<int|string,mixed|\WPGraphQL\Model\Model|null>
-	 *
-	 * @throws \Exception
 	 */
 	public function get_nodes() {
 		if ( ! isset( $this->nodes ) ) {
@@ -666,13 +781,20 @@ public function get_page_info() {
 	}
 
 	/**
-	 * Given a key and value, this sets a query_arg which will modify the query_args used by
-	 * the connection resolvers get_query();
+	 * ===============================
+	 * Public setters
+	 *
+	 * These are used to directly modify the instance properties from outside the class.
+	 * ===============================
+	 */
+
+	/**
+	 * Given a key and value, this sets a query_arg which will modify the query_args used by ::get_query();
 	 *
 	 * @param string $key   The key of the query arg to set
 	 * @param mixed  $value The value of the query arg to set
 	 *
-	 * @return self
+	 * @return static
 	 */
 	public function set_query_arg( $key, $value ) {
 		$this->query_args[ $key ] = $value;
@@ -680,10 +802,23 @@ public function set_query_arg( $key, $value ) {
 		return $this;
 	}
 
+	/**
+	 * Overloads the query_class which will be used to instantiate the query.
+	 *
+	 * @param class-string<TQueryClass> $query_class The class to use for the query. If empty, this will reset to the default query class.
+	 *
+	 * @return static
+	 */
+	public function set_query_class( string $query_class ) {
+		$this->query_class = $query_class ?: $this->query_class();
+
+		return $this;
+	}
+
 	/**
 	 * Whether the connection should resolve as a one-to-one connection.
 	 *
-	 * @return self
+	 * @return static
 	 */
 	public function one_to_one() {
 		$this->one_to_one = true;
@@ -789,12 +924,18 @@ public function get_amount_requested() {
 		return (int) $amount_requested;
 	}
 
+	/**
+	 * =====================
+	 * Resolver lifecycle methods
+	 *
+	 * These methods are used internally by the class to resolve the connection. They rarely should be overloaded by the child class, but if you do, make sure to preserve any WordPress hooks included in the parent method.
+	 * =====================
+	 */
+
 	/**
 	 * Get the connection to return to the Connection Resolver
 	 *
 	 * @return \GraphQL\Deferred
-	 *
-	 * @throws \Exception
 	 */
 	public function get_connection() {
 		$this->execute_and_get_ids();
@@ -864,8 +1005,6 @@ function () {
 	 * Execute the resolver query and get the data for the connection
 	 *
 	 * @return int[]|string[]
-	 *
-	 * @throws \Exception
 	 */
 	public function execute_and_get_ids() {
 		/**
@@ -904,6 +1043,8 @@ public function execute_and_get_ids() {
 		 * the query to that instead of a native WP_Query class. You could override this with a
 		 * query to that datasource instead.
 		 *
+		 *  @todo We reinstantate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_query_args().
+		 *
 		 * @param mixed                      $query               Instance of the Query for the resolver
 		 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the Connection Resolver
 		 */
@@ -931,6 +1072,68 @@ public function execute_and_get_ids() {
 		return $this->ids;
 	}
 
+	/**
+	 * Validates the $query_class set on the resolver.
+	 *
+	 * This runs before the query is executed to ensure that the query class is valid.
+	 *
+	 * @throws \GraphQL\Error\InvariantViolation If the query class is invalid.
+	 */
+	protected function validate_query_class(): void {
+		$default_query_class = $this->query_class();
+		$query_class         = $this->get_query_class();
+
+		// If the default query class is null, then the resolver should not use a query class.
+		if ( null === $default_query_class ) {
+			// If the query class is null, then we're good.
+			if ( null === $query_class ) {
+				return;
+			}
+
+			throw new InvariantViolation(
+				sprintf(
+					// translators: %1$s: The name of the class that should not use a query class. %2$s: The name of the query class that is set by the resolver.
+					esc_html__( 'Class %1$s should not use a query class, but is attempting to use the %2$s query class.', 'wp-graphql' ),
+					static::class,
+					esc_html( $query_class )
+				)
+			);
+		}
+
+		// If there's no query class set, throw an error.
+		if ( null === $query_class ) {
+			throw new InvariantViolation(
+				sprintf(
+					// translators: %s: The connection resolver class name.
+					esc_html__( '%s requires a query class, but no query class is set.', 'wp-graphql' ),
+					static::class
+				)
+			);
+		}
+
+		// If the class is invalid, throw an error.
+		if ( ! class_exists( $query_class ) ) {
+			throw new InvariantViolation(
+				sprintf(
+					// translators: %s: The name of the query class that is set by the resolver.
+					esc_html__( 'The query class %s does not exist.', 'wp-graphql' ),
+					esc_html( $query_class )
+				)
+			);
+		}
+
+		// If the class is not compatible with our AbstractConnectionResolver::query() method, throw an error.
+		if ( ! $this->is_valid_query_class( $query_class ) ) {
+			throw new InvariantViolation(
+				sprintf(
+					// translators: %1$s: The name of the query class that is set by the resolver. %2$s: The name of the resolver class.
+					esc_html__( 'The query class %1$s is not compatible with %2$s.', 'wp-graphql' ),
+					esc_html( $this->query_class ?? 'unknown-class' ),
+					static::class
+				)
+			);
+		}
+	}
 
 	/**
 	 * Returns an array slice of IDs, per the Relay Cursor Connection spec.
@@ -1057,7 +1260,6 @@ public function get_ids_for_nodes() {
 	 * @param int|string|mixed $id The ID to identify the object by. Could be a database ID or an in-memory ID (like post_type name)
 	 *
 	 * @return mixed|\WPGraphQL\Model\Model|null
-	 * @throws \Exception
 	 */
 	public function get_node_by_id( $id ) {
 		return $this->get_loader()->load( $id );
@@ -1280,7 +1482,7 @@ public function getArgs(): array {
 	 * @param string $key   The key of the query arg to set
 	 * @param mixed  $value The value of the query arg to set
 	 *
-	 * @return \WPGraphQL\Data\Connection\AbstractConnectionResolver
+	 * @return static
 	 *
 	 * @deprecated 0.3.0
 	 *
@@ -1366,7 +1568,6 @@ public function getShouldExecute(): bool {
 	 * @deprecated 1.24.0 in favor of $this->get_loader().
 	 *
 	 * @return \WPGraphQL\Data\Loader\AbstractDataLoader
-	 * @throws \Exception
 	 */
 	protected function getLoader() {
 		_deprecated_function( __METHOD__, '1.24.0', static::class . '::get_loader()' );
diff --git a/src/Data/Connection/CommentConnectionResolver.php b/src/Data/Connection/CommentConnectionResolver.php
index a9a28f8d5..14d272f6a 100644
--- a/src/Data/Connection/CommentConnectionResolver.php
+++ b/src/Data/Connection/CommentConnectionResolver.php
@@ -4,20 +4,14 @@
 
 use GraphQL\Error\UserError;
 use WPGraphQL\Utils\Utils;
-use WP_Comment_Query;
 
 /**
  * Class CommentConnectionResolver
  *
  * @package WPGraphQL\Data\Connection
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_Comment_Query>
  */
 class CommentConnectionResolver extends AbstractConnectionResolver {
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @var \WP_Comment_Query
-	 */
-	protected $query;
 
 	/**
 	 * {@inheritDoc}
@@ -129,7 +123,7 @@ protected function prepare_query_args( array $args ): array {
 		$query_args['fields'] = 'ids';
 
 		/**
-		 * Filter the query_args that should be applied to the query.
+		 * Filters the query args used by the connection.
 		 *
 		 * @param array<string,mixed>                  $query_args array of query_args being passed to the
 		 * @param mixed                                $source     source passed down from the resolve tree
@@ -144,12 +138,9 @@ protected function prepare_query_args( array $args ): array {
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return \WP_Comment_Query
-	 * @throws \Exception
 	 */
-	public function get_query() {
-		return new WP_Comment_Query( $this->get_query_args() );
+	protected function query_class(): string {
+		return \WP_Comment_Query::class;
 	}
 
 	/**
@@ -163,8 +154,14 @@ protected function loader_name(): string {
 	 * {@inheritDoc}
 	 */
 	public function get_ids_from_query() {
+		/**
+		 * @todo This is for b/c. We can just use $this->get_query().
+		 */
+		$queried  = isset( $this->query ) ? $this->query : $this->get_query();
+		$comments = $queried->get_comments();
+
 		/** @var int[]|string[] $ids */
-		$ids = ! empty( $this->query->get_comments() ) ? $this->query->get_comments() : [];
+		$ids = ! empty( $comments ) ? $comments : [];
 
 		// If we're going backwards, we need to reverse the array.
 		$args = $this->get_args();
diff --git a/src/Data/Connection/ContentTypeConnectionResolver.php b/src/Data/Connection/ContentTypeConnectionResolver.php
index b0eabfd46..53bd88108 100644
--- a/src/Data/Connection/ContentTypeConnectionResolver.php
+++ b/src/Data/Connection/ContentTypeConnectionResolver.php
@@ -5,15 +5,9 @@
  * Class ContentTypeConnectionResolver
  *
  * @package WPGraphQL\Data\Connection
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<string[]>
  */
 class ContentTypeConnectionResolver extends AbstractConnectionResolver {
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @var string[]
-	 */
-	protected $query;
-
 	/**
 	 * {@inheritDoc}
 	 */
@@ -42,12 +36,8 @@ protected function prepare_query_args( array $args ): array {
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return string[]
 	 */
-	public function get_query() {
-		$query_args = $this->get_query_args();
-
+	protected function query( array $query_args ) {
 		if ( isset( $query_args['contentTypeNames'] ) && is_array( $query_args['contentTypeNames'] ) ) {
 			return $query_args['contentTypeNames'];
 		}
diff --git a/src/Data/Connection/EnqueuedScriptsConnectionResolver.php b/src/Data/Connection/EnqueuedScriptsConnectionResolver.php
index 84ad2d565..096216462 100644
--- a/src/Data/Connection/EnqueuedScriptsConnectionResolver.php
+++ b/src/Data/Connection/EnqueuedScriptsConnectionResolver.php
@@ -5,6 +5,7 @@
  * Class EnqueuedScriptsConnectionResolver
  *
  * @package WPGraphQL\Data\Connection
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<string[]>
  */
 class EnqueuedScriptsConnectionResolver extends AbstractConnectionResolver {
 	/**
@@ -35,10 +36,8 @@ protected function prepare_query_args( array $args ): array {
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return string[]
 	 */
-	public function get_query() {
+	protected function query( array $query_args ) {
 		return $this->source->enqueuedScriptsQueue ? $this->source->enqueuedScriptsQueue : [];
 	}
 
diff --git a/src/Data/Connection/EnqueuedStylesheetConnectionResolver.php b/src/Data/Connection/EnqueuedStylesheetConnectionResolver.php
index 11aa6be28..cba600f96 100644
--- a/src/Data/Connection/EnqueuedStylesheetConnectionResolver.php
+++ b/src/Data/Connection/EnqueuedStylesheetConnectionResolver.php
@@ -5,15 +5,9 @@
  * Class EnqueuedStylesheetConnectionResolver
  *
  * @package WPGraphQL\Data\Connection
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<string[]>
  */
 class EnqueuedStylesheetConnectionResolver extends AbstractConnectionResolver {
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @var string[]
-	 */
-	protected $query;
-
 	/**
 	 * {@inheritDoc}
 	 */
@@ -42,10 +36,8 @@ protected function prepare_query_args( array $args ): array {
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return string[]
 	 */
-	public function get_query() {
+	protected function query( array $query_args ) {
 		return $this->source->enqueuedStylesheetsQueue ? $this->source->enqueuedStylesheetsQueue : [];
 	}
 
diff --git a/src/Data/Connection/PluginConnectionResolver.php b/src/Data/Connection/PluginConnectionResolver.php
index 8c49fa422..78adbea03 100644
--- a/src/Data/Connection/PluginConnectionResolver.php
+++ b/src/Data/Connection/PluginConnectionResolver.php
@@ -6,14 +6,9 @@
  *
  * @package WPGraphQL\Data\Connection
  * @since 0.0.5
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<array<string,array<string,mixed>>>
  */
 class PluginConnectionResolver extends AbstractConnectionResolver {
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @var array<string,array<string,mixed>>
-	 */
-	protected $query;
 
 	/**
 	 * A list of all the installed plugins, keyed by their type.
@@ -58,7 +53,7 @@ protected function prepare_query_args( array $args ): array {
 	 *
 	 * @return array<string,array<string,mixed>>
 	 */
-	public function get_query() {
+	protected function query( array $query_args ) {
 		// Get all plugins.
 		$plugins = $this->get_all_plugins();
 
@@ -69,9 +64,6 @@ public function get_query() {
 			return [];
 		}
 
-		// Get the GraphQL args for later.
-		$query_args = $this->get_query_args();
-
 		// Holds the plugin names sorted by status. The other ` status =>  [ plugin_names ] ` will be added later.
 		$plugins_by_status = [
 			'mustuse' => array_flip( array_keys( $plugins['mustuse'] ) ),
diff --git a/src/Data/Connection/PostObjectConnectionResolver.php b/src/Data/Connection/PostObjectConnectionResolver.php
index 45deacb58..36d80fcb3 100644
--- a/src/Data/Connection/PostObjectConnectionResolver.php
+++ b/src/Data/Connection/PostObjectConnectionResolver.php
@@ -12,6 +12,7 @@
  * Class PostObjectConnectionResolver
  *
  * @package WPGraphQL\Data\Connection
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_Query>
  */
 class PostObjectConnectionResolver extends AbstractConnectionResolver {
 
@@ -22,13 +23,6 @@ class PostObjectConnectionResolver extends AbstractConnectionResolver {
 	 */
 	protected $post_type;
 
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @var \WP_Query|object
-	 */
-	protected $query;
-
 	/**
 	 * {@inheritDoc}
 	 *
@@ -75,18 +69,18 @@ protected function loader_name(): string {
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return \WP_Query|object
+	 */
+	protected function query_class(): string {
+		return \WP_Query::class;
+	}
+
+	/**
+	 * {@inheritDoc}
 	 *
 	 * @throws \GraphQL\Error\InvariantViolation If the query has been modified to suppress_filters.
 	 */
-	public function get_query() {
-		// Get query class.
-		$queryClass = ! empty( $this->context->queryClass )
-			? $this->context->queryClass
-			: '\WP_Query';
-
-		$query = new $queryClass( $this->query_args );
+	protected function query( array $query_args ) {
+		$query = parent::query( $query_args );
 
 		if ( isset( $query->query_vars['suppress_filters'] ) && true === $query->query_vars['suppress_filters'] ) {
 			throw new InvariantViolation( esc_html__( 'WP_Query has been modified by a plugin or theme to suppress_filters, which will cause issues with WPGraphQL Execution. If you need to suppress filters for a specific reason within GraphQL, consider registering a custom field to the WPGraphQL Schema with a custom resolver.', 'wp-graphql' ) );
@@ -99,7 +93,13 @@ public function get_query() {
 	 * {@inheritDoc}
 	 */
 	public function get_ids_from_query() {
-		$ids = ! empty( $this->query->posts ) ? $this->query->posts : [];
+		/**
+		 * @todo This is for b/c. We can just use $this->get_query().
+		 */
+		$query = isset( $this->query ) ? $this->query : $this->get_query();
+
+		/** @var int[] */
+		$ids = ! empty( $query->posts ) ? $query->posts : [];
 
 		// If we're going backwards, we need to reverse the array.
 		$args = $this->get_args();
diff --git a/src/Data/Connection/TaxonomyConnectionResolver.php b/src/Data/Connection/TaxonomyConnectionResolver.php
index fc03baf7e..73ed25c58 100644
--- a/src/Data/Connection/TaxonomyConnectionResolver.php
+++ b/src/Data/Connection/TaxonomyConnectionResolver.php
@@ -5,21 +5,20 @@
  * Class TaxonomyConnectionResolver
  *
  * @package WPGraphQL\Data\Connection
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<string[]>
  */
 class TaxonomyConnectionResolver extends AbstractConnectionResolver {
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @var string[]
-	 */
-	protected $query;
 
 	/**
 	 * {@inheritDoc}
 	 */
 	public function get_ids_from_query() {
-		$ids     = [];
-		$queried = $this->query;
+		/**
+		 * @todo This is for b/c. We can just use $this->get_query().
+		 */
+		$queried = isset( $this->query ) ? $this->query : $this->get_query();
+
+		$ids = [];
 
 		if ( empty( $queried ) ) {
 			return $ids;
@@ -42,12 +41,8 @@ protected function prepare_query_args( array $args ): array {
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return string[]
 	 */
-	public function get_query() {
-		$query_args = $this->get_query_args();
-
+	protected function query( array $query_args ) {
 		if ( isset( $query_args['name'] ) ) {
 			return [ $query_args['name'] ];
 		}
diff --git a/src/Data/Connection/TermObjectConnectionResolver.php b/src/Data/Connection/TermObjectConnectionResolver.php
index f89ee9d6c..397de4302 100644
--- a/src/Data/Connection/TermObjectConnectionResolver.php
+++ b/src/Data/Connection/TermObjectConnectionResolver.php
@@ -10,16 +10,9 @@
  * Class TermObjectConnectionResolver
  *
  * @package WPGraphQL\Data\Connection
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_Term_Query>
  */
 class TermObjectConnectionResolver extends AbstractConnectionResolver {
-
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @var \WP_Term_Query
-	 */
-	protected $query;
-
 	/**
 	 * The name of the Taxonomy the resolver is intended to be used for
 	 *
@@ -145,21 +138,23 @@ protected function prepare_query_args( array $args ): array {
 	}
 
 	/**
-	 * Return an instance of WP_Term_Query with the args mapped to the query
-	 *
-	 * @return \WP_Term_Query
-	 * @throws \Exception
+	 * {@inheritDoc}
 	 */
-	public function get_query() {
-		return new \WP_Term_Query( $this->get_query_args() );
+	protected function query_class(): string {
+		return \WP_Term_Query::class;
 	}
 
 	/**
 	 * {@inheritDoc}
 	 */
 	public function get_ids_from_query() {
+		/**
+		 * @todo This is for b/c. We can just use $this->get_query().
+		 */
+		$queried = isset( $this->query ) ? $this->query : $this->get_query();
+
 		/** @var string[] $ids */
-		$ids = ! empty( $this->query->get_terms() ) ? $this->query->get_terms() : [];
+		$ids = ! empty( $queried->get_terms() ) ? $queried->get_terms() : [];
 
 		// If we're going backwards, we need to reverse the array.
 		$args = $this->get_args();
diff --git a/src/Data/Connection/ThemeConnectionResolver.php b/src/Data/Connection/ThemeConnectionResolver.php
index e9ee74069..74168c4d7 100644
--- a/src/Data/Connection/ThemeConnectionResolver.php
+++ b/src/Data/Connection/ThemeConnectionResolver.php
@@ -6,21 +6,19 @@
  *
  * @package WPGraphQL\Data\Resolvers
  * @since 0.5.0
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<string[]>
  */
 class ThemeConnectionResolver extends AbstractConnectionResolver {
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @var string[]
-	 */
-	protected $query;
-
 	/**
 	 * {@inheritDoc}
 	 */
 	public function get_ids_from_query() {
-		$ids     = [];
-		$queried = ! empty( $this->query ) ? $this->query : [];
+		/**
+		 * @todo This is for b/c. We can just use $this->get_query().
+		 */
+		$queried = isset( $this->query ) ? $this->query : $this->get_query();
+
+		$ids = [];
 
 		if ( empty( $queried ) ) {
 			return $ids;
@@ -44,12 +42,8 @@ protected function prepare_query_args( array $args ): array {
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return string[]
 	 */
-	public function get_query() {
-		$query_args = $this->get_query_args();
-
+	protected function query( array $query_args ) {
 		return array_keys( wp_get_themes( $query_args ) );
 	}
 
diff --git a/src/Data/Connection/UserConnectionResolver.php b/src/Data/Connection/UserConnectionResolver.php
index e437b3114..eff3d2b06 100644
--- a/src/Data/Connection/UserConnectionResolver.php
+++ b/src/Data/Connection/UserConnectionResolver.php
@@ -9,17 +9,9 @@
  * Class UserConnectionResolver
  *
  * @package WPGraphQL\Data\Connection
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_User_Query>
  */
 class UserConnectionResolver extends AbstractConnectionResolver {
-	/**
-	 * {@inheritDoc}
-	 *
-	 * A custom class is assumed to have the same core functions as WP_User_Query.
-	 *
-	 * @var \WP_User_Query|object
-	 */
-	protected $query;
-
 	/**
 	 * {@inheritDoc}
 	 */
@@ -179,26 +171,22 @@ protected function prepare_query_args( array $args ): array {
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return object|\WP_User_Query
-	 * @throws \Exception
 	 */
-	public function get_query() {
-		// Get query class.
-		$queryClass = ! empty( $this->context->queryClass )
-			? $this->context->queryClass
-			: '\WP_User_Query';
-
-		return new $queryClass( $this->query_args );
+	protected function query_class(): string {
+		return \WP_User_Query::class;
 	}
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return int[]
 	 */
 	public function get_ids_from_query() {
-		$ids = method_exists( $this->query, 'get_results' ) ? $this->query->get_results() : [];
+		/**
+		 * @todo This is for b/c. We can just use $this->get_query().
+		 */
+		$queried = isset( $this->query ) ? $this->query : $this->get_query();
+
+		/** @var int[] $ids */
+		$ids = $queried->get_results();
 
 		// If we're going backwards, we need to reverse the array.
 		$args = $this->get_args();
diff --git a/src/Data/Connection/UserRoleConnectionResolver.php b/src/Data/Connection/UserRoleConnectionResolver.php
index 7490e2519..6168eb5c4 100644
--- a/src/Data/Connection/UserRoleConnectionResolver.php
+++ b/src/Data/Connection/UserRoleConnectionResolver.php
@@ -9,15 +9,9 @@
  *
  * @package WPGraphQL\Data\Resolvers
  * @since   0.0.5
+ * @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<string[]>
  */
 class UserRoleConnectionResolver extends AbstractConnectionResolver {
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @var string[]
-	 */
-	protected $query;
-
 	/**
 	 * {@inheritDoc}
 	 */
@@ -53,10 +47,8 @@ protected function prepare_query_args( array $args ): array {
 
 	/**
 	 * {@inheritDoc}
-	 *
-	 * @return string[]
 	 */
-	public function get_query() {
+	protected function query( array $query_args ) {
 		$wp_roles = wp_roles();
 
 		return ! empty( $wp_roles->get_names() ) ? array_keys( $wp_roles->get_names() ) : [];


Test patch:
diff --git a/tests/_data/classes/WP_Query_Custom.php b/tests/_data/classes/WP_Query_Custom.php
new file mode 100644
index 000000000..c85800fed
--- /dev/null
+++ b/tests/_data/classes/WP_Query_Custom.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * WP_Query clone for testing
+ *
+ * @package Test\WPGraphQL
+ */
+
+/**
+ * Class WP_Query_Custom
+ */
+class WP_Query_Custom {
+	/**
+	 * Stores query results.
+	 *
+	 * @var \WP_Query
+	 */
+	protected $query;
+
+	/**
+	 * WP_Query_Custom constructor.
+	 *
+	 * @param array $args   Query Arguments.
+	 */
+	public function __construct( $args = [] ) {
+		$this->query = new \WP_Query(
+			array_merge(
+				$args,
+				[ 'posts_per_page' => 3 ]
+			)
+		);
+	}
+
+	/**
+	 * Returns the query.
+	 * 
+	 * (This is what AbstractConnectionResolver::is_valid_query_class() checks for by default.)
+	 */
+	public function query( $args = [] ) {
+		return $this->query;
+	}
+
+		/**
+	 * Magic method to re-map the isset check on the child class looking for properties when
+	 * resolving the fields
+	 *
+	 * @param string $key The name of the field you are trying to retrieve
+	 *
+	 * @return bool
+	 */
+	public function __isset( $key ) {
+		return isset( $this->query->$key );
+	}
+
+	/**
+	 * Pass thru for query instance.
+	 *
+	 * @param string $name  WP_Query_Custom member name.
+	 *
+	 * @return mixed
+	 */
+	public function __get( $name ) {
+		return $this->query->$name;
+	}
+
+
+		/**
+	 * Forwards function calls to WP_Query instance.
+	 *
+	 * @param string $method - function name.
+	 * @param array $args   - function call arguments.
+	 *
+	 * @return mixed
+	 *
+	 * @throws \BadMethodCallException Method not found on WP_Query object.
+	 */
+	public function __call( $method, $args ) {
+		if ( \is_callable( [ $this->query, $method ] ) ) {
+			return $this->query->$method( ...$args );
+		}
+
+		$class = __CLASS__;
+		throw new BadMethodCallException( "Call to undefined method {$method} on the {$class}" );
+	}
+}
diff --git a/tests/_data/classes/WP_Query_Incompatible.php b/tests/_data/classes/WP_Query_Incompatible.php
new file mode 100644
index 000000000..70b7c5df3
--- /dev/null
+++ b/tests/_data/classes/WP_Query_Incompatible.php
@@ -0,0 +1,11 @@
+<?php
+/**
+ * An Incompatible WP_Query clone for testing
+ *
+ * @package Test\WPGraphQL
+ */
+
+/**
+ * Class WP_Query_Incompatible
+ */
+class WP_Query_Incompatible {}
diff --git a/tests/wpunit/ConnectionRegistrationTest.php b/tests/wpunit/ConnectionRegistrationTest.php
index 3e1a05a21..25fed81bc 100644
--- a/tests/wpunit/ConnectionRegistrationTest.php
+++ b/tests/wpunit/ConnectionRegistrationTest.php
@@ -1,5 +1,8 @@
 <?php
 
+use WPGraphQL\Data\Connection\ContentTypeConnectionResolver;
+use WPGraphQL\Data\Connection\PostObjectConnectionResolver;
+
 class ConnectionRegistrationTest extends \Tests\WPGraphQL\TestCase\WPGraphQLTestCase {
 	public $connection_config;
 
@@ -767,6 +770,193 @@ static function ( $result, $source, $args, $context, \GraphQL\Type\Definition\Re
 		);
 	}
 
+
+	public function testWithSetQueryClassWhenNotSupported() : void {
+		$config = [
+			'fromType'      => 'RootQuery',
+			'toType'        => 'ContentType',
+			'fromFieldName' => 'testConnection',
+			'resolve'       => function ( $source, $args, $context, $info ) {
+				$resolver = new ContentTypeConnectionResolver( $source, $args, $context, $info );
+
+				$resolver->set_query_class( 'WP_Query' );
+
+				return $resolver->get_connection();
+			},
+		];
+
+		register_graphql_connection( $config );
+
+		$query = '
+			query {
+				testConnection {
+					nodes {
+						id
+					}
+				}
+			}
+		';
+
+		$actual = $this->graphql( compact( 'query' ) );
+
+		$this->assertArrayHasKey( 'errors', $actual );
+		$this->assertStringEndsWith( 'should not use a query class, but is attempting to use the WP_Query query class.', $actual['errors'][0]['debugMessage'] );
+	}
+
+	public function testWithCustomSetQueryClass() : void {
+		require_once __DIR__ . '/../_data/classes/WP_Query_Custom.php';
+
+		$post_ids = $this->factory()->post->create_many( 2 );
+
+		$query = '
+			query {
+				testConnection {
+					nodes {
+						__typename
+					}
+				}
+			}
+		';
+
+		// Test that empty query class still resolves
+		$config = [
+			'fromType'           => 'RootQuery',
+			'toType'             => 'Post',
+			'fromFieldName'      => 'testConnection',
+			'connectionTypeName' => 'CustomQueryClassConnection',
+			'resolve'            => function ( $source, $args, $context, $info ) {
+				$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info );
+
+				$resolver->set_query_class( WP_Query_Custom::class );
+
+				return $resolver->get_connection();
+			},
+		];
+
+		register_graphql_connection( $config );
+
+		$actual = $this->graphql( compact( 'query' ) );
+
+		$this->assertArrayNotHasKey( 'errors', $actual );
+		$this->assertCount( 2, $actual['data']['testConnection']['nodes'] );
+
+
+		// cleanup
+		foreach ( $post_ids as $post_id ) {
+			wp_delete_post( $post_id, true );
+		}
+	}
+
+	public function testWithEmptySetQueryClass() : void {
+		$post_ids = $this->factory()->post->create_many( 2 );
+
+		$query = '
+			query {
+				testConnection {
+					nodes {
+						__typename
+					}
+				}
+			}
+		';
+
+		// Test that empty query class still resolves
+		$config = [
+			'fromType'           => 'RootQuery',
+			'toType'             => 'Post',
+			'fromFieldName'      => 'testConnection',
+			'connectionTypeName' => 'EmptyQueryClassConnection',
+			'resolve'            => function ( $source, $args, $context, $info ) {
+				$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info );
+
+				$resolver->set_query_class( '' );
+
+				return $resolver->get_connection();
+			},
+		];
+
+		register_graphql_connection( $config );
+
+		$actual = $this->graphql( compact( 'query' ) );
+
+		$this->assertArrayNotHasKey( 'errors', $actual );
+		$this->assertCount( 2, $actual['data']['testConnection']['nodes'] );
+
+
+		// cleanup
+		foreach ( $post_ids as $post_id ) {
+			wp_delete_post( $post_id, true );
+		}
+	}
+
+	public function testWithNonExistentSetQueryClass() : void {
+		$query = '
+			query {
+				testConnection {
+					nodes {
+						__typename
+					}
+				}
+			}
+		';
+
+		$config = [
+			'fromType'           => 'RootQuery',
+			'toType'             => 'Post',
+			'fromFieldName'      => 'testConnection',
+			'connectionTypeName' => 'NonExistentQueryClassConnection',
+			'resolve'            => function ( $source, $args, $context, $info ) {
+				$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info );
+
+				$resolver->set_query_class( 'NonExistentQueryClass' );
+
+				return $resolver->get_connection();
+			},
+		];
+
+		register_graphql_connection( $config );
+
+		$actual = $this->graphql( compact( 'query' ) );
+
+		$this->assertArrayHasKey( 'errors', $actual );
+		$this->assertEquals( 'The query class NonExistentQueryClass does not exist.', $actual['errors'][0]['debugMessage'] );
+	}
+
+	public function testWithIncompatibleSetQueryClass() : void {
+		require_once __DIR__ . '/../_data/classes/WP_Query_Incompatible.php';
+
+		$query = '
+			query {
+				testConnection {
+					nodes {
+						__typename
+					}
+				}
+			}
+		';
+
+		$config = [
+			'fromType'           => 'RootQuery',
+			'toType'             => 'Post',
+			'fromFieldName'      => 'testConnection',
+			'connectionTypeName' => 'NonExistentQueryClassConnection',
+			'resolve'            => function ( $source, $args, $context, $info ) {
+				$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info );
+
+				$resolver->set_query_class( WP_Query_Incompatible::class );
+
+				return $resolver->get_connection();
+			},
+		];
+
+		register_graphql_connection( $config );
+
+		$actual = $this->graphql( compact( 'query' ) );
+
+		$this->assertArrayHasKey( 'errors', $actual );
+		$this->assertStringStartsWith( 'The query class WP_Query_Incompatible is not compatible with', $actual['errors'][0]['debugMessage'] );
+	}
+
 	protected function assertValidTypes( $actual ): void {
 		$this->assertEquals( 'RootQueryToTestObjectConnection', $actual['data']['testConnection']['__typename'] );
 		$this->assertEquals( 'RootQueryToTestObjectConnectionEdge', $actual['data']['testConnection']['edges'][0]['__typename'] );
diff --git a/tests/wpunit/PostObjectCursorTest.php b/tests/wpunit/PostObjectCursorTest.php
index 38cc77287..849b21978 100644
--- a/tests/wpunit/PostObjectCursorTest.php
+++ b/tests/wpunit/PostObjectCursorTest.php
@@ -1264,8 +1264,6 @@ static function ( $values ) {
 			'graphql_post_object_connection_query_args',
 			static function ( $query_args, $source, $input ) {
 
-				global $wpdb;
-
 				if ( isset( $input['where']['orderby'] ) && is_array( $input['where']['orderby'] ) ) {
 					$new_orderby = isset( $query_args['orderby'] ) ? $query_args['orderby'] : [];