Repository: apollographql/apollo-rs
Pull request #944 (2025-01-13 08:31:55)

Reduce scope of execution / introspection APIs
The `execution` module was sort of aiming for comprehensive handling of GraphQL execution. For example, `execution::Response` was able to represent any possible GraphQL response including `extensions` `enum ResponseData { Absent, Null, Object(JsonMap) }`. However a real-world GraphQL server is unlikely to be satisfied with using that external type as its only representation of responses. In practice Apollo Router has a struct that carries additional data not serialized to JSON.

This redesigns the public API and reduce it to what’s actually needed to support the main functionality we want to provide: introspection.

Additionally, the introspection "splitting" functionality is removed, as Apollo Router doesn’t use it and I don’t have great confidence it’s correct. Splitting a document into two `Valid<ExecutableDocument>`s requires keeping track of which variables and fragments are used where, since producing a document with unused definitions would make it invalid.
This is tricky to get right, and not particularly useful. Instead we now have introspection execution take the original unmodified document and ignore the fields it doesn’t know how to execute. It’s up to the caller to execute those fields separately and merge two partial responses.

New public API below, replacing `apollo_compiler::execution`. See doc-comments in the PR diff.

`apollo_compiler::request` module:

```rust
pub fn coerce_variable_values(
    schema: &Valid<Schema>,
    operation: &Operation,
    values: &JsonMap,
) -> Result<Valid<JsonMap>, RequestError> {…}

#[derive(Debug, Clone)]
pub struct RequestError {…}

impl RequestError {
    // Should this return a plain `&str`?
    // It’s a `String` underneath atm but we may want something more structured later
    pub fn message(&self) -> impl Display + '_ {…}
    pub fn location(&self) -> Option<SourceSpan> {…}
    pub fn to_graphql_error(&self, sources: &SourceMap) -> GraphQLError {…}
}
```

`apollo_compiler::response` module:

```rust
pub use serde_json_bytes;
pub type JsonValue = serde_json_bytes::Value;
pub type JsonMap = serde_json_bytes::Map<serde_json_bytes::ByteString, JsonValue>;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExecutionResponse {
    pub errors: Vec<GraphQLError>,
    // Intentionally cannot represent request error (absent `data`), `Result` is used instead:
    pub data: Option<JsonMap>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphQLError {
    pub message: String,
    pub locations: Vec<LineColumn>,
    pub path: Vec<ResponseDataPathSegment>,
    // Set to `{"APOLLO_SUSPECTED_VALIDATION_BUG": true}` for cases that "should never happen" in valid documents
    pub extensions: JsonMap,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ResponseDataPathSegment {
    Field(crate::Name),
    ListIndex(usize),
}

impl GraphQLError {
    pub fn new(
        message: impl Into<String>,
        location: Option<SourceSpan>,
        sources: &SourceMap,
    ) -> Self {…}
}
```

`apollo_compiler::introspection` module:

```rust
pub fn check_max_depth(
    document: &Valid<ExecutableDocument>,
    operation: &Operation,
) -> Result<(), RequestError> {…}

pub fn partial_execute(
    schema: &Valid<Schema>,
    implementers_map: &HashMap<Name, Implementers>,
    document: &Valid<ExecutableDocument>,
    operation: &Operation,
    variable_values: &Valid<JsonMap>,
) -> Result<ExecutionResponse, RequestError> {…}
```

Fixes https://github.com/apollographql/apollo-rs/issues/937
<!-- ROUTER-956 -->

SimonSapin: Last second breaking change in a separate commit: use the new more generic `RequestError` error type in `OperationMap::{get,get_mut}`, instead of a dedicated error struct. What do you think?
lrlna: >Last second breaking change in a separate commit: use the new more generic RequestError error type in OperationMap::{get,get_mut}, instead of a dedicated error struct. What do you think?

++, I like having the RequestError. It seems more aligned to the spec. In the future, if we want, we can can have a RequestError enum to encapsulate a specific operation name error. 

Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index 527971f..b2a5e2f 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 ## BREAKING
 
+- **Reduce scope of execution / introspection APIs - [SimonSapin], [pull/944]**
 - **Move `apollo_compiler::schema::ArgumentByNameError` into `apollo_compiler::ast` - [SimonSapin], [pull/942]**
 
 ## Features
@@ -41,6 +42,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 [pull/925]: https://github.com/apollographql/apollo-rs/issues/925
 [pull/929]: https://github.com/apollographql/apollo-rs/pull/929
 [pull/942]: https://github.com/apollographql/apollo-rs/pull/942
+[pull/944]: https://github.com/apollographql/apollo-rs/pull/944
 
 
 # [1.0.0-beta.24](https://crates.io/crates/apollo-compiler/1.0.0-beta.24) - 2024-09-24
diff --git a/crates/apollo-compiler/examples/introspect.rs b/crates/apollo-compiler/examples/introspect.rs
index b73b4ae..175e72a 100644
--- a/crates/apollo-compiler/examples/introspect.rs
+++ b/crates/apollo-compiler/examples/introspect.rs
@@ -1,4 +1,4 @@
-use apollo_compiler::execution::SchemaIntrospectionQuery;
+use apollo_compiler::introspection;
 use apollo_compiler::validation::Valid;
 use apollo_compiler::ExecutableDocument;
 use apollo_compiler::Schema;
@@ -20,7 +20,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
         return Err("Provide a query to execute".into());
     };
 
-    let query = if filename.starts_with('{') {
+    let doc = if filename.starts_with('{') {
         ExecutableDocument::parse_and_validate(&schema, filename, "input.graphql")
     } else {
         ExecutableDocument::parse_and_validate(
@@ -31,17 +31,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     }
     .map_err(|err| err.errors.to_string())?;
 
-    let variables = Default::default();
-    let response = SchemaIntrospectionQuery::split_and_execute(
+    let response = introspection::partial_execute(
         &schema,
-        &query,
-        query
-            .operations
+        &schema.implementers_map(),
+        &doc,
+        doc.operations
             .get(None)
-            .map_err(|_| "Provided query must be an anonymous introspection query")?,
-        Valid::assume_valid_ref(&variables),
-        |_| panic!("Provided query must not have non-introspection elements"),
-    );
+            .map_err(|_| "Must have exactly one operation")?,
+        Valid::assume_valid_ref(&Default::default()),
+    )
+    .map_err(|e| e.message().to_string())?;
 
     serde_json::to_writer_pretty(std::io::stdout().lock(), &response)?;
 
diff --git a/crates/apollo-compiler/src/ast/impls.rs b/crates/apollo-compiler/src/ast/impls.rs
index 65e22d8..f28ec4e 100644
--- a/crates/apollo-compiler/src/ast/impls.rs
+++ b/crates/apollo-compiler/src/ast/impls.rs
@@ -713,6 +713,18 @@ impl OperationType {
         }
     }
 
+    pub fn is_query(&self) -> bool {
+        matches!(self, Self::Query)
+    }
+
+    pub fn is_mutation(&self) -> bool {
+        matches!(self, Self::Mutation)
+    }
+
+    pub fn is_subscription(&self) -> bool {
+        matches!(self, Self::Subscription)
+    }
+
     serialize_method!();
 }
 
diff --git a/crates/apollo-compiler/src/diagnostic.rs b/crates/apollo-compiler/src/diagnostic.rs
index a663cac..9ed4c5c 100644
--- a/crates/apollo-compiler/src/diagnostic.rs
+++ b/crates/apollo-compiler/src/diagnostic.rs
@@ -67,12 +67,12 @@
 //!     }
 //! }
 //! ```
-use crate::execution::GraphQLError;
 use crate::parser::FileId;
 use crate::parser::LineColumn;
 use crate::parser::SourceFile;
 use crate::parser::SourceMap;
 use crate::parser::SourceSpan;
+use crate::response::GraphQLError;
 #[cfg(doc)]
 use crate::ExecutableDocument;
 #[cfg(doc)]
diff --git a/crates/apollo-compiler/src/executable/mod.rs b/crates/apollo-compiler/src/executable/mod.rs
index a3584dd..d5cda77 100644
--- a/crates/apollo-compiler/src/executable/mod.rs
+++ b/crates/apollo-compiler/src/executable/mod.rs
@@ -79,6 +79,7 @@ pub use crate::ast::OperationType;
 pub use crate::ast::Type;
 pub use crate::ast::Value;
 pub use crate::ast::VariableDefinition;
+use crate::request::RequestError;
 pub use crate::Name;
 
 /// Executable definitions, annotated with type information
@@ -332,18 +333,6 @@ pub(crate) enum ExecutableDefinitionName {
     Fragment(Name),
 }
 
-/// A request error returned by [`OperationMap::get`]
-///
-/// If `get_operation`’s `name_request` argument was `Some`, this error indicates
-/// that the document does not contain an operation with the requested name.
-///
-/// If `name_request` was `None`, the request is ambiguous
-/// because the document contains multiple operations
-/// (or zero, though the document would be invalid in that case).
-#[derive(Debug, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct GetOperationError();
-
 impl ExecutableDocument {
     /// Create an empty document, to be filled programatically
     pub fn new() -> Self {
@@ -432,7 +421,7 @@ impl OperationMap {
 
     /// Return the relevant operation for a request, or a request error
     ///
-    /// This the [GetOperation()](https://spec.graphql.org/October2021/#GetOperation())
+    /// This is the [_GetOperation()_](https://spec.graphql.org/October2021/#GetOperation())
     /// algorithm in the _Executing Requests_ section of the specification.
     ///
     /// A GraphQL request comes with a document (which may contain multiple operations)
@@ -440,44 +429,65 @@ impl OperationMap {
     /// with that name, which is expected to exist. When it is not given / null / `None`,
     /// the document is expected to contain a single operation (which may or may not be named)
     /// to avoid ambiguity.
-    pub fn get(&self, name_request: Option<&str>) -> Result<&Node<Operation>, GetOperationError> {
+    pub fn get(&self, name_request: Option<&str>) -> Result<&Node<Operation>, RequestError> {
         if let Some(name) = name_request {
             // Honor the request
-            self.named.get(name)
-        } else if let Some(op) = &self.anonymous {
-            // No name request, return the anonymous operation if it’s the only operation
-            self.named.is_empty().then_some(op)
-        } else {
-            // No name request or anonymous operation, return a named operation if it’s the only one
             self.named
-                .values()
-                .next()
-                .and_then(|op| (self.named.len() == 1).then_some(op))
+                .get(name)
+                .ok_or_else(|| format!("No operation named '{name}'"))
+        } else {
+            // No name request (`operationName` unspecified or null)
+            if let Some(op) = &self.anonymous {
+                // Return the anonymous operation if it’s the only operation
+                self.named.is_empty().then_some(op)
+            } else {
+                // No anonymous operation, return a named operation if it’s the only one
+                self.named
+                    .values()
+                    .next()
+                    .and_then(|op| (self.named.len() == 1).then_some(op))
+            }
+            .ok_or_else(|| {
+                "Ambiguous request: multiple operations but no specified `operationName`".to_owned()
+            })
         }
-        .ok_or(GetOperationError())
+        .map_err(|message| RequestError {
+            message,
+            location: None,
+            is_suspected_validation_bug: false,
+        })
     }
 
     /// Similar to [`get`][Self::get] but returns a mutable reference.
-    pub fn get_mut(
-        &mut self,
-        name_request: Option<&str>,
-    ) -> Result<&mut Operation, GetOperationError> {
+    pub fn get_mut(&mut self, name_request: Option<&str>) -> Result<&mut Operation, RequestError> {
         if let Some(name) = name_request {
             // Honor the request
-            self.named.get_mut(name)
-        } else if let Some(op) = &mut self.anonymous {
-            // No name request, return the anonymous operation if it’s the only operation
-            self.named.is_empty().then_some(op)
-        } else {
-            // No name request or anonymous operation, return a named operation if it’s the only one
-            let len = self.named.len();
             self.named
-                .values_mut()
-                .next()
-                .and_then(|op| (len == 1).then_some(op))
+                .get_mut(name)
+                .ok_or_else(|| format!("No operation named '{name}'"))
+        } else {
+            // No name request (`operationName` unspecified or null)
+            if let Some(op) = &mut self.anonymous {
+                // Return the anonymous operation if it’s the only operation
+                self.named.is_empty().then_some(op)
+            } else {
+                // No anonymous operation, return a named operation if it’s the only one
+                let len = self.named.len();
+                self.named
+                    .values_mut()
+                    .next()
+                    .and_then(|op| (len == 1).then_some(op))
+            }
+            .ok_or_else(|| {
+                "Ambiguous request: multiple operations but no specified `operationName`".to_owned()
+            })
         }
         .map(Node::make_mut)
-        .ok_or(GetOperationError())
+        .map_err(|message| RequestError {
+            message,
+            location: None,
+            is_suspected_validation_bug: false,
+        })
     }
 
     /// Insert the given operation in either `named_operations` or `anonymous_operation`
@@ -532,7 +542,8 @@ impl Operation {
     ///
     /// This does **not** perform [field merging] nor fragment spreads de-duplication,
     /// so multiple items in this iterator may have the same response key,
-    /// point to the same field definition, or even be the same field selection.
+    /// point to the same field definition,
+    /// or even be the same field selection (reached through different fragments).
     ///
     /// [field merging]: https://spec.graphql.org/draft/#sec-Field-Selection-Merging
     pub fn root_fields<'doc>(
diff --git a/crates/apollo-compiler/src/execution/engine.rs b/crates/apollo-compiler/src/execution/engine.rs
index 1067393..885831b 100644
--- a/crates/apollo-compiler/src/execution/engine.rs
+++ b/crates/apollo-compiler/src/execution/engine.rs
@@ -7,12 +7,12 @@ use crate::execution::input_coercion::coerce_argument_values;
 use crate::execution::resolver::ObjectValue;
 use crate::execution::resolver::ResolverError;
 use crate::execution::result_coercion::complete_value;
-use crate::execution::GraphQLError;
-use crate::execution::JsonMap;
-use crate::execution::JsonValue;
-use crate::execution::ResponseDataPathElement;
 use crate::parser::SourceMap;
 use crate::parser::SourceSpan;
+use crate::response::GraphQLError;
+use crate::response::JsonMap;
+use crate::response::JsonValue;
+use crate::response::ResponseDataPathSegment;
 use crate::schema::ExtendedType;
 use crate::schema::FieldDefinition;
 use crate::schema::ObjectType;
@@ -42,7 +42,7 @@ pub(crate) struct PropagateNull;
 pub(crate) type LinkedPath<'a> = Option<&'a LinkedPathElement<'a>>;
 
 pub(crate) struct LinkedPathElement<'a> {
-    pub(crate) element: ResponseDataPathElement,
+    pub(crate) element: ResponseDataPathSegment,
     pub(crate) next: LinkedPath<'a>,
 }
 
@@ -59,12 +59,13 @@ pub(crate) fn execute_selection_set<'a>(
     object_value: &ObjectValue<'_>,
     selections: impl IntoIterator<Item = &'a Selection>,
 ) -> Result<JsonMap, PropagateNull> {
-    let mut grouped_field_set = IndexMap::with_hasher(Default::default());
+    let mut grouped_field_set = IndexMap::default();
     collect_fields(
         schema,
         document,
         variable_values,
         object_type,
+        object_value,
         selections,
         &mut HashSet::default(),
         &mut grouped_field_set,
@@ -92,7 +93,7 @@ pub(crate) fn execute_selection_set<'a>(
             JsonValue::from(object_type.name.as_str())
         } else {
             let field_path = LinkedPathElement {
-                element: ResponseDataPathElement::Field(response_key.clone()),
+                element: ResponseDataPathSegment::Field(response_key.clone()),
                 next: path,
             };
             execute_field(
@@ -119,6 +120,7 @@ fn collect_fields<'a>(
     document: &'a ExecutableDocument,
     variable_values: &Valid<JsonMap>,
     object_type: &ObjectType,
+    object_value: &ObjectValue<'_>,
     selections: impl IntoIterator<Item = &'a Selection>,
     visited_fragments: &mut HashSet<&'a Name>,
     grouped_fields: &mut IndexMap<&'a Name, Vec<&'a Field>>,
@@ -130,10 +132,14 @@ fn collect_fields<'a>(
             continue;
         }
         match selection {
-            Selection::Field(field) => grouped_fields
-                .entry(field.response_key())
-                .or_default()
-                .push(field.as_ref()),
+            Selection::Field(field) => {
+                if !object_value.skip_field(&field.name) {
+                    grouped_fields
+                        .entry(field.response_key())
+                        .or_default()
+                        .push(field.as_ref())
+                }
+            }
             Selection::FragmentSpread(spread) => {
                 let new = visited_fragments.insert(&spread.fragment_name);
                 if !new {
@@ -150,6 +156,7 @@ fn collect_fields<'a>(
                     document,
                     variable_values,
                     object_type,
+                    object_value,
                     &fragment.selection_set.selections,
                     visited_fragments,
                     grouped_fields,
@@ -166,6 +173,7 @@ fn collect_fields<'a>(
                     document,
                     variable_values,
                     object_type,
+                    object_value,
                     &inline.selection_set.selections,
                     visited_fragments,
                     grouped_fields,
@@ -280,7 +288,7 @@ pub(crate) fn try_nullify(
     }
 }
 
-pub(crate) fn path_to_vec(mut link: LinkedPath<'_>) -> Vec<ResponseDataPathElement> {
+pub(crate) fn path_to_vec(mut link: LinkedPath<'_>) -> Vec<ResponseDataPathSegment> {
     let mut path = Vec::new();
     while let Some(node) = link {
         path.push(node.element.clone());
@@ -309,8 +317,10 @@ impl SuspectedValidationBug {
         sources: &SourceMap,
         path: LinkedPath<'_>,
     ) -> GraphQLError {
-        let mut err = self.into_graphql_error(sources);
-        err.path = path_to_vec(path);
+        let Self { message, location } = self;
+        let mut err = GraphQLError::field_error(message, path, location, sources);
+        err.extensions
+            .insert("APOLLO_SUSPECTED_VALIDATION_BUG", true.into());
         err
     }
 }
diff --git a/crates/apollo-compiler/src/execution/input_coercion.rs b/crates/apollo-compiler/src/execution/input_coercion.rs
index 4a967e7..836afe7 100644
--- a/crates/apollo-compiler/src/execution/input_coercion.rs
+++ b/crates/apollo-compiler/src/execution/input_coercion.rs
@@ -5,12 +5,11 @@ use crate::executable::Field;
 use crate::executable::Operation;
 use crate::execution::engine::LinkedPath;
 use crate::execution::engine::PropagateNull;
-use crate::execution::GraphQLError;
-use crate::execution::JsonMap;
-use crate::execution::JsonValue;
-use crate::execution::Response;
 use crate::parser::SourceMap;
 use crate::parser::SourceSpan;
+use crate::response::GraphQLError;
+use crate::response::JsonMap;
+use crate::response::JsonValue;
 use crate::schema::ExtendedType;
 use crate::schema::FieldDefinition;
 use crate::validation::SuspectedValidationBug;
@@ -20,7 +19,7 @@ use crate::Node;
 use crate::Schema;
 
 #[derive(Debug, Clone)]
-pub enum InputCoercionError {
+pub(crate) enum InputCoercionError {
     SuspectedValidationBug(SuspectedValidationBug),
     // TODO: split into more structured variants?
     ValueError {
@@ -29,13 +28,8 @@ pub enum InputCoercionError {
     },
 }
 
-/// Coerce the values of variables from a GraphQL request to the types expected by the operation.
-///
-/// If type coercion fails, a request error is returned and the request must not be executed.
-///
-/// This is [CoerceVariableValues()](https://spec.graphql.org/October2021/#CoerceVariableValues())
-/// in the GraphQL specification.
-pub fn coerce_variable_values(
+// Documented in `src/request.rs`
+pub(crate) fn coerce_variable_values(
     schema: &Valid<Schema>,
     operation: &Operation,
     values: &JsonMap,
@@ -516,22 +510,6 @@ impl From<SuspectedValidationBug> for InputCoercionError {
 }
 
 impl InputCoercionError {
-    /// Convert into a JSON-serializable error as represented in a GraphQL response
-    pub fn into_graphql_error(self, sources: &SourceMap) -> GraphQLError {
-        match self {
-            Self::SuspectedValidationBug(s) => s.into_graphql_error(sources),
-            Self::ValueError { message, location } => GraphQLError::new(message, location, sources),
-        }
-    }
-
-    /// Convert into a response with this error as a [request error]
-    /// that prevented execution from starting.
-    ///
-    /// [request error]: https://spec.graphql.org/October2021/#sec-Errors.Request-errors
-    pub fn into_response(self, sources: &SourceMap) -> Response {
-        Response::from_request_error(self.into_graphql_error(sources))
-    }
-
     pub(crate) fn into_field_error(
         self,
         path: LinkedPath<'_>,
diff --git a/crates/apollo-compiler/src/execution/introspection_split.rs b/crates/apollo-compiler/src/execution/introspection_split.rs
deleted file mode 100644
index 2b9a4a9..0000000
--- a/crates/apollo-compiler/src/execution/introspection_split.rs
+++ /dev/null
@@ -1,617 +0,0 @@
-use crate::ast;
-use crate::collections::HashSet;
-use crate::collections::IndexMap;
-use crate::executable::Field;
-use crate::executable::Fragment;
-use crate::executable::FragmentSpread;
-use crate::executable::InlineFragment;
-use crate::executable::Operation;
-use crate::executable::OperationType;
-use crate::executable::Selection;
-use crate::executable::SelectionSet;
-use crate::execution::introspection_max_depth::DeeplyNestedIntrospectionListError;
-use crate::execution::GraphQLError;
-use crate::execution::Response;
-use crate::execution::SchemaIntrospectionQuery;
-use crate::parser::SourceMap;
-use crate::parser::SourceSpan;
-use crate::schema;
-use crate::schema::Name;
-use crate::validation::SuspectedValidationBug;
-use crate::validation::Valid;
-use crate::ExecutableDocument;
-use crate::Node;
-use crate::Schema;
-use indexmap::map::Entry;
-
-/// Result of [`split`][Self::split]ting [schema introspection] fields from an operation.
-///
-/// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
-#[derive(Clone, Debug)]
-pub enum SchemaIntrospectionSplit {
-    /// The selected operation does *not* use [schema introspection] fields.
-    /// It should be executed unchanged.
-    ///
-    /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
-    None,
-
-    /// The selected operation *only* uses [schema introspection] fields.
-    /// This provides the [`execute`][SchemaIntrospectionQuery::execute]’able introspection query,
-    /// there is nothing else to execute.
-    ///
-    /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
-    Only(SchemaIntrospectionQuery),
-
-    /// The selected operation uses *both* [schema introspection] fields and other fields.
-    /// Each part should be executed, and their responses merged with [`Response::merge`].
-    ///
-    /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
-    Both {
-        /// The [`execute`][SchemaIntrospectionQuery::execute]’able query
-        /// for schema introspection parts of the original operation.
-        introspection_query: SchemaIntrospectionQuery,
-
-        /// The rest of the operation.
-        ///
-        /// This document contains exactly one operation with schema introspection fields removed,
-        /// and the fragment definitions that are still needed.
-        /// The operation definition name is preserved,
-        /// so either `None` or the original `Option<&str>` name request can be passed
-        /// to [`OperationMap::get`][crate::executable::OperationMap::get]
-        /// to obtain the one operation.
-        filtered_document: Valid<ExecutableDocument>,
-    },
-}
-
-#[derive(Debug)]
-pub enum SchemaIntrospectionError {
-    SuspectedValidationBug(SuspectedValidationBug),
-    DeeplyNestedIntrospectionList(DeeplyNestedIntrospectionListError),
-    Unsupported {
-        message: String,
-        location: Option<SourceSpan>,
-    },
-}
-
-impl SchemaIntrospectionSplit {
-    /// Splits [schema introspection] fields from an operation.
-    ///
-    /// Returns either a split result, or a [request error] that should stop any further execution
-    /// and can be converted with [`into_response`][SchemaIntrospectionError::into_response].
-    ///
-    /// In [the execution model described in the GrapQL specification][execution],
-    /// a single server executes an entire operation by traversing its selections
-    /// and calling resolver functions for each individual field.
-    /// In this model, schema introspection is just another set of fields
-    /// with dedicated resolver functions.
-    ///
-    /// In other models such as [Apollo Federation] there may not be an obvious place
-    /// to “plug in” such resolvers. Instead, this function splits an operation
-    /// into either or both introspection and other parts that can be executed separately.
-    /// Full execution of introspection parts is provided by [`SchemaIntrospectionQuery::execute`].
-    ///
-    /// In an unconventional schema
-    /// where the type of the `query` operation is also the type of some field,
-    /// it is possible to use [schema introspection] fields nested in other fields.
-    /// This function returns [`SchemaIntrospectionError::Unsupported`] for such operations,
-    /// as they cannot be split into parts that have disjoint response keys.
-    ///
-    /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
-    /// [request error]: https://spec.graphql.org/October2021/#sec-Errors.Request-errors
-    /// [execution]: https://spec.graphql.org/October2021/#sec-Execution
-    /// [Apollo Federation]: https://www.apollographql.com/docs/federation/
-    pub fn split(
-        schema: &Valid<Schema>,
-        document: &Valid<ExecutableDocument>,
-        operation: &Node<Operation>,
-    ) -> Result<Self, SchemaIntrospectionError> {
-        if operation.operation_type != OperationType::Query {
-            check_non_query(document, operation)?;
-            return Ok(Self::None);
-        }
-
-        let mut fragments_info = IndexMap::with_hasher(Default::default());
-        let operation_field_kinds =
-            collect_field_kinds(document, &mut fragments_info, &operation.selection_set)?;
-        if operation_field_kinds.schema_introspection.is_none() {
-            Ok(Self::None)
-        } else if !operation_field_kinds.has_other_fields {
-            // Clone unmodified operation
-            let new_operation = operation.clone();
-            // Clone unmodified fragments, but only the relevant ones
-            let fragments = fragments_info
-                .keys()
-                .map(|&key| (key.clone(), document.fragments[key].clone()))
-                .collect();
-            let introspection_document =
-                make_single_operation_document(schema, document, new_operation, fragments);
-            Ok(Self::Only(
-                SchemaIntrospectionQuery::assume_only_intropsection_fields(introspection_document)?,
-            ))
-        } else {
-            let mut fragments_done = HashSet::with_hasher(Default::default());
-            let mut new_documents = Split {
-                introspection: DocumentBuilder::new(document, operation),
-                other: DocumentBuilder::new(document, operation),
-            };
-            let operation_selection_set = split_selection_set(
-                &mut fragments_done,
-                &mut new_documents,
-                &operation.selection_set,
-            );
-            Ok(Self::Both {
-                introspection_query: SchemaIntrospectionQuery::assume_only_intropsection_fields(
-                    new_documents.introspection.build(
-                        schema,
-                        document,
-                        operation_selection_set.introspection,
-                    ),
-                )?,
-                filtered_document: new_documents.other.build(
-                    schema,
-                    document,
-                    operation_selection_set.other,
-                ),
-            })
-        }
-    }
-}
-
-fn field_is_schema_introspection(field: &Field) -> bool {
-    field.name == "__schema" || field.name == "__type"
-}
-
-/// Returns an error if schema introspection is used anywhere.
-/// Called for a mutation or subscription operation.
-fn check_non_query<'doc>(
-    document: &'doc Valid<ExecutableDocument>,
-    operation: &'doc Operation,
-) -> Result<(), SchemaIntrospectionError> {
-    fn check_selection_set<'doc>(
-        fragments_visited: &mut HashSet<&'doc Name>,
-        fragments_to_visit: &mut HashSet<&'doc Name>,
-        selection_set: &'doc SelectionSet,
-    ) -> Result<(), &'doc Node<Field>> {
-        for selection in &selection_set.selections {
-            match selection {
-                Selection::Field(field) => {
-                    if field_is_schema_introspection(field) {
-                        return Err(field);
-                    }
-                    check_selection_set(
-                        fragments_visited,
-                        fragments_to_visit,
-                        &field.selection_set,
-                    )?
-                }
-                Selection::InlineFragment(inline_fragment) => check_selection_set(
-                    fragments_visited,
-                    fragments_to_visit,
-                    &inline_fragment.selection_set,
-                )?,
-                Selection::FragmentSpread(fragment_spread) => {
-                    let name = &fragment_spread.fragment_name;
-                    if !fragments_visited.contains(name) {
-                        fragments_to_visit.insert(name);
-                    }
-                }
-            }
-        }
-        Ok(())
-    }
-    let unsupported = |field: &Node<Field>| SchemaIntrospectionError::Unsupported {
-        message: format!(
-            "Schema introspection field {} is not supported in a {} operation",
-            field.name, operation.operation_type,
-        ),
-        location: field.location(),
-    };
-    let mut fragments_visited = HashSet::with_hasher(Default::default());
-    let mut fragments_to_visit = HashSet::with_hasher(Default::default());
-    check_selection_set(
-        &mut fragments_visited,
-        &mut fragments_to_visit,
-        &operation.selection_set,
-    )
-    .map_err(unsupported)?;
-    while let Some(name) = fragments_to_visit.iter().next().copied() {
-        let fragment_def = get_fragment(document, name)?;
-        check_selection_set(
-            &mut fragments_visited,
-            &mut fragments_to_visit,
-            &fragment_def.selection_set,
-        )
-        .map_err(unsupported)?;
-        fragments_to_visit.remove(name);
-        fragments_visited.insert(name);
-    }
-    Ok(())
-}
-
-/// As found in `ExecutableDocument::fragments`
-type FragmentMap = IndexMap<Name, Node<Fragment>>;
-
-/// The given operation and fragments are expected to form a valid document.
-/// This is checked iff debug assertions are enabled.
-fn make_single_operation_document(
-    schema: &Valid<Schema>,
-    document: &Valid<ExecutableDocument>,
-    new_operation: Node<Operation>,
-    fragments: FragmentMap,
-) -> Valid<ExecutableDocument> {
-    let mut new_document = ExecutableDocument {
-        sources: document.sources.clone(),
-        operations: Default::default(),
-        fragments,
-    };
-    new_document.operations.insert(new_operation);
-    new_document
-        .validate(schema)
-        .expect("filtering a valid document should result in a valid document")
-}
-
-pub(crate) fn get_fragment<'doc>(
-    document: &'doc Valid<ExecutableDocument>,
-    name: &Name,
-) -> Result<&'doc Node<Fragment>, SchemaIntrospectionError> {
-    document.fragments.get(name).ok_or_else(|| {
-        SuspectedValidationBug {
-            message: format!("undefined fragment {name}"),
-            location: name.location(),
-        }
-        .into()
-    })
-}
-
-/// Which kinds of fields are reachable from a given selection set?
-/// Either directly or through (inline or named) fragments, but not through nested fields.
-#[derive(Clone, Copy, Default)]
-struct TopLevelFieldKinds<'doc> {
-    schema_introspection: Option<&'doc Node<Field>>,
-    has_other_fields: bool,
-}
-
-impl std::ops::BitOrAssign for TopLevelFieldKinds<'_> {
-    fn bitor_assign(&mut self, rhs: Self) {
-        if self.schema_introspection.is_none() {
-            self.schema_introspection = rhs.schema_introspection
-        }
-        self.has_other_fields |= rhs.has_other_fields
-    }
-}
-
-enum Computation<T> {
-    Ongoing,
-    Done(T),
-}
-
-/// This function has triple purpose:
-///
-/// * Return which kinds of fields are used at the “response top-level” of a selection set.
-///   That is, inline fragments and fragment spreads are considered same-level,
-///   but sub-selections of non-leaf fields are not.
-///   For the root selection set of the operation,
-///   this determines which `SchemaIntrospectionSplit` variant `split` returns.
-///
-/// * Populate the `fragments` hash map so it has a key
-///   for every fragment reachable from this selection set,
-///   including through other fragments.
-///   Map values are used for the first purpose, but `SchemaIntrospectionSplit::split`
-///   also relies on key presense.
-///
-/// * Return an "unsupported" error if schema introspection are used nested in other fields.
-fn collect_field_kinds<'doc>(
-    document: &'doc Valid<ExecutableDocument>,
-    fragments: &mut IndexMap<&'doc Name, Computation<TopLevelFieldKinds<'doc>>>,
-    selection_set: &'doc SelectionSet,
-) -> Result<TopLevelFieldKinds<'doc>, SchemaIntrospectionError> {
-    let mut top_level_field_kinds = TopLevelFieldKinds::default();
-    for selection in &selection_set.selections {
-        match selection {
-            Selection::Field(field) => {
-                let nested_field_kinds =
-                    collect_field_kinds(document, fragments, &field.selection_set)?;
-                if field_is_schema_introspection(field) {
-                    top_level_field_kinds
-                        .schema_introspection
-                        .get_or_insert(field);
-                    // `nested_field_kinds` not used here but the recursive call above
-                    // still populate `fragments` with reachable fragments.
-                } else {
-                    if let Some(schema_introspection_field) =
-                        nested_field_kinds.schema_introspection
-                    {
-                        return Err(SchemaIntrospectionError::Unsupported {
-                            message: format!(
-                                "Schema introspection field {} is not supported \
-                                 nested in other fields",
-                                schema_introspection_field.name
-                            ),
-                            location: schema_introspection_field.location(),
-                        });
-                    }
-                    top_level_field_kinds.has_other_fields = true;
-                }
-            }
-            Selection::InlineFragment(inline_fragment) => {
-                top_level_field_kinds |=
-                    collect_field_kinds(document, fragments, &inline_fragment.selection_set)?;
-            }
-            Selection::FragmentSpread(fragment_spread) => {
-                let fragment_def = get_fragment(document, &fragment_spread.fragment_name)?;
-                let name = &fragment_def.name; // with location at definition, not spread
-                match fragments.entry(name) {
-                    Entry::Occupied(entry) => match entry.get() {
-                        Computation::Ongoing => {
-                            return Err(SuspectedValidationBug {
-                                message: "fragment cycle".to_owned(),
-                                location: name.location(),
-                            }
-                            .into());
-                        }
-                        Computation::Done(fragment_field_kinds) => {
-                            top_level_field_kinds |= *fragment_field_kinds
-                        }
-                    },
-                    Entry::Vacant(entry) => {
-                        entry.insert(Computation::Ongoing);
-                        let fragment_field_kinds =
-                            collect_field_kinds(document, fragments, &fragment_def.selection_set)?;
-                        fragments.insert(name, Computation::Done(fragment_field_kinds));
-                        top_level_field_kinds |= fragment_field_kinds
-                    }
-                }
-            }
-        }
-    }
-    Ok(top_level_field_kinds)
-}
-
-#[derive(Default)]
-struct Split<T> {
-    introspection: T,
-    other: T,
-}
-
-struct DocumentBuilder<'doc> {
-    original_document: &'doc Valid<ExecutableDocument>,
-    original_operation: &'doc Node<Operation>,
-    variables_used: HashSet<&'doc Name>,
-    new_fragments: FragmentMap,
-}
-
-fn split_selection_set<'doc>(
-    fragments_done: &mut HashSet<&'doc Name>,
-    new_documents: &mut Split<DocumentBuilder<'doc>>,
-    selection_set: &'doc SelectionSet,
-) -> Split<SelectionSet> {
-    let mut new_selection_sets = Split {
-        introspection: SelectionSet::new(selection_set.ty.clone()),
-        other: SelectionSet::new(selection_set.ty.clone()),
-    };
-    for selection in &selection_set.selections {
-        match selection {
-            Selection::Field(field) => {
-                // A field’s sub-selections are not top-level and therefore don’t need to be split.
-                // Clone as-is, and visit to collect fragment definitions and variables used.
-                if field_is_schema_introspection(field) {
-                    new_selection_sets.introspection.push(field.clone());
-                    new_documents.introspection.visit_field(field);
-                } else {
-                    new_selection_sets.other.push(field.clone());
-                    new_documents.other.visit_field(field);
-                }
-            }
-            Selection::InlineFragment(inline_fragment) => {
-                // Add an inline fragment if the split nested selection set is non-empty
-                let if_non_empty = |doc: &mut DocumentBuilder<'doc>,
-                                    parent: &mut SelectionSet,
-                                    nested: SelectionSet| {
-                    if !nested.selections.is_empty() {
-                        doc.visit_directives(&inline_fragment.directives);
-                        parent.push(inline_fragment.same_location(InlineFragment {
-                            type_condition: inline_fragment.type_condition.clone(),
-                            directives: inline_fragment.directives.clone(),
-                            selection_set: nested,
-                        }))
-                    }
-                };
-                let nested = split_selection_set(
-                    // document,
-                    fragments_done,
-                    new_documents,
-                    &inline_fragment.selection_set,
-                );
-                if_non_empty(
-                    &mut new_documents.introspection,
-                    &mut new_selection_sets.introspection,
-                    nested.introspection,
-                );
-                if_non_empty(
-                    &mut new_documents.other,
-                    &mut new_selection_sets.other,
-                    nested.other,
-                );
-            }
-            Selection::FragmentSpread(fragment_spread) => {
-                let name = &fragment_spread.fragment_name;
-                let new = fragments_done.insert(name);
-                if new {
-                    let document = &new_documents.introspection.original_document;
-                    // Checked in `collect_field_kinds`
-                    let fragment_def = &document.fragments[name];
-                    // Add a fragment definition if the split selection set is non-empty
-                    let if_non_empty = |doc: &mut DocumentBuilder<'doc>, nested: SelectionSet| {
-                        if !nested.selections.is_empty() {
-                            doc.visit_directives(&fragment_def.directives);
-                            doc.new_fragments.insert(
-                                fragment_def.name.clone(),
-                                fragment_def.same_location(Fragment {
-                                    name: fragment_def.name.clone(),
-                                    directives: fragment_def.directives.clone(),
-                                    selection_set: nested,
-                                }),
-                            );
-                        }
-                    };
-                    let nested = split_selection_set(
-                        fragments_done,
-                        new_documents,
-                        &fragment_def.selection_set,
-                    );
-                    if_non_empty(&mut new_documents.introspection, nested.introspection);
-                    if_non_empty(&mut new_documents.other, nested.other);
-                }
-                // Add a fragment spread if the above resulted in a fragment definition
-                let if_defined = |doc: &mut DocumentBuilder<'doc>, parent: &mut SelectionSet| {
-                    if doc.new_fragments.contains_key(name) {
-                        doc.visit_directives(&fragment_spread.directives);
-                        parent.push(fragment_spread.same_location(FragmentSpread {
-                            fragment_name: fragment_spread.fragment_name.clone(),
-                            directives: fragment_spread.directives.clone(),
-                        }))
-                    }
-                };
-                if_defined(
-                    &mut new_documents.introspection,
-                    &mut new_selection_sets.introspection,
-                );
-                if_defined(&mut new_documents.other, &mut new_selection_sets.other);
-            }
-        }
-    }
-    new_selection_sets
-}
-
-impl<'doc> DocumentBuilder<'doc> {
-    fn new(
-        original_document: &'doc Valid<ExecutableDocument>,
-        original_operation: &'doc Node<Operation>,
-    ) -> Self {
-        Self {
-            original_document,
-            original_operation,
-            variables_used: Default::default(),
-            new_fragments: Default::default(),
-        }
-    }
-
-    fn visit_selection_set(&mut self, selection_set: &'doc SelectionSet) {
-        for selection in &selection_set.selections {
-            match selection {
-                Selection::Field(field) => self.visit_field(field),
-                Selection::InlineFragment(inline_fragment) => {
-                    self.visit_directives(&inline_fragment.directives);
-                    self.visit_selection_set(&inline_fragment.selection_set);
-                }
-                Selection::FragmentSpread(fragment_spread) => {
-                    self.visit_directives(&fragment_spread.directives);
-                    let name = &fragment_spread.fragment_name;
-                    if let Entry::Vacant(entry) = self.new_fragments.entry(name.clone()) {
-                        // Checked in `collect_field_kinds`
-                        let fragment_def = &self.original_document.fragments[name];
-                        entry.insert(fragment_def.clone());
-                        self.visit_directives(&fragment_def.directives);
-                        self.visit_selection_set(&fragment_def.selection_set);
-                    };
-                }
-            }
-        }
-    }
-
-    fn visit_field(&mut self, field: &'doc Field) {
-        for arg in &field.arguments {
-            self.visit_value(&arg.value)
-        }
-        self.visit_directives(&field.directives);
-        self.visit_selection_set(&field.selection_set);
-    }
-
-    fn visit_directives(&mut self, directives: &'doc ast::DirectiveList) {
-        for directive in directives {
-            for arg in &directive.arguments {
-                self.visit_value(&arg.value)
-            }
-        }
-    }
-
-    fn visit_value(&mut self, value: &'doc ast::Value) {
-        match value {
-            schema::Value::Variable(name) => {
-                self.variables_used.insert(name);
-            }
-            schema::Value::List(list) => {
-                for value in list {
-                    self.visit_value(value)
-                }
-            }
-            schema::Value::Object(object) => {
-                for (_name, value) in object {
-                    self.visit_value(value)
-                }
-            }
-            schema::Value::Null
-            | schema::Value::Enum(_)
-            | schema::Value::String(_)
-            | schema::Value::Float(_)
-            | schema::Value::Int(_)
-            | schema::Value::Boolean(_) => {}
-        }
-    }
-
-    fn build(
-        mut self,
-        schema: &Valid<Schema>,
-        document: &Valid<ExecutableDocument>,
-        operation_selection_set: SelectionSet,
-    ) -> Valid<ExecutableDocument> {
-        for directive in &self.original_operation.directives {
-            for arg in &directive.arguments {
-                self.visit_value(&arg.value)
-            }
-        }
-        let new_operation = self.original_operation.same_location(Operation {
-            operation_type: self.original_operation.operation_type,
-            name: self.original_operation.name.clone(),
-            variables: self
-                .original_operation
-                .variables
-                .iter()
-                .filter(|var| self.variables_used.contains(&var.name))
-                .cloned()
-                .collect(),
-            directives: self.original_operation.directives.clone(),
-            selection_set: operation_selection_set,
-        });
-        make_single_operation_document(schema, document, new_operation, self.new_fragments)
-    }
-}
-
-impl From<SuspectedValidationBug> for SchemaIntrospectionError {
-    fn from(value: SuspectedValidationBug) -> Self {
-        Self::SuspectedValidationBug(value)
-    }
-}
-
-impl SchemaIntrospectionError {
-    /// Convert into a JSON-serializable error as represented in a GraphQL response
-    pub fn into_graphql_error(self, sources: &SourceMap) -> GraphQLError {
-        match self {
-            Self::SuspectedValidationBug(s) => s.into_graphql_error(sources),
-            Self::DeeplyNestedIntrospectionList(e) => {
-                GraphQLError::new("Maximum introspection depth exceeded", e.location, sources)
-            }
-            Self::Unsupported { message, location } => {
-                GraphQLError::new(message, location, sources)
-            }
-        }
-    }
-
-    /// Convert into a response with this error as a [request error]
-    /// that prevented execution from starting.
-    ///
-    /// [request error]: https://spec.graphql.org/October2021/#sec-Errors.Request-errors
-    pub fn into_response(self, sources: &SourceMap) -> Response {
-        Response::from_request_error(self.into_graphql_error(sources))
-    }
-}
diff --git a/crates/apollo-compiler/src/execution/mod.rs b/crates/apollo-compiler/src/execution/mod.rs
index 902f3a5..ace52d6 100644
--- a/crates/apollo-compiler/src/execution/mod.rs
+++ b/crates/apollo-compiler/src/execution/mod.rs
@@ -5,36 +5,7 @@
 //! [response]: https://spec.graphql.org/October2021/#sec-Response
 
 #[macro_use]
-mod resolver;
-mod engine;
-mod input_coercion;
-mod introspection_execute;
-mod introspection_max_depth;
-mod introspection_split;
-mod response;
-mod result_coercion;
-
-pub use self::input_coercion::coerce_variable_values;
-pub use self::input_coercion::InputCoercionError;
-pub use self::introspection_execute::execute_introspection_only_query;
-pub use self::introspection_execute::SchemaIntrospectionQuery;
-pub use self::introspection_max_depth::check_introspection_max_depth;
-pub use self::introspection_split::SchemaIntrospectionError;
-pub use self::introspection_split::SchemaIntrospectionSplit;
-pub use self::response::GraphQLError;
-pub use self::response::Response;
-pub use self::response::ResponseData;
-pub use self::response::ResponseDataPathElement;
-/// Re-export of the version of the `serde_json_bytes` crate used for [`JsonValue`] and [`JsonMap`]
-pub use serde_json_bytes;
-
-/// A JSON-compatible dynamically-typed value.
-///
-/// Note: [`serde_json_bytes::Value`] is similar
-/// to [`serde_json::Value`][serde_json_bytes::serde_json::Value]
-/// but uses its reference-counted [`ByteString`][serde_json_bytes::ByteString]
-/// for string values and map keys.
-pub type JsonValue = serde_json_bytes::Value;
-
-/// A JSON-compatible object/map with string keys and dynamically-typed values.
-pub type JsonMap = serde_json_bytes::Map<serde_json_bytes::ByteString, JsonValue>;
+pub(crate) mod resolver;
+pub(crate) mod engine;
+pub(crate) mod input_coercion;
+pub(crate) mod result_coercion;
diff --git a/crates/apollo-compiler/src/execution/resolver.rs b/crates/apollo-compiler/src/execution/resolver.rs
index 3a9d656..5d32d37 100644
--- a/crates/apollo-compiler/src/execution/resolver.rs
+++ b/crates/apollo-compiler/src/execution/resolver.rs
@@ -1,4 +1,4 @@
-use crate::execution::JsonMap;
+use crate::response::JsonMap;
 use serde_json_bytes::Value as JsonValue;
 
 /// A GraphQL object whose fields can be resolved during execution
@@ -24,6 +24,14 @@ pub(crate) trait Resolver {
         field_name: &'a str,
         arguments: &'a JsonMap,
     ) -> Result<ResolvedValue<'a>, ResolverError>;
+
+    /// Returns true if this field should be skipped,
+    /// as if the corresponding selection has `@skip(if: true)`.
+    ///
+    /// This is used to exclude root concrete fields in [crate::introspection::partial_execute].
+    fn skip_field(&self, _field_name: &str) -> bool {
+        false
+    }
 }
 
 pub(crate) struct ResolverError {
@@ -60,7 +68,7 @@ macro_rules! impl_resolver {
             fn resolve_field<'a>(
                 &'a self,
                 field_name: &'a str,
-                arguments: &'a $crate::execution::JsonMap,
+                arguments: &'a $crate::response::JsonMap,
             ) -> Result<
                 $crate::execution::resolver::ResolvedValue<'a>,
                 crate::execution::resolver::ResolverError
diff --git a/crates/apollo-compiler/src/execution/response.rs b/crates/apollo-compiler/src/execution/response.rs
deleted file mode 100644
index e70100a..0000000
--- a/crates/apollo-compiler/src/execution/response.rs
+++ /dev/null
@@ -1,185 +0,0 @@
-use crate::execution::engine::PropagateNull;
-use crate::execution::JsonMap;
-use crate::parser::LineColumn;
-use crate::parser::SourceMap;
-use crate::parser::SourceSpan;
-use serde::Deserialize;
-use serde::Serialize;
-
-/// A [GraphQL response](https://spec.graphql.org/October2021/#sec-Response-Format)
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct Response {
-    // <https://spec.graphql.org/October2021/#note-6f005> suggests serializing this first
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    #[serde(default)]
-    pub errors: Vec<GraphQLError>,
-
-    #[serde(skip_serializing_if = "ResponseData::is_absent")]
-    #[serde(default = "ResponseData::absent")]
-    pub data: ResponseData,
-
-    /// Reserved for any additional information
-    #[serde(skip_serializing_if = "JsonMap::is_empty")]
-    #[serde(default)]
-    pub extensions: JsonMap,
-}
-
-/// The `data` entry of a [`Response`]
-#[derive(Debug, Clone, PartialEq, Deserialize)]
-#[serde(from = "Option<JsonMap>")]
-pub enum ResponseData {
-    /// Execution returned an object.
-    /// [`Response::data`] is serialized as a JSON object.
-    Object(JsonMap),
-
-    /// Execution encountered a [field error] on a non-null field,
-    /// and null was [propagated] all the way to the root of the response.
-    /// [`Response::data`] is serialized as JSON null.
-    ///
-    /// [field error]: https://spec.graphql.org/October2021/#sec-Errors.Field-errors
-    /// [propagated]: https://spec.graphql.org/October2021/#sec-Handling-Field-Errors
-    Null,
-
-    /// A [request error] was encountered. Execution did not start.
-    /// [`Response::data`] is skipped from serialization.
-    ///
-    /// [request error]: https://spec.graphql.org/October2021/#sec-Errors.Request-errors
-    Absent,
-}
-
-/// A serializable [error](https://spec.graphql.org/October2021/#sec-Errors.Error-result-format),
-/// as found in a GraphQL [response][Response].
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct GraphQLError {
-    /// The error message.
-    pub message: String,
-
-    /// Locations in relevant to the error, if any.
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    #[serde(default)]
-    pub locations: Vec<LineColumn>,
-
-    /// If non-empty, the error is a [field error]
-    /// for the particular field found at this path in [`Response::data`].
-    ///
-    /// [field error]: https://spec.graphql.org/October2021/#sec-Errors.Field-errors
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    #[serde(default)]
-    pub path: Vec<ResponseDataPathElement>,
-
-    /// Reserved for any additional information
-    #[serde(skip_serializing_if = "JsonMap::is_empty")]
-    #[serde(default)]
-    pub extensions: JsonMap,
-}
-
-/// An element of [`GraphQLError::path`]
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-#[serde(untagged)]
-pub enum ResponseDataPathElement {
-    /// The relevant key in an object value
-    Field(crate::Name),
-
-    /// The index of the relevant item in a list value
-    ListIndex(usize),
-}
-
-impl Response {
-    /// Create a response for a [request error]:
-    /// handling of a request was aborted before execution started.
-    ///
-    /// [request error]: https://spec.graphql.org/October2021/#sec-Errors.Request-errors
-    pub fn from_request_error(error: impl Into<GraphQLError>) -> Self {
-        Self {
-            errors: vec![error.into()],
-            data: ResponseData::Absent,
-            extensions: JsonMap::new(),
-        }
-    }
-
-    /// Merge two responses into one, such as to handle
-    /// [`SchemaIntrospectionSplit::Both`][crate::execution::SchemaIntrospectionSplit::Both].
-    pub fn merge(mut self, mut other: Self) -> Self {
-        match (&mut self.data, other.data) {
-            (ResponseData::Absent, _) | (_, ResponseData::Absent) => {
-                // If either side is a request error (absent data), return a request error
-                self.data = ResponseData::Absent
-            }
-            (ResponseData::Null, _) | (_, ResponseData::Null) => {
-                // Otherwise if either side propagated null from a field error
-                // to the root of the response, return null data.
-                self.data = ResponseData::Null
-            }
-            (ResponseData::Object(self_data), ResponseData::Object(other_data)) => {
-                // Merge two objects/maps
-                self_data.extend(other_data)
-            }
-        }
-        self.errors.append(&mut other.errors);
-        self.extensions.extend(other.extensions);
-        self
-    }
-}
-
-impl GraphQLError {
-    pub fn new(
-        message: impl Into<String>,
-        location: Option<SourceSpan>,
-        sources: &SourceMap,
-    ) -> Self {
-        Self {
-            message: message.into(),
-            locations: location
-                .into_iter()
-                .filter_map(|location| location.line_column(sources))
-                .collect(),
-            path: Default::default(),
-            extensions: Default::default(),
-        }
-    }
-}
-
-impl ResponseData {
-    /// For serde `skip_serializing_if`
-    pub fn is_absent(&self) -> bool {
-        matches!(self, Self::Absent)
-    }
-
-    /// For serde `default`
-    fn absent() -> Self {
-        Self::Absent
-    }
-}
-
-impl Serialize for ResponseData {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        match self {
-            ResponseData::Absent => panic!("ResponseData::None should not be serialized"),
-            ResponseData::Null => serializer.serialize_unit(),
-            ResponseData::Object(map) => map.serialize(serializer),
-        }
-    }
-}
-
-impl From<Option<JsonMap>> for ResponseData {
-    fn from(value: Option<JsonMap>) -> Self {
-        match value {
-            Some(data) => Self::Object(data),
-            None => Self::Null,
-        }
-    }
-}
-
-impl From<Result<JsonMap, PropagateNull>> for ResponseData {
-    fn from(result: Result<JsonMap, PropagateNull>) -> Self {
-        match result {
-            Ok(data) => Self::Object(data),
-            Err(PropagateNull) => Self::Null,
-        }
-    }
-}
diff --git a/crates/apollo-compiler/src/execution/result_coercion.rs b/crates/apollo-compiler/src/execution/result_coercion.rs
index a8f9d3f..e07ecdf 100644
--- a/crates/apollo-compiler/src/execution/result_coercion.rs
+++ b/crates/apollo-compiler/src/execution/result_coercion.rs
@@ -6,10 +6,10 @@ use crate::execution::engine::LinkedPath;
 use crate::execution::engine::LinkedPathElement;
 use crate::execution::engine::PropagateNull;
 use crate::execution::resolver::ResolvedValue;
-use crate::execution::response::ResponseDataPathElement;
-use crate::execution::GraphQLError;
-use crate::execution::JsonMap;
-use crate::execution::JsonValue;
+use crate::response::GraphQLError;
+use crate::response::JsonMap;
+use crate::response::JsonValue;
+use crate::response::ResponseDataPathSegment;
 use crate::schema::ExtendedType;
 use crate::schema::Type;
 use crate::validation::SuspectedValidationBug;
@@ -62,7 +62,7 @@ pub(crate) fn complete_value<'a, 'b>(
                 let mut completed_list = Vec::with_capacity(iter.size_hint().0);
                 for (index, inner_resolved) in iter.enumerate() {
                     let inner_path = LinkedPathElement {
-                        element: ResponseDataPathElement::ListIndex(index),
+                        element: ResponseDataPathSegment::ListIndex(index),
                         next: path,
                     };
                     let inner_result = complete_value(
diff --git a/crates/apollo-compiler/src/execution/introspection_max_depth.rs b/crates/apollo-compiler/src/introspection/max_depth.rs
similarity index 70%
rename from crates/apollo-compiler/src/execution/introspection_max_depth.rs
rename to crates/apollo-compiler/src/introspection/max_depth.rs
index 09cc8fb..8c6047c 100644
--- a/crates/apollo-compiler/src/execution/introspection_max_depth.rs
+++ b/crates/apollo-compiler/src/introspection/max_depth.rs
@@ -1,30 +1,16 @@
-use crate::executable::Operation;
 use crate::executable::Selection;
 use crate::executable::SelectionSet;
-use crate::parser::SourceSpan;
+use crate::request::RequestError;
 use crate::validation::Valid;
 use crate::ExecutableDocument;
 
 const MAX_LISTS_DEPTH: u32 = 3;
 
-#[derive(Debug)]
-pub struct DeeplyNestedIntrospectionListError {
-    pub location: Option<SourceSpan>,
-}
-
-pub fn check_introspection_max_depth(
-    document: &Valid<ExecutableDocument>,
-    operation: &Operation,
-) -> Result<(), DeeplyNestedIntrospectionListError> {
-    let initial_depth = 0;
-    check_selection_set(document, initial_depth, &operation.selection_set)
-}
-
-fn check_selection_set(
+pub(super) fn check_selection_set(
     document: &Valid<ExecutableDocument>,
     depth_so_far: u32,
     selection_set: &SelectionSet,
-) -> Result<(), DeeplyNestedIntrospectionListError> {
+) -> Result<(), RequestError> {
     for selection in &selection_set.selections {
         match selection {
             Selection::InlineFragment(inline) => {
@@ -44,8 +30,10 @@ fn check_selection_set(
                 ) {
                     depth += 1;
                     if depth >= MAX_LISTS_DEPTH {
-                        return Err(DeeplyNestedIntrospectionListError {
+                        return Err(RequestError {
+                            message: "Maximum introspection depth exceeded".into(),
                             location: field.name.location(),
+                            is_suspected_validation_bug: false,
                         });
                     }
                 }
diff --git a/crates/apollo-compiler/src/introspection/mod.rs b/crates/apollo-compiler/src/introspection/mod.rs
new file mode 100644
index 0000000..02bf860
--- /dev/null
+++ b/crates/apollo-compiler/src/introspection/mod.rs
@@ -0,0 +1,123 @@
+//! [Execution](https://spec.graphql.org/draft/#sec-Execution) engine
+//! for the [schema introspection](https://spec.graphql.org/draft/#sec-Schema-Introspection)
+//! portion of a query
+//!
+//! The main entry point is [`partial_execute`].
+
+use crate::collections::HashMap;
+use crate::executable::Operation;
+#[cfg(doc)]
+use crate::executable::OperationMap;
+use crate::execution::engine::execute_selection_set;
+use crate::execution::engine::ExecutionMode;
+use crate::execution::engine::PropagateNull;
+#[cfg(doc)]
+use crate::request::coerce_variable_values;
+use crate::request::RequestError;
+use crate::response::ExecutionResponse;
+use crate::response::JsonMap;
+use crate::schema::Implementers;
+use crate::validation::Valid;
+use crate::ExecutableDocument;
+use crate::Name;
+use crate::Schema;
+
+mod max_depth;
+mod resolvers;
+
+/// Check that the nesting level of some list fields does not exceed a fixed depth limit.
+///
+/// Since [the schema-introspection schema][s] is recursive,
+/// a malicious query could cause huge responses that grow exponentially to the nesting depth.
+///
+/// An error result is a [request error](https://spec.graphql.org/draft/#request-error):
+/// execution must not run at all,
+/// and the GraphQL response must not have a `data` key (which is different from `data: null`).
+///
+/// The exact criteria may change in future apollo-compiler versions.
+///
+/// [s]: https://spec.graphql.org/draft/#sec-Schema-Introspection.Schema-Introspection-Schema
+pub fn check_max_depth(
+    document: &Valid<ExecutableDocument>,
+    operation: &Operation,
+) -> Result<(), RequestError> {
+    let initial_depth = 0;
+    max_depth::check_selection_set(document, initial_depth, &operation.selection_set)
+}
+
+/// Excecutes the [schema introspection](https://spec.graphql.org/draft/#sec-Schema-Introspection)
+/// portion of a query and returns a partial response.
+///
+/// * Consider calling [`check_max_depth`] before this function
+/// * `implementers_map` is expected to be form
+///   [`schema.implementers_map()`][Schema::implementers_map],
+///   allowing it to be computed once and reused for many queries
+/// * `operation` is expected to be from
+///   [`document.operations.get(operation_name)?`][OperationMap::get]
+/// * `operation` is expected to be a query,
+///   check [`operation.operation_type.is_query()`][Operation::operation_type]
+/// * `variable_values` is expected to be from [`coerce_variable_values`]
+///
+/// Concrete [root fields][Operation::root_fields] (those with an explicit definition in the schema)
+/// are **_silently ignored_**.
+///
+/// Only introspection meta-fields are executed:
+/// `__typename` (at the response root), `__type`, and `__schema`.
+/// If the operation also contains concrete fields,
+/// the caller can execute them separately and merge the two partial responses.
+/// To categorize which kinds of root fields are present, consider using code like:
+///
+/// ```
+/// # use apollo_compiler::executable as exe;
+/// # fn categorize_fields(document: &exe::ExecutableDocument, operation: &exe::Operation) {
+/// let mut has_schema_introspection_fields = false;
+/// let mut has_root_typename_fields = false;
+/// let mut has_concrete_fields = false;
+/// for root_field in operation.root_fields(document) {
+///     match root_field.name.as_str() {
+///         "__type" | "__schema" => has_schema_introspection_fields = true,
+///         "__typename" => has_root_typename_fields = true,
+///         _ => has_concrete_fields = true,
+///     }
+/// }
+/// # }
+/// ```
+pub fn partial_execute(
+    schema: &Valid<Schema>,
+    implementers_map: &HashMap<Name, Implementers>,
+    document: &Valid<ExecutableDocument>,
+    operation: &Operation,
+    variable_values: &Valid<JsonMap>,
+) -> Result<ExecutionResponse, RequestError> {
+    let object_type_name = operation.object_type();
+    let Some(root_operation_object_type_def) = schema.get_object(object_type_name) else {
+        return Err(RequestError {
+            message: "Undefined root operation type".to_owned(),
+            location: object_type_name.location(),
+            is_suspected_validation_bug: true,
+        });
+    };
+
+    let initial_value =
+        &resolvers::IntrospectionRootResolver(resolvers::SchemaWithImplementersMap {
+            schema,
+            implementers_map,
+        });
+    let mut errors = Vec::new();
+    let path = None;
+    let data = match execute_selection_set(
+        schema,
+        document,
+        variable_values,
+        &mut errors,
+        path,
+        ExecutionMode::Normal,
+        root_operation_object_type_def,
+        initial_value,
+        &operation.selection_set.selections,
+    ) {
+        Ok(map) => Some(map),
+        Err(PropagateNull) => None,
+    };
+    Ok(ExecutionResponse { data, errors })
+}
diff --git a/crates/apollo-compiler/src/execution/introspection_execute.rs b/crates/apollo-compiler/src/introspection/resolvers.rs
similarity index 64%
rename from crates/apollo-compiler/src/execution/introspection_execute.rs
rename to crates/apollo-compiler/src/introspection/resolvers.rs
index 1e623cf..ffb7ad5 100644
--- a/crates/apollo-compiler/src/execution/introspection_execute.rs
+++ b/crates/apollo-compiler/src/introspection/resolvers.rs
@@ -1,176 +1,31 @@
 use crate::collections::HashMap;
-use crate::executable::Operation;
-use crate::executable::OperationType;
-use crate::execution::engine::execute_selection_set;
-use crate::execution::engine::ExecutionMode;
 use crate::execution::resolver::ResolvedValue;
-use crate::execution::GraphQLError;
-use crate::execution::JsonMap;
-use crate::execution::Response;
-use crate::execution::SchemaIntrospectionError;
-use crate::execution::SchemaIntrospectionSplit;
+use crate::execution::resolver::Resolver;
+use crate::execution::resolver::ResolverError;
+use crate::response::JsonMap;
 use crate::schema;
 use crate::schema::Implementers;
 use crate::schema::Name;
-use crate::validation::Valid;
-use crate::ExecutableDocument;
 use crate::Node;
 use crate::Schema;
 use std::borrow::Cow;
-use std::sync::OnceLock;
-
-/// A document with a single query that only has [schema introspection] fields.
-/// Obtained from [`SchemaIntrospectionSplit::split`].
-///
-/// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
-#[derive(Clone, Debug)]
-pub struct SchemaIntrospectionQuery(Valid<ExecutableDocument>);
-
-impl std::ops::Deref for SchemaIntrospectionQuery {
-    type Target = Valid<ExecutableDocument>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl std::fmt::Display for SchemaIntrospectionQuery {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-impl SchemaIntrospectionQuery {
-    /// Construct a `SchemaIntrospectionQuery` from a document
-    /// assumed to contain exactly one operation that only has [schema introspection] fields.
-    ///
-    /// Generally [`split`][SchemaIntrospectionSplit::split] should be used instead.
-    ///
-    /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
-    pub(crate) fn assume_only_intropsection_fields(
-        document: Valid<ExecutableDocument>,
-    ) -> Result<Self, SchemaIntrospectionError> {
-        let operation = document.operations.get(None).unwrap();
-        super::introspection_max_depth::check_introspection_max_depth(&document, operation)
-            .map_err(SchemaIntrospectionError::DeeplyNestedIntrospectionList)?;
-        Ok(Self(document))
-    }
-
-    /// Execute the [schema introspection] parts of an operation
-    /// and wrap a callback to execute the rest (if any).
-    ///
-    /// This is a convenience wrapper for [`split`][SchemaIntrospectionSplit::split]
-    /// and [`execute`][Self::execute].
-    ///
-    /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
-    pub fn split_and_execute(
-        schema: &Valid<Schema>,
-        document: &Valid<ExecutableDocument>,
-        operation: &Node<Operation>,
-        variable_values: &Valid<JsonMap>,
-        execute_non_introspection_parts: impl FnOnce(&Valid<ExecutableDocument>) -> Response,
-    ) -> Response {
-        match SchemaIntrospectionSplit::split(schema, document, operation) {
-            Ok(SchemaIntrospectionSplit::Only(introspection_query)) => {
-                introspection_query.execute(schema, variable_values)
-            }
-            Ok(SchemaIntrospectionSplit::None) => execute_non_introspection_parts(document),
-            Ok(SchemaIntrospectionSplit::Both {
-                introspection_query,
-                filtered_document: filtered_operation,
-            }) => {
-                let non_introspection_response =
-                    execute_non_introspection_parts(&filtered_operation);
-                let introspection_response = introspection_query.execute(schema, variable_values);
-                non_introspection_response.merge(introspection_response)
-            }
-            Err(err) => err.into_response(&document.sources),
-        }
-    }
-
-    /// Execute this query and return either a response with data or a [request error]
-    /// that can also be converted with [`into_response`][SuspectedValidationBug::into_response].
-    ///
-    /// [request error]: https://spec.graphql.org/October2021/#sec-Errors.Request-errors
-    pub fn execute(&self, schema: &Valid<Schema>, variable_values: &Valid<JsonMap>) -> Response {
-        let operation = self.0.operations.get(None).unwrap();
-        execute_introspection_only_query(schema, &self.0, operation, variable_values)
-    }
-}
-
-/// Execute a query whose [root fields][Operation::root_fields] are all intropsection meta-fields:
-/// `__schema`, `__type`, or `__typename`.
-///
-/// Returns an error if `operation_name` does not [correspond to a an operation definition](https://spec.graphql.org/October2021/#GetOperation())
-pub fn execute_introspection_only_query(
-    schema: &Valid<Schema>,
-    document: &Valid<ExecutableDocument>,
-    operation: &Node<Operation>,
-    variable_values: &Valid<JsonMap>,
-) -> Response {
-    if operation.operation_type != OperationType::Query {
-        return Response::from_request_error(GraphQLError::new(
-            format!(
-                "execute_introspection_only_query called with a {}",
-                operation.operation_type.name()
-            ),
-            operation.location(),
-            &document.sources,
-        ));
-    }
-
-    // https://spec.graphql.org/October2021/#sec-Query
-    let object_type_name = operation.object_type();
-    let Some(object_type_def) = schema.get_object(object_type_name) else {
-        return Response::from_request_error(GraphQLError::new(
-            "Undefined root operation type",
-            object_type_name.location(),
-            &document.sources,
-        ));
-    };
-    let implementers_map = &OnceLock::new();
-    let initial_value = &IntrospectionRootResolver(SchemaWithCache {
-        schema,
-        implementers_map,
-    });
-
-    let mut errors = Vec::new();
-    let path = None;
-    let data = execute_selection_set(
-        schema,
-        document,
-        variable_values,
-        &mut errors,
-        path,
-        ExecutionMode::Normal,
-        object_type_def,
-        initial_value,
-        &operation.selection_set.selections,
-    );
-    Response {
-        data: data.into(),
-        errors,
-        extensions: Default::default(),
-    }
-}
 
 #[derive(Clone, Copy)]
-struct SchemaWithCache<'a> {
-    schema: &'a Schema,
-    implementers_map: &'a OnceLock<HashMap<Name, Implementers>>,
+pub(crate) struct SchemaWithImplementersMap<'a> {
+    pub(crate) schema: &'a Schema,
+    pub(crate) implementers_map: &'a HashMap<Name, Implementers>,
 }
 
-impl<'a> SchemaWithCache<'a> {
+impl<'a> SchemaWithImplementersMap<'a> {
     fn implementers_of(&self, interface_name: &str) -> impl Iterator<Item = &'a Name> {
         self.implementers_map
-            .get_or_init(|| self.schema.implementers_map())
             .get(interface_name)
             .into_iter()
             .flat_map(|implementers| &implementers.objects)
     }
 }
 
-impl<'a> std::ops::Deref for SchemaWithCache<'a> {
+impl<'a> std::ops::Deref for SchemaWithImplementersMap<'a> {
     type Target = &'a Schema;
 
     fn deref(&self) -> &Self::Target {
@@ -178,41 +33,41 @@ impl<'a> std::ops::Deref for SchemaWithCache<'a> {
     }
 }
 
-struct IntrospectionRootResolver<'a>(SchemaWithCache<'a>);
+pub(crate) struct IntrospectionRootResolver<'a>(pub(crate) SchemaWithImplementersMap<'a>);
 
 struct TypeDefResolver<'a> {
-    schema: SchemaWithCache<'a>,
+    schema: SchemaWithImplementersMap<'a>,
     name: &'a str,
     def: &'a schema::ExtendedType,
 }
 
 /// Only used for non-null and list types. `TypeDef` is used for everything else.
-struct TypeResolver<'a> {
-    schema: SchemaWithCache<'a>,
+pub(crate) struct TypeResolver<'a> {
+    schema: SchemaWithImplementersMap<'a>,
     ty: Cow<'a, schema::Type>,
 }
 
 struct DirectiveResolver<'a> {
-    schema: SchemaWithCache<'a>,
+    schema: SchemaWithImplementersMap<'a>,
     def: &'a schema::DirectiveDefinition,
 }
 
 struct FieldResolver<'a> {
-    schema: SchemaWithCache<'a>,
+    schema: SchemaWithImplementersMap<'a>,
     def: &'a schema::FieldDefinition,
 }
 
 struct EnumValueResolver<'a> {
-    schema: SchemaWithCache<'a>,
+    schema: SchemaWithImplementersMap<'a>,
     def: &'a schema::EnumValueDefinition,
 }
 
 struct InputValueResolver<'a> {
-    schema: SchemaWithCache<'a>,
+    schema: SchemaWithImplementersMap<'a>,
     def: &'a schema::InputValueDefinition,
 }
 
-fn type_def(schema: SchemaWithCache<'_>, name: impl AsRef<str>) -> ResolvedValue<'_> {
+fn type_def(schema: SchemaWithImplementersMap<'_>, name: impl AsRef<str>) -> ResolvedValue<'_> {
     ResolvedValue::opt_object(
         schema
             .types
@@ -222,7 +77,7 @@ fn type_def(schema: SchemaWithCache<'_>, name: impl AsRef<str>) -> ResolvedValue
 }
 
 fn type_def_opt<'a>(
-    schema: SchemaWithCache<'a>,
+    schema: SchemaWithImplementersMap<'a>,
     name: &Option<impl AsRef<str>>,
 ) -> ResolvedValue<'a> {
     if let Some(name) = name.as_ref() {
@@ -232,7 +87,7 @@ fn type_def_opt<'a>(
     }
 }
 
-fn ty<'a>(schema: SchemaWithCache<'a>, ty: &'a schema::Type) -> ResolvedValue<'a> {
+fn ty<'a>(schema: SchemaWithImplementersMap<'a>, ty: &'a schema::Type) -> ResolvedValue<'a> {
     if let schema::Type::Named(name) = ty {
         type_def(schema, name)
     } else {
@@ -244,7 +99,7 @@ fn ty<'a>(schema: SchemaWithCache<'a>, ty: &'a schema::Type) -> ResolvedValue<'a
 }
 
 fn deprecation_reason<'a>(
-    schema: &SchemaWithCache<'_>,
+    schema: &SchemaWithImplementersMap<'_>,
     opt_directive: Option<&Node<schema::Directive>>,
 ) -> ResolvedValue<'a> {
     ResolvedValue::leaf(
@@ -254,23 +109,35 @@ fn deprecation_reason<'a>(
     )
 }
 
-impl_resolver! {
-    for IntrospectionRootResolver<'_>:
-
-    __typename = unreachable!();
-
-    fn __schema(&self_) {
-        Ok(ResolvedValue::object(self_.0))
+impl Resolver for IntrospectionRootResolver<'_> {
+    fn type_name(&self) -> &'static str {
+        unreachable!()
+    }
+
+    fn resolve_field<'a>(
+        &'a self,
+        field_name: &'a str,
+        arguments: &'a JsonMap,
+    ) -> Result<ResolvedValue<'a>, ResolverError> {
+        match field_name {
+            "__schema" => Ok(ResolvedValue::object(self.0)),
+            "__type" => {
+                let name = arguments["name"].as_str().unwrap();
+                Ok(type_def(self.0, name))
+            }
+            // "__typename" is handled in `execute_selection_set` without calling here.
+            // Other fields are skipped in `skip_field` below.
+            _ => unreachable!(),
+        }
     }
 
-    fn __type(&self_, args) {
-        let name = args["name"].as_str().unwrap();
-        Ok(type_def(self_.0, name))
+    fn skip_field(&self, field_name: &str) -> bool {
+        !matches!(field_name, "__schema" | "__type" | "__typename")
     }
 }
 
 impl_resolver! {
-    for SchemaWithCache<'_>:
+    for SchemaWithImplementersMap<'_>:
 
     __typename = "__Schema";
 
diff --git a/crates/apollo-compiler/src/lib.rs b/crates/apollo-compiler/src/lib.rs
index 9a51191..893ee76 100644
--- a/crates/apollo-compiler/src/lib.rs
+++ b/crates/apollo-compiler/src/lib.rs
@@ -1,4 +1,5 @@
 #![doc = include_str!("../README.md")]
+#![deny(unreachable_pub)]
 
 #[macro_use]
 mod macros;
@@ -7,10 +8,14 @@ pub mod collections;
 pub mod coordinate;
 pub mod diagnostic;
 pub mod executable;
-pub mod execution;
+#[macro_use]
+mod execution;
+pub mod introspection;
 mod name;
 mod node;
 pub mod parser;
+pub mod request;
+pub mod response;
 pub mod schema;
 pub mod validation;
 
diff --git a/crates/apollo-compiler/src/request.rs b/crates/apollo-compiler/src/request.rs
new file mode 100644
index 0000000..8279469
--- /dev/null
+++ b/crates/apollo-compiler/src/request.rs
@@ -0,0 +1,92 @@
+//! GraphQL [requests](https://spec.graphql.org/draft/#request)
+//!
+//! This exists primarily to support [`introspection::partial_execute`].
+
+use crate::executable::Operation;
+use crate::execution::input_coercion::InputCoercionError;
+#[cfg(doc)]
+use crate::introspection;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
+use crate::response::GraphQLError;
+use crate::response::JsonMap;
+use crate::validation::SuspectedValidationBug;
+use crate::validation::Valid;
+use crate::Schema;
+
+/// Coerce the values of [variables](https://spec.graphql.org/draft/#sec-Language.Variables)
+/// from a GraphQL request to the types expected by the operation.
+///
+/// This is [_CoerceVariableValues()_](https://spec.graphql.org/October2021/#CoerceVariableValues())
+/// in the GraphQL specification.
+///
+/// Returns a [request error](https://spec.graphql.org/draft/#sec-Errors.Request-Errors)
+/// if a value as an incompatible type, or if a required variable is not provided.
+pub fn coerce_variable_values(
+    schema: &Valid<Schema>,
+    operation: &Operation,
+    values: &JsonMap,
+) -> Result<Valid<JsonMap>, RequestError> {
+    Ok(crate::execution::input_coercion::coerce_variable_values(
+        schema, operation, values,
+    )?)
+}
+
+/// A [request error](https://spec.graphql.org/draft/#sec-Errors.Request-Errors) is an error
+/// raised during an early phase of the [execution](https://spec.graphql.org/draft/#sec-Execution)
+/// to indicate that the request as a whole is considered faulty.
+///
+/// A request error should cause the rest of execution to be aborted,
+/// and result in a GraphQL response that does not have a `data` key.
+/// This differs from a response with `"data": null` which can happen with
+/// a [field error](https://spec.graphql.org/draft/#sec-Errors.Field-Errors)
+/// on a non-null field whose ancestors fields are all also non-null.
+/// In that case the `null` value
+/// is [propagated](https://spec.graphql.org/draft/#sec-Handling-Field-Errors)
+/// all the way to the entire response data.
+#[derive(Debug, Clone)]
+pub struct RequestError {
+    pub(crate) message: String,
+    pub(crate) location: Option<SourceSpan>,
+    pub(crate) is_suspected_validation_bug: bool,
+}
+
+impl From<InputCoercionError> for RequestError {
+    fn from(error: InputCoercionError) -> Self {
+        match error {
+            InputCoercionError::SuspectedValidationBug(SuspectedValidationBug {
+                message,
+                location,
+            }) => Self {
+                message,
+                location,
+                is_suspected_validation_bug: true,
+            },
+            InputCoercionError::ValueError { message, location } => Self {
+                message,
+                location,
+                is_suspected_validation_bug: false,
+            },
+        }
+    }
+}
+
+impl RequestError {
+    pub fn message(&self) -> impl std::fmt::Display + '_ {
+        &self.message
+    }
+
+    pub fn location(&self) -> Option<SourceSpan> {
+        self.location
+    }
+
+    pub fn to_graphql_error(&self, sources: &SourceMap) -> GraphQLError {
+        let mut error = GraphQLError::new(&self.message, self.location, sources);
+        if self.is_suspected_validation_bug {
+            error
+                .extensions
+                .insert("APOLLO_SUSPECTED_VALIDATION_BUG", true.into());
+        }
+        error
+    }
+}
diff --git a/crates/apollo-compiler/src/response.rs b/crates/apollo-compiler/src/response.rs
new file mode 100644
index 0000000..42d45f9
--- /dev/null
+++ b/crates/apollo-compiler/src/response.rs
@@ -0,0 +1,123 @@
+//! GraphQL [responses](https://spec.graphql.org/draft/#sec-Response)
+//!
+//! This exists primarily to support [`introspection::partial_execute`].
+
+#[cfg(doc)]
+use crate::introspection;
+use crate::parser::LineColumn;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
+use serde::Deserialize;
+use serde::Serialize;
+/// Re-export of the version of the `serde_json_bytes` crate used for [`JsonValue`] and [`JsonMap`]
+pub use serde_json_bytes;
+
+/// A JSON-compatible dynamically-typed value.
+///
+/// Note: [`serde_json_bytes::Value`] is similar
+/// to [`serde_json::Value`][serde_json_bytes::serde_json::Value]
+/// but uses its reference-counted [`ByteString`][serde_json_bytes::ByteString]
+/// for string values and map keys.
+pub type JsonValue = serde_json_bytes::Value;
+
+/// A JSON-compatible object/map with string keys and dynamically-typed values.
+pub type JsonMap = serde_json_bytes::Map<serde_json_bytes::ByteString, JsonValue>;
+
+/// A [response](https://spec.graphql.org/October2021/#sec-Response-Format)
+/// to a GraphQL request that did not cause any [request error][crate::request::RequestError]
+/// and started [execution](https://spec.graphql.org/draft/#sec-Execution)
+/// of selection sets and fields.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct ExecutionResponse {
+    // <https://spec.graphql.org/October2021/#note-6f005> suggests serializing this first
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    #[serde(default)]
+    pub errors: Vec<GraphQLError>,
+
+    pub data: Option<JsonMap>,
+}
+
+/// A serializable [error](https://spec.graphql.org/October2021/#sec-Errors.Error-result-format),
+/// as found in a GraphQL response.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct GraphQLError {
+    /// The error message.
+    pub message: String,
+
+    /// Locations in relevant to the error, if any.
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    #[serde(default)]
+    pub locations: Vec<LineColumn>,
+
+    /// If non-empty, the error is a [field error]
+    /// for the particular field found at this path in [`ExecutionResponse::data`].
+    ///
+    /// [field error]: https://spec.graphql.org/October2021/#sec-Errors.Field-errors
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    #[serde(default)]
+    pub path: Vec<ResponseDataPathSegment>,
+
+    /// Reserved for any additional information
+    #[serde(skip_serializing_if = "JsonMap::is_empty")]
+    #[serde(default)]
+    pub extensions: JsonMap,
+}
+
+/// A `Vec<ResponseDataPathSegment>` like in [`GraphQLError::path`]
+/// represents a [path](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format)
+/// into [`ExecutionResponse::data`],
+/// starting at the root and indexing into increasingly nested JSON objects or arrays.
+///
+/// # Example
+///
+/// In a GraphQL response like this:
+///
+/// ```json
+/// {
+///   "data": {
+///     "players": [
+///       {"name": "Alice"},
+///       {"name": "Bob"}
+///     ]
+///   },
+///   "errors": [
+///     {
+///       "message": "Something went wrong",
+///       "path": ["players", 1, "name"]
+///     }
+///   ]
+/// }
+/// ```
+///
+/// The error path would have a Rust representation like
+/// `vec![Field("players"), ListIndex(1), Field("name")]`
+/// and designate the value `"name": "Bob"`.
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum ResponseDataPathSegment {
+    /// The relevant key in an object value
+    Field(crate::Name),
+
+    /// The index of the relevant item in a list value
+    ListIndex(usize),
+}
+
+impl GraphQLError {
+    pub fn new(
+        message: impl Into<String>,
+        location: Option<SourceSpan>,
+        sources: &SourceMap,
+    ) -> Self {
+        Self {
+            message: message.into(),
+            locations: location
+                .into_iter()
+                .filter_map(|location| location.line_column(sources))
+                .collect(),
+            path: Default::default(),
+            extensions: Default::default(),
+        }
+    }
+}
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index f520a56..35f93e0 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -33,10 +33,9 @@ use crate::executable::ConflictingFieldArgument;
 use crate::executable::ConflictingFieldName;
 use crate::executable::ConflictingFieldType;
 use crate::executable::VariableDefinition;
-use crate::execution::GraphQLError;
-use crate::execution::Response;
 use crate::parser::SourceMap;
 use crate::parser::SourceSpan;
+use crate::response::GraphQLError;
 use crate::schema::BuildError as SchemaBuildError;
 use crate::schema::Implementers;
 use crate::Name;
@@ -54,7 +53,7 @@ use std::sync::OnceLock;
 /// * [`Schema::validate`]
 /// * [`ExecutableDocument::parse_and_validate`]
 /// * [`ExecutableDocument::validate`]
-/// * [`coerce_variable_values`][crate::execution::coerce_variable_values]
+/// * [`coerce_variable_values`][crate::request::coerce_variable_values]
 ///
 /// … or by explicitly skipping it with [`Valid::assume_valid`].
 ///
@@ -132,7 +131,7 @@ pub(crate) struct ExecutableValidationContext<'a> {
 }
 
 impl<'a> ExecutableValidationContext<'a> {
-    pub fn new(schema: Option<&'a Schema>) -> Self {
+    pub(crate) fn new(schema: Option<&'a Schema>) -> Self {
         Self {
             schema,
             implementers_map: Default::default(),
@@ -140,12 +139,12 @@ impl<'a> ExecutableValidationContext<'a> {
     }
 
     /// Returns the schema to validate against, if any.
-    pub fn schema(&self) -> Option<&'a Schema> {
+    pub(crate) fn schema(&self) -> Option<&'a Schema> {
         self.schema
     }
 
     /// Returns a cached reference to the implementers map.
-    pub fn implementers_map(&self) -> &HashMap<Name, Implementers> {
+    pub(crate) fn implementers_map(&self) -> &HashMap<Name, Implementers> {
         self.implementers_map.get_or_init(|| {
             self.schema
                 .map(|schema| schema.implementers_map())
@@ -154,7 +153,7 @@ impl<'a> ExecutableValidationContext<'a> {
     }
 
     /// Returns a context for operation validation.
-    pub fn operation_context<'o>(
+    pub(crate) fn operation_context<'o>(
         &'o self,
         variables: &'o [Node<VariableDefinition>],
     ) -> OperationValidationContext<'o> {
@@ -176,12 +175,12 @@ pub(crate) struct OperationValidationContext<'a> {
 }
 
 impl<'a> OperationValidationContext<'a> {
-    pub fn schema(&self) -> Option<&'a Schema> {
+    pub(crate) fn schema(&self) -> Option<&'a Schema> {
         self.executable.schema
     }
 
     /// Returns a cached reference to the implementers map.
-    pub fn implementers_map(&self) -> &HashMap<Name, Implementers> {
+    pub(crate) fn implementers_map(&self) -> &HashMap<Name, Implementers> {
         self.executable.implementers_map()
     }
 }
@@ -225,30 +224,11 @@ impl<T> fmt::Display for WithErrors<T> {
 /// which populates [`extensions`][GraphQLError::extensions]
 /// with a `"APOLLO_SUSPECTED_VALIDATION_BUG": true` entry.
 #[derive(Debug, Clone)]
-pub struct SuspectedValidationBug {
+pub(crate) struct SuspectedValidationBug {
     pub message: String,
     pub location: Option<SourceSpan>,
 }
 
-impl SuspectedValidationBug {
-    /// Convert into a JSON-serializable error as represented in a GraphQL response
-    pub fn into_graphql_error(self, sources: &SourceMap) -> GraphQLError {
-        let Self { message, location } = self;
-        let mut err = GraphQLError::new(message, location, sources);
-        err.extensions
-            .insert("APOLLO_SUSPECTED_VALIDATION_BUG", true.into());
-        err
-    }
-
-    /// Convert into a response with this error as a [request error]
-    /// that prevented execution from starting.
-    ///
-    /// [request error]: https://spec.graphql.org/October2021/#sec-Errors.Request-errors
-    pub fn into_response(self, sources: &SourceMap) -> Response {
-        Response::from_request_error(self.into_graphql_error(sources))
-    }
-}
-
 /// A collection of diagnostics returned by some validation method
 #[derive(Clone)]
 pub struct DiagnosticList {
diff --git a/crates/apollo-compiler/src/validation/selection.rs b/crates/apollo-compiler/src/validation/selection.rs
index 95ed545..fb6d3a0 100644
--- a/crates/apollo-compiler/src/validation/selection.rs
+++ b/crates/apollo-compiler/src/validation/selection.rs
@@ -55,7 +55,7 @@ impl<'a> FieldSelection<'a> {
         }
     }
 
-    pub fn coordinate(&self) -> TypeAttributeCoordinate {
+    pub(crate) fn coordinate(&self) -> TypeAttributeCoordinate {
         TypeAttributeCoordinate {
             ty: self.parent_type.clone(),
             attribute: self.field.name.clone(),


Test patch:
diff --git a/crates/apollo-compiler/tests/introspection.rs b/crates/apollo-compiler/tests/introspection.rs
index 0add06d..021af68 100644
--- a/crates/apollo-compiler/tests/introspection.rs
+++ b/crates/apollo-compiler/tests/introspection.rs
@@ -1,10 +1,9 @@
 use apollo_compiler::ast::FieldDefinition;
 use apollo_compiler::ast::InputValueDefinition;
-use apollo_compiler::execution::coerce_variable_values;
-use apollo_compiler::execution::JsonMap;
-use apollo_compiler::execution::Response;
-use apollo_compiler::execution::SchemaIntrospectionQuery;
+use apollo_compiler::introspection;
 use apollo_compiler::name;
+use apollo_compiler::request::coerce_variable_values;
+use apollo_compiler::response::JsonMap;
 use apollo_compiler::schema::ExtendedType;
 use apollo_compiler::ty;
 use apollo_compiler::ExecutableDocument;
@@ -59,26 +58,14 @@ fn test() {
             ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
         let operation = document.operations.get(None).unwrap();
         let variables = coerce_variable_values(&schema, operation, &variables).unwrap();
-        let response = SchemaIntrospectionQuery::split_and_execute(
+        let response = introspection::partial_execute(
             &schema,
+            &schema.implementers_map(),
             &document,
             operation,
             &variables,
-            |non_introspection_document| Response {
-                errors: Default::default(),
-                data: apollo_compiler::execution::ResponseData::Object(Default::default()),
-                extensions: [(
-                    "NON_INTROSPECTION".into(),
-                    non_introspection_document
-                        .serialize()
-                        .no_indent()
-                        .to_string()
-                        .into(),
-                )]
-                .into_iter()
-                .collect(),
-            },
-        );
+        )
+        .unwrap();
         serde_json::to_string_pretty(&response).unwrap()
     };
 
diff --git a/crates/apollo-compiler/tests/introspection_max_depth.rs b/crates/apollo-compiler/tests/introspection_max_depth.rs
index 1ed4dc0..d3aba09 100644
--- a/crates/apollo-compiler/tests/introspection_max_depth.rs
+++ b/crates/apollo-compiler/tests/introspection_max_depth.rs
@@ -1,4 +1,4 @@
-use apollo_compiler::execution::SchemaIntrospectionSplit;
+use apollo_compiler::introspection;
 use apollo_compiler::ExecutableDocument;
 use apollo_compiler::Schema;
 use expect_test::expect;
@@ -368,8 +368,8 @@ fn assert_split(doc: &str, expected: expect_test::Expect) {
     let doc = ExecutableDocument::parse_and_validate(&schema, doc, "doc.graphql").unwrap();
     let operation = doc.operations.get(None).unwrap();
 
-    match SchemaIntrospectionSplit::split(&schema, &doc, operation) {
+    match introspection::check_max_depth(&doc, operation) {
         Ok(_) => expected.assert_eq("Ok"),
-        Err(err) => expected.assert_debug_eq(&err.into_graphql_error(&doc.sources)),
+        Err(err) => expected.assert_debug_eq(&err.to_graphql_error(&doc.sources)),
     }
 }
diff --git a/crates/apollo-compiler/tests/introspection_split.rs b/crates/apollo-compiler/tests/introspection_split.rs
deleted file mode 100644
index 478233f..0000000
--- a/crates/apollo-compiler/tests/introspection_split.rs
+++ /dev/null
@@ -1,370 +0,0 @@
-use apollo_compiler::execution::SchemaIntrospectionSplit;
-use apollo_compiler::ExecutableDocument;
-use apollo_compiler::Schema;
-use expect_test::expect;
-
-const SCHEMA: &str = r#"
-    schema {
-        query: Fibonacci
-        mutation: Fibonacci
-    }
-
-    type Fibonacci {
-        value: Int
-        next(n: Int = 1): Fibonacci
-    }
-
-    input In {
-        list: [Int]
-    }
-
-    directive @someDir(arg: In) on FRAGMENT_DEFINITION
-"#;
-
-#[test]
-fn test_none() {
-    let doc = r#"
-        query TheOperation { value }
-        query OtherOperation { __schema { description } }
-    "#;
-    let expected = expect!["None"];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_only() {
-    let doc = r#"
-        query TheOperation { __schema { description } }
-        query OtherOperation { value }
-    "#;
-    let expected = expect![[r#"
-        Only:
-        query TheOperation {
-          __schema {
-            description
-          }
-        }
-    "#]];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_both() {
-    let doc = r#"
-        query TheOperation { value __schema { description } }
-    "#;
-    let expected = expect![[r#"
-        Introspection parts:
-        query TheOperation {
-          __schema {
-            description
-          }
-        }
-
-        Other parts:
-        query TheOperation {
-          value
-        }
-    "#]];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_nested() {
-    let doc = r#"
-        query TheOperation { value next { __schema { description } } }
-    "#;
-    let expected = expect![[r#"
-        GraphQLError {
-            message: "Schema introspection field __schema is not supported nested in other fields",
-            locations: [
-                2:43,
-            ],
-            path: [],
-            extensions: {},
-        }
-    "#]];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_nested_in_fragment() {
-    let doc = r#"
-        query TheOperation { ...A next { ...A } }
-        fragment A on Fibonacci { value __schema { description } }
-    "#;
-    let expected = expect![[r#"
-        GraphQLError {
-            message: "Schema introspection field __schema is not supported nested in other fields",
-            locations: [
-                3:41,
-            ],
-            path: [],
-            extensions: {},
-        }
-    "#]];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_mutation() {
-    let doc = r#"
-        mutation TheOperation { __schema { description } }
-    "#;
-    let expected = expect![[r#"
-        GraphQLError {
-            message: "Schema introspection field __schema is not supported in a mutation operation",
-            locations: [
-                2:33,
-            ],
-            path: [],
-            extensions: {},
-        }
-    "#]];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_mutation_nested() {
-    let doc = r#"
-        mutation TheOperation { value next { __schema { description } } }
-    "#;
-    let expected = expect![[r#"
-        GraphQLError {
-            message: "Schema introspection field __schema is not supported in a mutation operation",
-            locations: [
-                2:46,
-            ],
-            path: [],
-            extensions: {},
-        }
-    "#]];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_nested_in_other_operation() {
-    let doc = r#"
-        query TheOperation { value }
-        query OtherOperation { value next { __schema { description } } }
-    "#;
-    let expected = expect!["None"];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_none_with_fragments() {
-    let doc = r#"
-        query TheOperation { ... { ...A } }
-        query OtherOperation { ... { ...C } }
-        fragment A on Fibonacci { ...B }
-        fragment B on Fibonacci { ... { value } }
-        fragment C on Fibonacci { ...D }
-        fragment D on Fibonacci { ... { __schema { description } } }
-    "#;
-    let expected = expect!["None"];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_only_with_fragments() {
-    let doc = r#"
-        query TheOperation { ... { ...A } }
-        query OtherOperation { ... { ...C } }
-        fragment A on Fibonacci { ...B }
-        fragment B on Fibonacci { ... { __schema { description } } }
-        fragment C on Fibonacci { ...D }
-        fragment D on Fibonacci { ... { value } }
-    "#;
-    let expected = expect![[r#"
-        Only:
-        query TheOperation {
-          ... {
-            ...A
-          }
-        }
-
-        fragment A on Fibonacci {
-          ...B
-        }
-
-        fragment B on Fibonacci {
-          ... {
-            __schema {
-              description
-            }
-          }
-        }
-    "#]];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_both_with_fragments() {
-    let doc = r#"
-        query TheOperation { ... { ...A } }
-        query OtherOperation { ...O }
-        fragment A on Fibonacci { ...B ...D }
-        fragment B on Fibonacci { value next { ... { ...C } } }
-        fragment C on Fibonacci { value }
-        fragment D on Fibonacci { __schema { ...S } }
-        fragment O on Fibonacci { __typename }
-        fragment S on __Schema { description }
-    "#;
-    let expected = expect![[r#"
-        Introspection parts:
-        query TheOperation {
-          ... {
-            ...A
-          }
-        }
-
-        fragment S on __Schema {
-          description
-        }
-
-        fragment D on Fibonacci {
-          __schema {
-            ...S
-          }
-        }
-
-        fragment A on Fibonacci {
-          ...D
-        }
-
-        Other parts:
-        query TheOperation {
-          ... {
-            ...A
-          }
-        }
-
-        fragment C on Fibonacci {
-          value
-        }
-
-        fragment B on Fibonacci {
-          value
-          next {
-            ... {
-              ...C
-            }
-          }
-        }
-
-        fragment A on Fibonacci {
-          ...B
-        }
-    "#]];
-    assert_split(doc, expected);
-}
-
-#[test]
-fn test_variables() {
-    let doc = r#"
-        query TheOperation(
-            $v1: Boolean!,
-            $v2: Boolean!,
-            $v3: Int!,
-            $v4: Boolean!,
-            $v5: Int!,
-            $v6: Boolean!,
-            $v7: Boolean!,
-            $v8: Int!,
-            $v9: Int!,
-            $v10: Boolean!,
-            $v11: Int!,
-            $v12: Boolean!,
-            $v13: Boolean!,
-        ) {
-            ... A @skip(if: $v1)
-            ... @skip(if: $v2) {
-                next(n: $v3) { ...B }
-            }
-            ... @skip(if: $v4) { ...A }
-        }
-        fragment A on Fibonacci @someDir(arg: {list: [$v5]}) {
-            value @skip(if: $v6)
-            __schema {
-                ...C @skip(if: $v7)
-            }
-        }
-        fragment B on Fibonacci @someDir(arg: {list: [$v8]}) {
-            next(n: $v9) {
-                value @skip(if: $v10)
-            }
-        }
-        fragment C on __Schema @someDir(arg: {list: [$v11]}) {
-            ... @skip(if: $v12) {
-                description @skip(if: $v13)
-            }
-        }
-    "#;
-    let expected = expect![[r#"
-        Introspection parts:
-        query TheOperation($v1: Boolean!, $v4: Boolean!, $v5: Int!, $v7: Boolean!, $v11: Int!, $v12: Boolean!, $v13: Boolean!) {
-          ...A @skip(if: $v1)
-          ... @skip(if: $v4) {
-            ...A
-          }
-        }
-
-        fragment C on __Schema @someDir(arg: {list: [$v11]}) {
-          ... @skip(if: $v12) {
-            description @skip(if: $v13)
-          }
-        }
-
-        fragment A on Fibonacci @someDir(arg: {list: [$v5]}) {
-          __schema {
-            ...C @skip(if: $v7)
-          }
-        }
-
-        Other parts:
-        query TheOperation($v1: Boolean!, $v2: Boolean!, $v3: Int!, $v4: Boolean!, $v5: Int!, $v6: Boolean!, $v8: Int!, $v9: Int!, $v10: Boolean!) {
-          ...A @skip(if: $v1)
-          ... @skip(if: $v2) {
-            next(n: $v3) {
-              ...B
-            }
-          }
-          ... @skip(if: $v4) {
-            ...A
-          }
-        }
-
-        fragment A on Fibonacci @someDir(arg: {list: [$v5]}) {
-          value @skip(if: $v6)
-        }
-
-        fragment B on Fibonacci @someDir(arg: {list: [$v8]}) {
-          next(n: $v9) {
-            value @skip(if: $v10)
-          }
-        }
-    "#]];
-    assert_split(doc, expected);
-}
-
-#[track_caller]
-fn assert_split(doc: &str, expected: expect_test::Expect) {
-    let schema = Schema::parse_and_validate(SCHEMA, "schema.graphql").unwrap();
-    let doc = ExecutableDocument::parse_and_validate(&schema, doc, "doc.graphql").unwrap();
-    let operation = doc.operations.get(Some("TheOperation")).unwrap();
-
-    match SchemaIntrospectionSplit::split(&schema, &doc, operation) {
-        Ok(SchemaIntrospectionSplit::None) => expected.assert_eq("None"),
-        Ok(SchemaIntrospectionSplit::Only(introspection_query)) => {
-            expected.assert_eq(&format!("Only:\n{introspection_query}"))
-        }
-        Ok(SchemaIntrospectionSplit::Both {
-            introspection_query,
-            filtered_document,
-        }) => expected.assert_eq(&format!(
-            "Introspection parts:\n{introspection_query}\nOther parts:\n{filtered_document}"
-        )),
-        Err(err) => expected.assert_debug_eq(&err.into_graphql_error(&doc.sources)),
-    }
-}
diff --git a/crates/apollo-compiler/tests/main.rs b/crates/apollo-compiler/tests/main.rs
index bd9e3c6..d18397f 100644
--- a/crates/apollo-compiler/tests/main.rs
+++ b/crates/apollo-compiler/tests/main.rs
@@ -4,7 +4,6 @@ mod field_set;
 mod field_type;
 mod introspection;
 mod introspection_max_depth;
-mod introspection_split;
 mod locations;
 mod merge_schemas;
 /// Formerly in src/lib.rs