Repository: apollographql/apollo-rs
Pull request #758 (2023-11-23 18:57:34)

Add schema introspection support and execution-related APIs
Changelog:

- Add data structure in `apollo_compiler::execution` for a GraphQL response, its data, and errors.
  All (de)serializable with `serde`.
- Add [`coerce_variable_values()`] in that same module.
- Add `apollo_compiler::execution::SchemaIntrospection`
  providing full execution for the [schema introspection] parts of an operation
  and separating the rest to be executed separately.
  In order to support all kinds of introspection queries this actually includes
  a full execution engine where users provide objects with resolvable fields.
  At this time this engine is not exposed in the public API.
  If you’re interested in it [let us know] about your use case!
- Add `ExecutableDocument::insert_operation` convenience method.

[`coerce_variable_values()`]: https://spec.graphql.org/October2021/#sec-Coercing-Variable-Values
[schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
[let us know]: https://github.com/apollographql/apollo-rs/issues/new?assignees=&labels=triage&projects=&template=feature_request.md

----

Replaces and closes https://github.com/apollographql/apollo-rs/pull/645: 
* The execution engine and introspection resolvers are copied from there. `async` was removed (since unused by introspection, the only user)
* The public API was reworked to be part of apollo-compiler
* The logic splitting an operation into introspection and non-introspection parts was rewritten two produce valid documents. This logic needs a lot more testing.

SimonSapin: I pushed a rework of error handling. `introspection_split.rs` still needs work to reject non-top-level introspection, but the rest of the PR can be considered ready for review.
SimonSapin: Alright, this is now fully ready for review! Please also take a look at `introspection_split.rs` and its tests.

Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index bb3909b..982039f 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -20,6 +20,18 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 # [x.x.x] (unreleased) - 2023-xx-xx
 
 ## Features
+- **Add execution-related and introspection functionality - [SimonSapin], [pull/758]:**
+  - Add data structure in `apollo_compiler::execution` for a GraphQL response, its data, and errors.
+    All (de)serializable with `serde`.
+  - Add [`coerce_variable_values()`] in that same module.
+  - Add `apollo_compiler::execution::SchemaIntrospection`
+    providing full execution for the [schema introspection] parts of an operation
+    and separating the rest to be executed separately.
+    In order to support all kinds of introspection queries this actually includes
+    a full execution engine where users provide objects with resolvable fields.
+    At this time this engine is not exposed in the public API.
+    If you’re interested in it [let us know] about your use case!
+  - Add `ExecutableDocument::insert_operation` convenience method.
 - **Add `NodeStr::from(Name)` - [goto-bus-stop], [pull/773]**
 - **Convenience accessors for `ast::Selection` enum - [SimonSapin], [pull/777]**
   `as_field`, `as_inline_fragment`, and `as_fragment_spread`; all returning `Option<&_>`.
@@ -30,9 +42,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 [goto-bus-stop]: https://github.com/goto-bus-stop]
 [SimonSapin]: https://github.com/SimonSapin
+[pull/758]: https://github.com/apollographql/apollo-rs/pull/758
 [pull/773]: https://github.com/apollographql/apollo-rs/pull/773
 [pull/774]: https://github.com/apollographql/apollo-rs/pull/774
 [pull/777]: https://github.com/apollographql/apollo-rs/pull/777
+[`coerce_variable_values()`]: https://spec.graphql.org/October2021/#sec-Coercing-Variable-Values
+[schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
+[let us know]: https://github.com/apollographql/apollo-rs/issues/new?assignees=&labels=triage&projects=&template=feature_request.md
 
 # [1.0.0-beta.10](https://crates.io/crates/apollo-compiler/1.0.0-beta.10) - 2023-12-04
 
diff --git a/crates/apollo-compiler/Cargo.toml b/crates/apollo-compiler/Cargo.toml
index f7a2bb4..d272dad 100644
--- a/crates/apollo-compiler/Cargo.toml
+++ b/crates/apollo-compiler/Cargo.toml
@@ -24,6 +24,7 @@ indexmap = "2.0.0"
 rowan = "0.15.5"
 salsa = "0.16.1"
 serde = { version = "1.0", features = ["derive"] }
+serde_json_bytes = { version = "0.2.2", features = ["preserve_order"] }
 thiserror = "1.0.31"
 triomphe = "0.1.9"
 # TODO: replace `sptr` with standard library methods when available:
diff --git a/crates/apollo-compiler/src/executable/mod.rs b/crates/apollo-compiler/src/executable/mod.rs
index 0f9d13f..3810886 100644
--- a/crates/apollo-compiler/src/executable/mod.rs
+++ b/crates/apollo-compiler/src/executable/mod.rs
@@ -241,7 +241,7 @@ impl ExecutableDocument {
 
     /// Return the relevant operation for a request, or a request error
     ///
-    /// This the [GetOperation](https://spec.graphql.org/October2021/#GetOperation())
+    /// This 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)
@@ -292,6 +292,20 @@ impl ExecutableDocument {
         .ok_or(GetOperationError())
     }
 
+    /// Insert the given operation in either `named_operations` or `anonymous_operation`
+    /// as appropriate, and return the old operation (if any) with that name (or lack thereof).
+    pub fn insert_operation(
+        &mut self,
+        operation: impl Into<Node<Operation>>,
+    ) -> Option<Node<Operation>> {
+        let operation = operation.into();
+        if let Some(name) = &operation.name {
+            self.named_operations.insert(name.clone(), operation)
+        } else {
+            self.anonymous_operation.replace(operation)
+        }
+    }
+
     serialize_method!();
 }
 
diff --git a/crates/apollo-compiler/src/execution/engine.rs b/crates/apollo-compiler/src/execution/engine.rs
new file mode 100644
index 0000000..05c12b6
--- /dev/null
+++ b/crates/apollo-compiler/src/execution/engine.rs
@@ -0,0 +1,327 @@
+use crate::ast::Name;
+use crate::ast::Value;
+use crate::executable::Field;
+use crate::executable::Selection;
+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::node::NodeLocation;
+use crate::schema::ExtendedType;
+use crate::schema::FieldDefinition;
+use crate::schema::ObjectType;
+use crate::schema::Type;
+use crate::validation::SuspectedValidationBug;
+use crate::validation::Valid;
+use crate::ExecutableDocument;
+use crate::Schema;
+use crate::SourceMap;
+use indexmap::IndexMap;
+use std::collections::HashSet;
+
+/// <https://spec.graphql.org/October2021/#sec-Normal-and-Serial-Execution>
+#[derive(Debug, Copy, Clone)]
+pub(crate) enum ExecutionMode {
+    /// Allowed to resolve fields in any order, including in parellel
+    Normal,
+    /// Top-level fields of a mutation operation must be executed in order
+    #[allow(unused)]
+    Sequential,
+}
+
+/// Return in `Err` when a field error occurred at some non-nullable place
+///
+/// <https://spec.graphql.org/October2021/#sec-Handling-Field-Errors>
+pub(crate) struct PropagateNull;
+
+/// Linked-list version of `Vec<PathElement>`, taking advantage of the call stack
+pub(crate) type LinkedPath<'a> = Option<&'a LinkedPathElement<'a>>;
+
+pub(crate) struct LinkedPathElement<'a> {
+    pub(crate) element: ResponseDataPathElement,
+    pub(crate) next: LinkedPath<'a>,
+}
+
+/// <https://spec.graphql.org/October2021/#ExecuteSelectionSet()>
+#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
+pub(crate) fn execute_selection_set<'a>(
+    schema: &Valid<Schema>,
+    document: &'a Valid<ExecutableDocument>,
+    variable_values: &Valid<JsonMap>,
+    errors: &mut Vec<GraphQLError>,
+    path: LinkedPath<'_>,
+    mode: ExecutionMode,
+    object_type_name: &str,
+    object_type: &ObjectType,
+    object_value: &ObjectValue<'_>,
+    selections: impl IntoIterator<Item = &'a Selection>,
+) -> Result<JsonMap, PropagateNull> {
+    let mut grouped_field_set = IndexMap::new();
+    collect_fields(
+        schema,
+        document,
+        variable_values,
+        object_type_name,
+        object_type,
+        selections,
+        &mut HashSet::new(),
+        &mut grouped_field_set,
+    );
+
+    match mode {
+        ExecutionMode::Normal => {}
+        ExecutionMode::Sequential => {
+            // If we want parallelism, use `futures::future::join_all` (async)
+            // or Rayon’s `par_iter` (sync) here.
+        }
+    }
+
+    let mut response_map = JsonMap::with_capacity(grouped_field_set.len());
+    for (&response_key, fields) in &grouped_field_set {
+        // Indexing should not panic: `collect_fields` only creates a `Vec` to push to it
+        let field_name = &fields[0].name;
+        let Ok(field_def) = schema.type_field(object_type_name, field_name) else {
+            // TODO: Return a `validation_bug`` field error here?
+            // The spec specifically has a “If fieldType is defined” condition,
+            // but it being undefined would make the request invalid, right?
+            continue;
+        };
+        let value = if field_name == "__typename" {
+            JsonValue::from(object_type_name)
+        } else {
+            let field_path = LinkedPathElement {
+                element: ResponseDataPathElement::Field(response_key.clone()),
+                next: path,
+            };
+            execute_field(
+                schema,
+                document,
+                variable_values,
+                errors,
+                Some(&field_path),
+                mode,
+                object_value,
+                field_def,
+                fields,
+            )?
+        };
+        response_map.insert(response_key.as_str(), value);
+    }
+    Ok(response_map)
+}
+
+/// <https://spec.graphql.org/October2021/#CollectFields()>
+#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
+fn collect_fields<'a>(
+    schema: &Schema,
+    document: &'a ExecutableDocument,
+    variable_values: &Valid<JsonMap>,
+    object_type_name: &str,
+    object_type: &ObjectType,
+    selections: impl IntoIterator<Item = &'a Selection>,
+    visited_fragments: &mut HashSet<&'a Name>,
+    grouped_fields: &mut IndexMap<&'a Name, Vec<&'a Field>>,
+) {
+    for selection in selections {
+        if eval_if_arg(selection, "skip", variable_values).unwrap_or(false)
+            || !eval_if_arg(selection, "include", variable_values).unwrap_or(true)
+        {
+            continue;
+        }
+        match selection {
+            Selection::Field(field) => 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 {
+                    continue;
+                }
+                let Some(fragment) = document.fragments.get(&spread.fragment_name) else {
+                    continue;
+                };
+                if !does_fragment_type_apply(
+                    schema,
+                    object_type_name,
+                    object_type,
+                    fragment.type_condition(),
+                ) {
+                    continue;
+                }
+                collect_fields(
+                    schema,
+                    document,
+                    variable_values,
+                    object_type_name,
+                    object_type,
+                    &fragment.selection_set.selections,
+                    visited_fragments,
+                    grouped_fields,
+                )
+            }
+            Selection::InlineFragment(inline) => {
+                if let Some(condition) = &inline.type_condition {
+                    if !does_fragment_type_apply(schema, object_type_name, object_type, condition) {
+                        continue;
+                    }
+                }
+                collect_fields(
+                    schema,
+                    document,
+                    variable_values,
+                    object_type_name,
+                    object_type,
+                    &inline.selection_set.selections,
+                    visited_fragments,
+                    grouped_fields,
+                )
+            }
+        }
+    }
+}
+
+/// <https://spec.graphql.org/October2021/#DoesFragmentTypeApply()>
+fn does_fragment_type_apply(
+    schema: &Schema,
+    object_type_name: &str,
+    object_type: &ObjectType,
+    fragment_type: &Name,
+) -> bool {
+    match schema.types.get(fragment_type) {
+        Some(ExtendedType::Object(_)) => fragment_type == object_type_name,
+        Some(ExtendedType::Interface(_)) => {
+            object_type.implements_interfaces.contains(fragment_type)
+        }
+        Some(ExtendedType::Union(def)) => def.members.contains(object_type_name),
+        // Undefined or not an output type: validation should have caught this
+        _ => false,
+    }
+}
+
+fn eval_if_arg(
+    selection: &Selection,
+    directive_name: &str,
+    variable_values: &Valid<JsonMap>,
+) -> Option<bool> {
+    match selection
+        .directives()
+        .get(directive_name)?
+        .argument_by_name("if")?
+        .as_ref()
+    {
+        Value::Boolean(value) => Some(*value),
+        Value::Variable(var) => variable_values.get(var.as_str())?.as_bool(),
+        _ => None,
+    }
+}
+
+/// <https://spec.graphql.org/October2021/#ExecuteField()>
+#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
+fn execute_field(
+    schema: &Valid<Schema>,
+    document: &Valid<ExecutableDocument>,
+    variable_values: &Valid<JsonMap>,
+    errors: &mut Vec<GraphQLError>,
+    path: LinkedPath<'_>,
+    mode: ExecutionMode,
+    object_value: &ObjectValue<'_>,
+    field_def: &FieldDefinition,
+    fields: &[&Field],
+) -> Result<JsonValue, PropagateNull> {
+    let field = fields[0];
+    let argument_values = match coerce_argument_values(
+        schema,
+        document,
+        variable_values,
+        errors,
+        path,
+        field_def,
+        field,
+    ) {
+        Ok(argument_values) => argument_values,
+        Err(PropagateNull) => return try_nullify(&field_def.ty, Err(PropagateNull)),
+    };
+    let resolved_result = object_value.resolve_field(&field.name, &argument_values);
+    let completed_result = match resolved_result {
+        Ok(resolved) => complete_value(
+            schema,
+            document,
+            variable_values,
+            errors,
+            path,
+            mode,
+            field.ty(),
+            resolved,
+            fields,
+        ),
+        Err(ResolverError { message }) => {
+            errors.push(GraphQLError::field_error(
+                format!("resolver error: {message}"),
+                path,
+                field.name.location(),
+                &document.sources,
+            ));
+            Err(PropagateNull)
+        }
+    };
+    try_nullify(&field_def.ty, completed_result)
+}
+
+/// Try to insert a propagated null if possible, or keep propagating it.
+///
+/// <https://spec.graphql.org/October2021/#sec-Handling-Field-Errors>
+pub(crate) fn try_nullify(
+    ty: &Type,
+    result: Result<JsonValue, PropagateNull>,
+) -> Result<JsonValue, PropagateNull> {
+    match result {
+        Ok(json) => Ok(json),
+        Err(PropagateNull) => {
+            if ty.is_non_null() {
+                Err(PropagateNull)
+            } else {
+                Ok(JsonValue::Null)
+            }
+        }
+    }
+}
+
+pub(crate) fn path_to_vec(mut link: LinkedPath<'_>) -> Vec<ResponseDataPathElement> {
+    let mut path = Vec::new();
+    while let Some(node) = link {
+        path.push(node.element.clone());
+        link = node.next;
+    }
+    path.reverse();
+    path
+}
+
+impl GraphQLError {
+    pub(crate) fn field_error(
+        message: impl Into<String>,
+        path: LinkedPath<'_>,
+        location: Option<NodeLocation>,
+        sources: &SourceMap,
+    ) -> Self {
+        let mut err = Self::new(message, location, sources);
+        err.path = path_to_vec(path);
+        err
+    }
+}
+
+impl SuspectedValidationBug {
+    pub(crate) fn into_field_error(
+        self,
+        sources: &SourceMap,
+        path: LinkedPath<'_>,
+    ) -> GraphQLError {
+        let mut err = self.into_graphql_error(sources);
+        err.path = path_to_vec(path);
+        err
+    }
+}
diff --git a/crates/apollo-compiler/src/execution/input_coercion.rs b/crates/apollo-compiler/src/execution/input_coercion.rs
new file mode 100644
index 0000000..cfff687
--- /dev/null
+++ b/crates/apollo-compiler/src/execution/input_coercion.rs
@@ -0,0 +1,547 @@
+use crate::ast::Type;
+use crate::ast::Value;
+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::node::NodeLocation;
+use crate::schema::ExtendedType;
+use crate::schema::FieldDefinition;
+use crate::validation::SuspectedValidationBug;
+use crate::validation::Valid;
+use crate::ExecutableDocument;
+use crate::Node;
+use crate::Schema;
+use crate::SourceMap;
+use std::collections::HashMap;
+
+#[derive(Debug, Clone)]
+pub enum InputCoercionError {
+    SuspectedValidationBug(SuspectedValidationBug),
+    // TODO: split into more structured variants?
+    ValueError {
+        message: String,
+        location: Option<NodeLocation>,
+    },
+}
+
+/// 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(
+    schema: &Valid<Schema>,
+    operation: &Operation,
+    values: &JsonMap,
+) -> Result<Valid<JsonMap>, InputCoercionError> {
+    let mut coerced_values = JsonMap::new();
+    for variable_def in &operation.variables {
+        let name = variable_def.name.as_str();
+        if let Some((key, value)) = values.get_key_value(name) {
+            let value =
+                coerce_variable_value(schema, "variable", "", "", name, &variable_def.ty, value)?;
+            coerced_values.insert(key.clone(), value);
+        } else if let Some(default) = &variable_def.default_value {
+            let value = graphql_value_to_json("variable default value", "", "", name, default)?;
+            coerced_values.insert(name, value);
+        } else if variable_def.ty.is_non_null() {
+            return Err(InputCoercionError::ValueError {
+                message: format!("missing value for non-null variable '{name}'"),
+                location: variable_def.location(),
+            });
+        } else {
+            // Nullable variable with no provided value nor explicit default.
+            // Spec says nothing for this case, but for the similar case in input objects:
+            //
+            // > there is a semantic difference between the explicitly provided value null
+            // > versus having not provided a value
+        }
+    }
+    Ok(Valid(coerced_values))
+}
+
+#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
+fn coerce_variable_value(
+    schema: &Valid<Schema>,
+    kind: &str,
+    parent: &str,
+    sep: &str,
+    name: &str,
+    ty: &Type,
+    value: &JsonValue,
+) -> Result<JsonValue, InputCoercionError> {
+    if value.is_null() {
+        if ty.is_non_null() {
+            return Err(InputCoercionError::ValueError {
+                message: format!("null value for {kind} {parent}{sep}{name} of non-null type {ty}"),
+                location: None,
+            });
+        } else {
+            return Ok(JsonValue::Null);
+        }
+    }
+    let ty_name = match ty {
+        Type::List(inner) | Type::NonNullList(inner) => {
+            // https://spec.graphql.org/October2021/#sec-List.Input-Coercion
+            return value
+                .as_array()
+                .map(Vec::as_slice)
+                // If not an array, treat the value as an array of size one:
+                .unwrap_or(std::slice::from_ref(value))
+                .iter()
+                .map(|item| coerce_variable_value(schema, kind, parent, sep, name, inner, item))
+                .collect();
+        }
+        Type::Named(ty_name) | Type::NonNullNamed(ty_name) => ty_name,
+    };
+    let Some(ty_def) = schema.types.get(ty_name) else {
+        Err(SuspectedValidationBug {
+            message: format!("Undefined type {ty_name} for {kind} {parent}{sep}{name}"),
+            location: ty_name.location(),
+        })?
+    };
+    match ty_def {
+        ExtendedType::Object(_) | ExtendedType::Interface(_) | ExtendedType::Union(_) => {
+            Err(SuspectedValidationBug {
+                message: format!("Non-input type {ty_name} for {kind} {parent}{sep}{name}."),
+                location: ty_name.location(),
+            })?
+        }
+        ExtendedType::Scalar(_) => match ty_name.as_str() {
+            "Int" => {
+                // https://spec.graphql.org/October2021/#sec-Int.Input-Coercion
+                if value
+                    .as_i64()
+                    .is_some_and(|value| i32::try_from(value).is_ok())
+                {
+                    return Ok(value.clone());
+                }
+            }
+            "Float" => {
+                // https://spec.graphql.org/October2021/#sec-Float.Input-Coercion
+                if value.is_f64() {
+                    return Ok(value.clone());
+                }
+            }
+            "String" => {
+                // https://spec.graphql.org/October2021/#sec-String.Input-Coercion
+                if value.is_string() {
+                    return Ok(value.clone());
+                }
+            }
+            "Boolean" => {
+                // https://spec.graphql.org/October2021/#sec-Boolean.Input-Coercion
+                if value.is_boolean() {
+                    return Ok(value.clone());
+                }
+            }
+            "ID" => {
+                // https://spec.graphql.org/October2021/#sec-ID.Input-Coercion
+                if value.is_string() || value.is_i64() {
+                    return Ok(value.clone());
+                }
+            }
+            _ => {
+                // Custom scalar
+                // TODO: have a hook for coercion of custom scalars?
+                return Ok(value.clone());
+            }
+        },
+        ExtendedType::Enum(ty_def) => {
+            // https://spec.graphql.org/October2021/#sec-Enums.Input-Coercion
+            if let Some(str) = value.as_str() {
+                if ty_def.values.keys().any(|value_name| value_name == str) {
+                    return Ok(value.clone());
+                }
+            }
+        }
+        ExtendedType::InputObject(ty_def) => {
+            // https://spec.graphql.org/October2021/#sec-Input-Objects.Input-Coercion
+            if let Some(object) = value.as_object() {
+                if let Some(key) = object
+                    .keys()
+                    .find(|key| !ty_def.fields.contains_key(key.as_str()))
+                {
+                    return Err(InputCoercionError::ValueError {
+                        message: format!(
+                            "Input object has key {} not in type {ty_name}",
+                            key.as_str()
+                        ),
+                        location: None,
+                    });
+                }
+                let mut object = object.clone();
+                for (field_name, field_def) in &ty_def.fields {
+                    if let Some(field_value) = object.get_mut(field_name.as_str()) {
+                        *field_value = coerce_variable_value(
+                            schema,
+                            "input field",
+                            ty_name,
+                            ".",
+                            field_name,
+                            &field_def.ty,
+                            field_value,
+                        )?
+                    } else if let Some(default) = &field_def.default_value {
+                        let default = graphql_value_to_json(
+                            "input field",
+                            ty_name,
+                            ".",
+                            field_name,
+                            default,
+                        )?;
+                        object.insert(field_name.as_str(), default);
+                    } else if field_def.ty.is_non_null() {
+                        return Err(InputCoercionError::ValueError {
+                            message: format!("Missing value for non-null input object field {ty_name}.{field_name}"),
+                            location: None,
+                        });
+                    } else {
+                        // Field not required
+                    }
+                }
+                return Ok(object.into());
+            }
+        }
+    }
+    Err(InputCoercionError::ValueError {
+        message: format!("Could not coerce {kind} {parent}{sep}{name}: {value} to type {ty_name}"),
+        location: None,
+    })
+}
+
+fn graphql_value_to_json(
+    kind: &str,
+    parent: &str,
+    sep: &str,
+    name: &str,
+    value: &Node<Value>,
+) -> Result<JsonValue, InputCoercionError> {
+    match value.as_ref() {
+        Value::Null => Ok(JsonValue::Null),
+        Value::Variable(_) => {
+            // TODO: separate `ContValue` enum without this variant?
+            Err(InputCoercionError::SuspectedValidationBug(
+                SuspectedValidationBug {
+                    message: format!("Variable in default value of {kind} {parent}{sep}{name}."),
+                    location: value.location(),
+                },
+            ))
+        }
+        Value::Enum(value) => Ok(value.as_str().into()),
+        Value::String(value) => Ok(value.as_str().into()),
+        Value::Boolean(value) => Ok((*value).into()),
+        // Rely on `serde_json::Number`’s own parser to use whatever precision it supports
+        Value::Int(i) => Ok(JsonValue::Number(i.as_str().parse().map_err(|_| {
+            InputCoercionError::ValueError {
+                message: format!("IntValue overflow in {kind} {parent}{sep}{name}"),
+                location: value.location(),
+            }
+        })?)),
+        Value::Float(f) => Ok(JsonValue::Number(f.as_str().parse().map_err(|_| {
+            InputCoercionError::ValueError {
+                message: format!("FloatValue overflow in {kind} {parent}{sep}{name}"),
+                location: value.location(),
+            }
+        })?)),
+        Value::List(value) => value
+            .iter()
+            .map(|value| graphql_value_to_json(kind, parent, sep, name, value))
+            .collect(),
+        Value::Object(value) => value
+            .iter()
+            .map(|(key, value)| {
+                Ok((
+                    key.as_str(),
+                    graphql_value_to_json(kind, parent, sep, name, value)?,
+                ))
+            })
+            .collect(),
+    }
+}
+
+/// <https://spec.graphql.org/October2021/#sec-Coercing-Field-Arguments>
+pub(crate) fn coerce_argument_values(
+    schema: &Schema,
+    document: &Valid<ExecutableDocument>,
+    variable_values: &Valid<JsonMap>,
+    errors: &mut Vec<GraphQLError>,
+    path: LinkedPath<'_>,
+    field_def: &FieldDefinition,
+    field: &Field,
+) -> Result<JsonMap, PropagateNull> {
+    let mut coerced_values = JsonMap::new();
+    for arg_def in &field_def.arguments {
+        let arg_name = &arg_def.name;
+        if let Some(arg) = field.arguments.iter().find(|arg| arg.name == *arg_name) {
+            if let Value::Variable(var_name) = arg.value.as_ref() {
+                if let Some(var_value) = variable_values.get(var_name.as_str()) {
+                    if var_value.is_null() && arg_def.ty.is_non_null() {
+                        errors.push(GraphQLError::field_error(
+                            format!("null value for non-nullable argument {arg_name}"),
+                            path,
+                            arg_def.location(),
+                            &document.sources,
+                        ));
+                        return Err(PropagateNull);
+                    } else {
+                        coerced_values.insert(arg_name.as_str(), var_value.clone());
+                        continue;
+                    }
+                }
+            } else if arg.value.is_null() && arg_def.ty.is_non_null() {
+                errors.push(GraphQLError::field_error(
+                    format!("null value for non-nullable argument {arg_name}"),
+                    path,
+                    arg_def.location(),
+                    &document.sources,
+                ));
+                return Err(PropagateNull);
+            } else {
+                let coerced_value = coerce_argument_value(
+                    schema,
+                    document,
+                    variable_values,
+                    errors,
+                    path,
+                    "argument",
+                    "",
+                    "",
+                    arg_name,
+                    &arg_def.ty,
+                    &arg.value,
+                )?;
+                coerced_values.insert(arg_name.as_str(), coerced_value);
+                continue;
+            }
+        }
+        if let Some(default) = &arg_def.default_value {
+            let value =
+                graphql_value_to_json("argument", "", "", arg_name, default).map_err(|err| {
+                    errors.push(err.into_field_error(path, &document.sources));
+                    PropagateNull
+                })?;
+            coerced_values.insert(arg_def.name.as_str(), value);
+            continue;
+        }
+        if arg_def.ty.is_non_null() {
+            errors.push(GraphQLError::field_error(
+                format!("missing value for required argument {arg_name}"),
+                path,
+                arg_def.location(),
+                &document.sources,
+            ));
+            return Err(PropagateNull);
+        }
+    }
+    Ok(coerced_values)
+}
+
+#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
+fn coerce_argument_value(
+    schema: &Schema,
+    document: &Valid<ExecutableDocument>,
+    variable_values: &Valid<JsonMap>,
+    errors: &mut Vec<GraphQLError>,
+    path: LinkedPath<'_>,
+    kind: &str,
+    parent: &str,
+    sep: &str,
+    name: &str,
+    ty: &Type,
+    value: &Node<Value>,
+) -> Result<JsonValue, PropagateNull> {
+    if value.is_null() {
+        if ty.is_non_null() {
+            errors.push(GraphQLError::field_error(
+                format!("null value for non-null {kind} {parent}{sep}{name}"),
+                path,
+                value.location(),
+                &document.sources,
+            ));
+            return Err(PropagateNull);
+        } else {
+            return Ok(JsonValue::Null);
+        }
+    }
+    if let Some(var_name) = value.as_variable() {
+        if let Some(var_value) = variable_values.get(var_name.as_str()) {
+            if var_value.is_null() && ty.is_non_null() {
+                errors.push(GraphQLError::field_error(
+                    format!("null variable value for non-null {kind} {parent}{sep}{name}"),
+                    path,
+                    value.location(),
+                    &document.sources,
+                ));
+                return Err(PropagateNull);
+            } else {
+                return Ok(var_value.clone());
+            }
+        } else if ty.is_non_null() {
+            errors.push(GraphQLError::field_error(
+                format!("missing variable for non-null {kind} {parent}{sep}{name}"),
+                path,
+                value.location(),
+                &document.sources,
+            ));
+            return Err(PropagateNull);
+        } else {
+            return Ok(JsonValue::Null);
+        }
+    }
+    let ty_name = match ty {
+        Type::List(inner_ty) | Type::NonNullList(inner_ty) => {
+            // https://spec.graphql.org/October2021/#sec-List.Input-Coercion
+            return value
+                .as_list()
+                // If not an array, treat the value as an array of size one:
+                .unwrap_or(std::slice::from_ref(value))
+                .iter()
+                .map(|item| {
+                    coerce_argument_value(
+                        schema,
+                        document,
+                        variable_values,
+                        errors,
+                        path,
+                        kind,
+                        parent,
+                        sep,
+                        name,
+                        inner_ty,
+                        item,
+                    )
+                })
+                .collect();
+        }
+        Type::Named(ty_name) | Type::NonNullNamed(ty_name) => ty_name,
+    };
+    let Some(ty_def) = schema.types.get(ty_name) else {
+        errors.push(
+            SuspectedValidationBug {
+                message: format!("Undefined type {ty_name} for {kind} {parent}{sep}{name}"),
+                location: value.location(),
+            }
+            .into_field_error(&document.sources, path),
+        );
+        return Err(PropagateNull);
+    };
+    match ty_def {
+        ExtendedType::InputObject(ty_def) => {
+            // https://spec.graphql.org/October2021/#sec-Input-Objects.Input-Coercion
+            if let Some(object) = value.as_object() {
+                if let Some((key, _value)) = object
+                    .iter()
+                    .find(|(key, _value)| !ty_def.fields.contains_key(key))
+                {
+                    errors.push(GraphQLError::field_error(
+                        format!("Input object has key {key} not in type {ty_name}",),
+                        path,
+                        value.location(),
+                        &document.sources,
+                    ));
+                    return Err(PropagateNull);
+                }
+                // `map` converts `&(k, v)` to `(&k, &v)`
+                let object: HashMap<_, _> = object.iter().map(|(k, v)| (k, v)).collect();
+                let mut coerced_object = JsonMap::new();
+                for (field_name, field_def) in &ty_def.fields {
+                    if let Some(field_value) = object.get(field_name) {
+                        let coerced_value = coerce_argument_value(
+                            schema,
+                            document,
+                            variable_values,
+                            errors,
+                            path,
+                            "input field",
+                            ty_name,
+                            ".",
+                            field_name,
+                            &field_def.ty,
+                            field_value,
+                        )?;
+                        coerced_object.insert(field_name.as_str(), coerced_value);
+                    } else if let Some(default) = &field_def.default_value {
+                        let default =
+                            graphql_value_to_json("input field", ty_name, ".", field_name, default)
+                                .map_err(|err| {
+                                    errors.push(err.into_field_error(path, &document.sources));
+                                    PropagateNull
+                                })?;
+                        coerced_object.insert(field_name.as_str(), default);
+                    } else if field_def.ty.is_non_null() {
+                        errors.push(GraphQLError::field_error(
+                            format!(
+                                "Missing value for non-null input object field {ty_name}.{field_name}"
+                            ),
+                            path,
+                            value.location(),
+                            &document.sources,
+                        ));
+                        return Err(PropagateNull);
+                    } else {
+                        // Field not required
+                    }
+                }
+                return Ok(coerced_object.into());
+            }
+        }
+        _ => {
+            // For scalar and enums, rely and validation and just convert between Rust types
+            return graphql_value_to_json(kind, parent, sep, name, value).map_err(|err| {
+                errors.push(err.into_field_error(path, &document.sources));
+                PropagateNull
+            });
+        }
+    }
+    errors.push(GraphQLError::field_error(
+        format!("Could not coerce {kind} {parent}{sep}{name}: {value} to type {ty_name}"),
+        path,
+        value.location(),
+        &document.sources,
+    ));
+    Err(PropagateNull)
+}
+
+impl From<SuspectedValidationBug> for InputCoercionError {
+    fn from(value: SuspectedValidationBug) -> Self {
+        Self::SuspectedValidationBug(value)
+    }
+}
+
+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<'_>,
+        sources: &SourceMap,
+    ) -> GraphQLError {
+        match self {
+            Self::SuspectedValidationBug(s) => s.into_field_error(sources, path),
+            Self::ValueError { message, location } => {
+                GraphQLError::field_error(message, path, location, sources)
+            }
+        }
+    }
+}
diff --git a/crates/apollo-compiler/src/execution/introspection_execute.rs b/crates/apollo-compiler/src/execution/introspection_execute.rs
new file mode 100644
index 0000000..22e5edf
--- /dev/null
+++ b/crates/apollo-compiler/src/execution/introspection_execute.rs
@@ -0,0 +1,570 @@
+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::JsonMap;
+use crate::execution::Response;
+use crate::execution::SchemaIntrospectionSplit;
+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 std::borrow::Cow;
+use std::collections::HashMap;
+use std::collections::HashSet;
+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
+pub struct SchemaIntrospectionQuery(pub(crate) 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 {
+    /// 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)
+                .unwrap_or_else(|err| err.into_response(&document.sources)),
+            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)
+                    .unwrap_or_else(|err| err.into_response(&document.sources));
+                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>,
+    ) -> Result<Response, SuspectedValidationBug> {
+        let operation = self.0.get_operation(None).unwrap();
+        debug_assert_eq!(operation.operation_type, OperationType::Query);
+
+        // https://spec.graphql.org/October2021/#sec-Query
+        let object_type_name = operation.object_type();
+        let object_type_def =
+            schema
+                .get_object(object_type_name)
+                .ok_or_else(|| SuspectedValidationBug {
+                    message: format!(
+                    "Root operation type {object_type_name} is undefined or not an object type."
+                ),
+                    location: None,
+                })?;
+        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,
+            &self.0,
+            variable_values,
+            &mut errors,
+            path,
+            ExecutionMode::Normal,
+            object_type_name,
+            object_type_def,
+            initial_value,
+            &operation.selection_set.selections,
+        );
+        Ok(Response {
+            data: data.into(),
+            errors,
+            extensions: Default::default(),
+        })
+    }
+}
+
+#[derive(Clone, Copy)]
+struct SchemaWithCache<'a> {
+    schema: &'a Schema,
+    implementers_map: &'a OnceLock<HashMap<Name, HashSet<Name>>>,
+}
+
+impl<'a> SchemaWithCache<'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()
+            .flatten()
+    }
+}
+
+impl<'a> std::ops::Deref for SchemaWithCache<'a> {
+    type Target = &'a Schema;
+
+    fn deref(&self) -> &Self::Target {
+        &self.schema
+    }
+}
+
+struct IntrospectionRootResolver<'a>(SchemaWithCache<'a>);
+
+struct TypeDefResolver<'a> {
+    schema: SchemaWithCache<'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>,
+    ty: Cow<'a, schema::Type>,
+}
+
+struct DirectiveResolver<'a> {
+    schema: SchemaWithCache<'a>,
+    def: &'a schema::DirectiveDefinition,
+}
+
+struct FieldResolver<'a> {
+    schema: SchemaWithCache<'a>,
+    def: &'a schema::FieldDefinition,
+}
+
+struct EnumValueResolver<'a> {
+    def: &'a schema::EnumValueDefinition,
+}
+
+struct InputValueResolver<'a> {
+    schema: SchemaWithCache<'a>,
+    def: &'a schema::InputValueDefinition,
+}
+
+fn type_def(schema: SchemaWithCache<'_>, name: impl AsRef<str>) -> ResolvedValue<'_> {
+    ResolvedValue::opt_object(
+        schema
+            .types
+            .get_key_value(name.as_ref())
+            .map(|(name, def)| TypeDefResolver { schema, name, def }),
+    )
+}
+
+fn type_def_opt<'a>(
+    schema: SchemaWithCache<'a>,
+    name: &Option<impl AsRef<str>>,
+) -> ResolvedValue<'a> {
+    if let Some(name) = name.as_ref() {
+        type_def(schema, name)
+    } else {
+        ResolvedValue::null()
+    }
+}
+
+fn ty<'a>(schema: SchemaWithCache<'a>, ty: &'a schema::Type) -> ResolvedValue<'a> {
+    if let schema::Type::Named(name) = ty {
+        type_def(schema, name)
+    } else {
+        ResolvedValue::object(TypeResolver {
+            schema,
+            ty: Cow::Borrowed(ty),
+        })
+    }
+}
+
+fn deprecation_reason(opt_directive: Option<&Node<schema::Directive>>) -> ResolvedValue<'_> {
+    ResolvedValue::leaf(
+        opt_directive
+            .and_then(|directive| directive.argument_by_name("reason"))
+            .and_then(|arg| arg.as_str()),
+    )
+}
+
+impl_resolver! {
+    for IntrospectionRootResolver<'_>:
+
+    __typename = unreachable!();
+
+    fn __schema(&self_) {
+        Ok(ResolvedValue::object(self_.0))
+    }
+
+    fn __type(&self_, args) {
+        let name = args["name"].as_str().unwrap();
+        Ok(type_def(self_.0, name))
+    }
+}
+
+impl_resolver! {
+    for SchemaWithCache<'_>:
+
+    __typename = "__Schema";
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.schema_definition.description.as_deref()))
+    }
+
+    fn types(&self_) {
+        Ok(ResolvedValue::list(self_.types.iter().map(|(name, def)| {
+            ResolvedValue::object(TypeDefResolver { schema: *self_, name, def })
+        })))
+    }
+
+    fn directives(&self_) {
+        Ok(ResolvedValue::list(self_.directive_definitions.values().map(|def| {
+            ResolvedValue::object(DirectiveResolver { schema: *self_, def })
+        })))
+    }
+
+    fn queryType(&self_) {
+        Ok(type_def_opt(*self_, &self_.schema_definition.query))
+    }
+
+    fn mutationType(&self_) {
+        Ok(type_def_opt(*self_, &self_.schema_definition.mutation))
+    }
+
+    fn subscriptionType(&self_) {
+        Ok(type_def_opt(*self_, &self_.schema_definition.subscription))
+    }
+}
+
+impl_resolver! {
+    for TypeDefResolver<'_>:
+
+    __typename = "__Type";
+
+    fn kind(&self_) {
+        Ok(ResolvedValue::leaf(match self_.def {
+            schema::ExtendedType::Scalar(_) => "SCALAR",
+            schema::ExtendedType::Object(_) => "OBJECT",
+            schema::ExtendedType::Interface(_) => "INTERFACE",
+            schema::ExtendedType::Union(_) => "UNION",
+            schema::ExtendedType::Enum(_) => "ENUM",
+            schema::ExtendedType::InputObject(_) => "INPUT_OBJECT",
+        }))
+    }
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.name))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description().map(|desc| desc.as_str())))
+    }
+
+    fn fields(&self_, args) {
+        let fields = match self_.def {
+            schema::ExtendedType::Object(def) => &def.fields,
+            schema::ExtendedType::Interface(def) => &def.fields,
+            schema::ExtendedType::Scalar(_) |
+            schema::ExtendedType::Union(_) |
+            schema::ExtendedType::Enum(_) |
+            schema::ExtendedType::InputObject(_) => return Ok(ResolvedValue::null()),
+        };
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(fields
+            .values()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(FieldResolver { schema: self_.schema, def })
+            })
+        ))
+    }
+
+    fn interfaces(&self_) {
+        let implements_interfaces = match self_.def {
+            schema::ExtendedType::Object(def) => &def.implements_interfaces,
+            schema::ExtendedType::Interface(def) => &def.implements_interfaces,
+            schema::ExtendedType::Scalar(_) |
+            schema::ExtendedType::Union(_) |
+            schema::ExtendedType::Enum(_) |
+            schema::ExtendedType::InputObject(_) => return Ok(ResolvedValue::null()),
+        };
+        Ok(ResolvedValue::list(implements_interfaces.iter().filter_map(|name| {
+            self_.schema.types.get(&name.name).map(|def| {
+                ResolvedValue::object(TypeDefResolver { schema: self_.schema, name, def })
+            })
+        })))
+    }
+
+    fn possibleTypes(&self_) {
+        macro_rules! types {
+            ($names: expr) => {
+                Ok(ResolvedValue::list($names.filter_map(move |name| {
+                    self_.schema.types.get(name).map(move |def| {
+                        ResolvedValue::object(TypeDefResolver { schema: self_.schema, name, def })
+                    })
+                })))
+            }
+        }
+        match self_.def {
+            schema::ExtendedType::Interface(_) => types!(self_.schema.implementers_of(self_.name)),
+            schema::ExtendedType::Union(def) => types!(def.members.iter().map(|c| &c.name)),
+            schema::ExtendedType::Object(_) |
+            schema::ExtendedType::Scalar(_) |
+            schema::ExtendedType::Enum(_) |
+            schema::ExtendedType::InputObject(_) => Ok(ResolvedValue::null()),
+        }
+    }
+
+    fn enumValues(&self_, args) {
+        let schema::ExtendedType::Enum(def) = self_.def else {
+            return Ok(ResolvedValue::null());
+        };
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(def
+            .values
+            .values()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(EnumValueResolver { def })
+            })
+        ))
+    }
+
+    fn inputFields(&self_, args) {
+        let schema::ExtendedType::InputObject(def) = self_.def else {
+            return Ok(ResolvedValue::null());
+        };
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(def
+            .fields
+            .values()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(InputValueResolver { schema: self_.schema, def })
+            })
+        ))
+    }
+
+    fn ofType() {
+        Ok(ResolvedValue::null())
+    }
+
+    fn specifiedByURL(&self_) {
+        let schema::ExtendedType::Scalar(def) = self_.def else {
+            return Ok(ResolvedValue::null())
+        };
+        Ok(ResolvedValue::leaf(def
+            .directives.get("specifiedBy")
+            .and_then(|dir| dir.argument_by_name("url"))
+            .and_then(|arg| arg.as_str())
+        ))
+    }
+}
+
+// Only used for non-null and list types
+impl_resolver! {
+    for TypeResolver<'_>:
+
+    __typename = "__Type";
+
+    fn kind(&self_) {
+        Ok(ResolvedValue::leaf(match &*self_.ty {
+            schema::Type::Named(_) => unreachable!(),
+            schema::Type::List(_) => "LIST",
+            schema::Type::NonNullNamed(_) |
+            schema::Type::NonNullList(_) => "NON_NULL",
+        }))
+    }
+
+    fn ofType(&self_) {
+        Ok(match &*self_.ty {
+            schema::Type::Named(_) => unreachable!(),
+            schema::Type::List(inner) => ty(self_.schema, inner),
+            schema::Type::NonNullNamed(inner) => type_def(self_.schema, inner),
+            schema::Type::NonNullList(inner) => ResolvedValue::object(Self {
+                schema: self_.schema,
+                ty: Cow::Owned(schema::Type::List(inner.clone()))
+            }),
+        })
+    }
+
+    fn name() { Ok(ResolvedValue::null()) }
+    fn description() { Ok(ResolvedValue::null()) }
+    fn fields() { Ok(ResolvedValue::null()) }
+    fn interfaces() { Ok(ResolvedValue::null()) }
+    fn possibleTypes() { Ok(ResolvedValue::null()) }
+    fn enumValues() { Ok(ResolvedValue::null()) }
+    fn inputFields() { Ok(ResolvedValue::null()) }
+    fn specifiedBy() { Ok(ResolvedValue::null()) }
+}
+
+impl_resolver! {
+    for DirectiveResolver<'_>:
+
+    __typename = "__Directive";
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.name.as_str()))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description.as_deref()))
+    }
+
+    fn args(&self_, args) {
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(self_
+            .def
+            .arguments
+            .iter()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(InputValueResolver { schema: self_.schema, def })
+            })
+        ))
+    }
+
+    fn locations(&self_) {
+        Ok(ResolvedValue::list(self_.def.locations.iter().map(|loc| {
+            ResolvedValue::leaf(loc.name())
+        })))
+    }
+
+    fn isRepeatable(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.repeatable))
+    }
+}
+
+impl_resolver! {
+    for FieldResolver<'_>:
+
+    __typename = "__Field";
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.name.as_str()))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description.as_deref()))
+    }
+
+    fn args(&self_, args) {
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(self_
+            .def
+            .arguments
+            .iter()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(InputValueResolver { schema: self_.schema, def })
+            })
+        ))
+    }
+
+    fn type(&self_) {
+        Ok(ty(self_.schema, &self_.def.ty))
+    }
+
+    fn isDeprecated(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some()))
+    }
+
+    fn deprecationReason(&self_) {
+        Ok(deprecation_reason(self_.def.directives.get("deprecated")))
+    }
+}
+
+impl_resolver! {
+    for EnumValueResolver<'_>:
+
+    __typename = "__EnumValue";
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.value.as_str()))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description.as_deref()))
+    }
+
+    fn isDeprecated(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some()))
+    }
+
+    fn deprecationReason(&self_) {
+        Ok(deprecation_reason(self_.def.directives.get("deprecated")))
+    }
+}
+
+impl_resolver! {
+    for InputValueResolver<'_>:
+
+    __typename = "__InputValue";
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.name.as_str()))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description.as_deref()))
+    }
+
+    fn type(&self_) {
+        Ok(ty(self_.schema, &self_.def.ty))
+    }
+
+    fn defaultValue(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.default_value.as_ref().map(|val| val.to_string())))
+    }
+
+    fn isDeprecated(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some()))
+    }
+
+    fn deprecationReason(&self_) {
+        Ok(deprecation_reason(self_.def.directives.get("deprecated")))
+    }
+}
diff --git a/crates/apollo-compiler/src/execution/introspection_split.rs b/crates/apollo-compiler/src/execution/introspection_split.rs
new file mode 100644
index 0000000..f14afa3
--- /dev/null
+++ b/crates/apollo-compiler/src/execution/introspection_split.rs
@@ -0,0 +1,610 @@
+use crate::ast;
+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::GraphQLError;
+use crate::execution::Response;
+use crate::execution::SchemaIntrospectionQuery;
+use crate::node::NodeLocation;
+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 crate::SourceMap;
+use indexmap::map::Entry;
+use indexmap::IndexMap;
+use std::collections::HashSet;
+
+/// Result of [`split`][Self::split]ting [schema introspection] fields from an operation.
+///
+/// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection
+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 [`ExecutableDocument::get_operation`] to obtain the one operation.
+        filtered_document: Valid<ExecutableDocument>,
+    },
+}
+
+pub enum SchemaIntrospectionError {
+    SuspectedValidationBug(SuspectedValidationBug),
+    Unsupported {
+        message: String,
+        location: Option<NodeLocation>,
+    },
+}
+
+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::new();
+        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(introspection_document)))
+        } else {
+            let mut fragments_done = HashSet::new();
+            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(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::new();
+    let mut fragments_to_visit = HashSet::new();
+    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(),
+        anonymous_operation: None,
+        named_operations: IndexMap::new(),
+        fragments,
+    };
+    new_document.insert_operation(new_operation);
+    if cfg!(debug_assertions) {
+        new_document
+            .validate(schema)
+            .expect("filtering a valid document should result in a valid document")
+    } else {
+        Valid::assume_valid(new_document)
+    }
+}
+
+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::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 0829cda..ee1d0b6 100644
--- a/crates/apollo-compiler/src/execution/mod.rs
+++ b/crates/apollo-compiler/src/execution/mod.rs
@@ -4,23 +4,35 @@
 //! [execution]: https://spec.graphql.org/October2021/#sec-Execution
 //! [response]: https://spec.graphql.org/October2021/#sec-Response
 
-/// A source location (line and column numbers) for a [`GraphQLError`].
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
-pub struct GraphQLLocation {
-    /// The line number for this location, starting at 1 for the first line.
-    pub line: usize,
-    /// The column number for this location, starting at 1 and counting characters (Unicode Scalar
-    /// Values) like [`str::chars`].
-    pub column: usize,
-}
+#[macro_use]
+mod resolver;
+mod engine;
+mod input_coercion;
+mod introspection_execute;
+mod introspection_split;
+mod response;
+mod result_coercion;
 
-/// A serializable error, as found in a GraphQL response.
-#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
-pub struct GraphQLError {
-    /// The error message.
-    pub message: String,
+pub use self::input_coercion::coerce_variable_values;
+pub use self::input_coercion::InputCoercionError;
+pub use self::introspection_execute::SchemaIntrospectionQuery;
+pub use self::introspection_split::SchemaIntrospectionError;
+pub use self::introspection_split::SchemaIntrospectionSplit;
+pub use self::response::GraphQLError;
+pub use self::response::GraphQLLocation;
+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;
 
-    /// Locations relevant to the error, if any.
-    #[serde(default, skip_serializing_if = "Vec::is_empty")]
-    pub locations: Vec<GraphQLLocation>,
-}
+/// 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>;
diff --git a/crates/apollo-compiler/src/execution/resolver.rs b/crates/apollo-compiler/src/execution/resolver.rs
new file mode 100644
index 0000000..d86df2b
--- /dev/null
+++ b/crates/apollo-compiler/src/execution/resolver.rs
@@ -0,0 +1,171 @@
+use crate::execution::JsonMap;
+use serde_json_bytes::Value as JsonValue;
+
+/// A GraphQL object whose fields can be resolved during execution
+pub(crate) type ObjectValue<'a> = dyn Resolver + 'a;
+
+/// Abstraction for implementing field resolvers. Used through [`ObjectValue`].
+///
+/// Use the [`impl_resolver!`][crate::impl_resolver] macro to implement this trait
+/// with reduced boilerplate
+pub(crate) trait Resolver {
+    /// Returns the name of the concrete object type this resolver represents
+    ///
+    /// That name expected to be that of an object type defined in the schema.
+    /// This is called when the schema indicates an abstract (interface or union) type.
+    fn type_name(&self) -> &'static str;
+
+    /// Resolves a field of this object with the given arguments
+    ///
+    /// The resolved is expected to match the type of the corresponding field definition
+    /// in the schema.
+    fn resolve_field<'a>(
+        &'a self,
+        field_name: &'a str,
+        arguments: &'a JsonMap,
+    ) -> Result<ResolvedValue<'_>, ResolverError>;
+}
+
+pub(crate) struct ResolverError {
+    pub(crate) message: String,
+}
+
+/// Implements the [`Resolver`] trait with reduced boilerplate
+///
+/// Define:
+///
+/// * The implementing Rust type
+/// * The __typename string
+/// * One pseudo-method per field. Types are omitted in the signature for brevity.
+///   - Takes two optional arguments: `&self` (which must be spelled something else because macros)
+///     and `args: `[`&JsonMap`][crate::JsonMap] for the field arguments.
+///     Field arguments are coerced according to their definition in the schema.
+///   - Returns `Result<ResolvedValue, String>`, `Err` it turned into a field error
+macro_rules! impl_resolver {
+    (
+        for $ty: ty:
+        __typename = $type_name: expr;
+        $(
+            fn $field_name: ident(
+                $( &$self_: ident $(, $( $args: ident $(,)? )? )? )?
+            ) $block: block
+        )*
+
+    ) => {
+        impl $crate::execution::resolver::Resolver for $ty {
+            fn type_name(&self) -> &'static str {
+                $type_name
+            }
+
+            fn resolve_field<'a>(
+                &'a self,
+                field_name: &'a str,
+                arguments: &'a $crate::execution::JsonMap,
+            ) -> Result<
+                $crate::execution::resolver::ResolvedValue<'_>,
+                crate::execution::resolver::ResolverError
+            > {
+                let _allow_unused = arguments;
+                match field_name {
+                    $(
+                        stringify!($field_name) => {
+                            $(
+                                let $self_ = self;
+                                $($(
+                                    let $args = arguments;
+                                )?)?
+                            )?
+                            return $block
+                        },
+                    )*
+                    _ => Err(crate::execution::resolver::ResolverError {
+                        message: format!("unexpected field name: {field_name}")
+                    }),
+                }
+            }
+        }
+    };
+}
+
+/// The value of a resolved field
+pub(crate) enum ResolvedValue<'a> {
+    /// * JSON null represents GraphQL null
+    /// * A GraphQL enum value is represented as a JSON string
+    /// * GraphQL built-in scalars are coerced according to their respective *Result Coercion* spec
+    /// * For custom scalars, any JSON value is passed through as-is (including array or object)
+    Leaf(JsonValue),
+
+    /// Expected where the GraphQL type is an object, interface, or union type
+    Object(Box<ObjectValue<'a>>),
+
+    /// Expected for GraphQL list types
+    List(Box<dyn Iterator<Item = ResolvedValue<'a>> + 'a>),
+}
+
+impl<'a> ResolvedValue<'a> {
+    /// Construct a null leaf resolved value
+    pub(crate) fn null() -> Self {
+        Self::Leaf(JsonValue::Null)
+    }
+
+    /// Construct a leaf resolved value from something that is convertible to JSON
+    pub(crate) fn leaf(json: impl Into<JsonValue>) -> Self {
+        Self::Leaf(json.into())
+    }
+
+    /// Construct an object resolved value from the resolver for that object
+    pub(crate) fn object(resolver: impl Resolver + 'a) -> Self {
+        Self::Object(Box::new(resolver))
+    }
+
+    /// Construct an object resolved value or null, from an optional resolver
+    pub(crate) fn opt_object(opt_resolver: Option<impl Resolver + 'a>) -> Self {
+        match opt_resolver {
+            Some(resolver) => Self::Object(Box::new(resolver)),
+            None => Self::null(),
+        }
+    }
+
+    /// Construct a list resolved value from an iterator
+    pub(crate) fn list<I>(iter: I) -> Self
+    where
+        I: IntoIterator<Item = Self>,
+        I::IntoIter: 'a,
+    {
+        Self::List(Box::new(iter.into_iter()))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::execution::resolver::ResolvedValue;
+
+    struct QueryResolver {
+        world: String,
+    }
+
+    impl_resolver! {
+        for &'_ QueryResolver:
+
+        __typename = "Query";
+
+        fn null() {
+            Ok(ResolvedValue::null())
+        }
+
+        fn hello(&self_) {
+            Ok(ResolvedValue::list([
+                ResolvedValue::leaf(format!("Hello {}!", self_.world)),
+                ResolvedValue::leaf(format!("Hello {}!", self_.world)),
+            ]))
+        }
+
+        fn echo(&_self, args) {
+            Ok(ResolvedValue::leaf(args["value"].clone()))
+        }
+
+        fn myself_again(&self_) {
+            Ok(ResolvedValue::object(*self_))
+        }
+    }
+}
diff --git a/crates/apollo-compiler/src/execution/response.rs b/crates/apollo-compiler/src/execution/response.rs
new file mode 100644
index 0000000..87d86f6
--- /dev/null
+++ b/crates/apollo-compiler/src/execution/response.rs
@@ -0,0 +1,208 @@
+use crate::execution::engine::PropagateNull;
+use crate::execution::JsonMap;
+use crate::node::NodeLocation;
+use crate::SourceMap;
+use serde::Deserialize;
+use serde::Serialize;
+
+/// A [GraphQL response](https://spec.graphql.org/October2021/#sec-Response-Format)
+#[derive(Debug, Clone, 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, 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<GraphQLLocation>,
+
+    /// 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,
+}
+
+/// A source location (line and column numbers) for a [`GraphQLError`].
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct GraphQLLocation {
+    /// The line number for this location, starting at 1 for the first line.
+    pub line: usize,
+    /// The column number for this location, starting at 1 and counting characters (Unicode Scalar
+    /// Values) like [`str::chars`].
+    pub column: usize,
+}
+
+/// 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::ast::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<NodeLocation>,
+        sources: &SourceMap,
+    ) -> Self {
+        Self {
+            message: message.into(),
+            locations: GraphQLLocation::from_node(sources, location)
+                .into_iter()
+                .collect(),
+            path: Default::default(),
+            extensions: Default::default(),
+        }
+    }
+}
+
+impl GraphQLLocation {
+    /// Convert a `NodeLocation` to a line and column number
+    pub fn from_node(sources: &SourceMap, location: Option<NodeLocation>) -> Option<Self> {
+        let loc = location?;
+        let source = sources.get(&loc.file_id)?;
+        source
+            .get_line_column(loc.offset())
+            .map(|(line, column)| GraphQLLocation {
+                line: line + 1,
+                column: column + 1,
+            })
+    }
+}
+
+impl ResponseData {
+    /// For serde `skip_serializing_if`
+    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
new file mode 100644
index 0000000..3982b7e
--- /dev/null
+++ b/crates/apollo-compiler/src/execution/result_coercion.rs
@@ -0,0 +1,224 @@
+use crate::executable::Field;
+use crate::execution::engine::execute_selection_set;
+use crate::execution::engine::try_nullify;
+use crate::execution::engine::ExecutionMode;
+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::schema::ExtendedType;
+use crate::schema::Type;
+use crate::validation::SuspectedValidationBug;
+use crate::validation::Valid;
+use crate::ExecutableDocument;
+use crate::Schema;
+
+/// <https://spec.graphql.org/October2021/#CompleteValue()>
+///
+/// Returns `Err` for a field error being propagated upwards to find a nullable place
+#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
+pub(crate) fn complete_value<'a, 'b>(
+    schema: &'a Valid<Schema>,
+    document: &'a Valid<ExecutableDocument>,
+    variable_values: &'a Valid<JsonMap>,
+    errors: &'b mut Vec<GraphQLError>,
+    path: LinkedPath<'b>,
+    mode: ExecutionMode,
+    ty: &'a Type,
+    resolved: ResolvedValue<'a>,
+    fields: &'a [&'a Field],
+) -> Result<JsonValue, PropagateNull> {
+    let location = fields[0].name.location();
+    macro_rules! field_error {
+        ($($arg: tt)+) => {
+            {
+                errors.push(GraphQLError::field_error(
+                    format!($($arg)+),
+                    path,
+                    location,
+                    &document.sources
+                ));
+                return Err(PropagateNull);
+            }
+        };
+    }
+    if let ResolvedValue::Leaf(JsonValue::Null) = resolved {
+        if ty.is_non_null() {
+            field_error!("Non-null type {ty} resolved to null")
+        } else {
+            return Ok(JsonValue::Null);
+        }
+    }
+    if let ResolvedValue::List(iter) = resolved {
+        match ty {
+            Type::Named(_) | Type::NonNullNamed(_) => {
+                field_error!("Non-list type {ty} resolved to a list")
+            }
+            Type::List(inner_ty) | Type::NonNullList(inner_ty) => {
+                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),
+                        next: path,
+                    };
+                    let inner_result = complete_value(
+                        schema,
+                        document,
+                        variable_values,
+                        errors,
+                        Some(&inner_path),
+                        mode,
+                        inner_ty,
+                        inner_resolved,
+                        fields,
+                    );
+                    // On field error, try to nullify that item
+                    match try_nullify(inner_ty, inner_result) {
+                        Ok(inner_value) => completed_list.push(inner_value),
+                        // If the item is non-null, try to nullify the list
+                        Err(PropagateNull) => return try_nullify(ty, Err(PropagateNull)),
+                    }
+                }
+                return Ok(completed_list.into());
+            }
+        }
+    }
+    let ty_name = match ty {
+        Type::List(_) | Type::NonNullList(_) => {
+            field_error!("List type {ty} resolved to an object")
+        }
+        Type::Named(name) | Type::NonNullNamed(name) => name,
+    };
+    let Some(ty_def) = schema.types.get(ty_name) else {
+        errors.push(
+            SuspectedValidationBug {
+                message: format!("Undefined type {ty_name}"),
+                location,
+            }
+            .into_field_error(&document.sources, path),
+        );
+        return Err(PropagateNull);
+    };
+    if let ExtendedType::InputObject(_) = ty_def {
+        errors.push(
+            SuspectedValidationBug {
+                message: format!("Field with input object type {ty_name}"),
+                location,
+            }
+            .into_field_error(&document.sources, path),
+        );
+        return Err(PropagateNull);
+    }
+    let resolved_obj = match resolved {
+        ResolvedValue::List(_) => unreachable!(), // early return above
+        ResolvedValue::Leaf(json_value) => {
+            match ty_def {
+                ExtendedType::InputObject(_) => unreachable!(), // early return above
+                ExtendedType::Object(_) | ExtendedType::Interface(_) | ExtendedType::Union(_) => {
+                    field_error!(
+                        "Resolver returned a leaf value \
+                         but expected an object for type {ty_name}"
+                    )
+                }
+                ExtendedType::Enum(enum_def) => {
+                    // https://spec.graphql.org/October2021/#sec-Enums.Result-Coercion
+                    if !json_value
+                        .as_str()
+                        .is_some_and(|str| enum_def.values.contains_key(str))
+                    {
+                        field_error!("Resolver returned {json_value}, expected enum {ty_name}")
+                    }
+                }
+                ExtendedType::Scalar(_) => match ty_name.as_str() {
+                    "Int" => {
+                        // https://spec.graphql.org/October2021/#sec-Int.Result-Coercion
+                        // > GraphQL services may coerce non-integer internal values to integers
+                        // > when reasonable without losing information
+                        //
+                        // We choose not to, to keep with Rust’s strong typing
+                        if let Some(int) = json_value.as_i64() {
+                            if i32::try_from(int).is_err() {
+                                field_error!("Resolver returned {json_value} which overflows Int")
+                            }
+                        } else {
+                            field_error!("Resolver returned {json_value}, expected Int")
+                        }
+                    }
+                    "Float" => {
+                        // https://spec.graphql.org/October2021/#sec-Float.Result-Coercion
+                        if !json_value.is_f64() {
+                            field_error!("Resolver returned {json_value}, expected Float")
+                        }
+                    }
+                    "String" => {
+                        // https://spec.graphql.org/October2021/#sec-String.Result-Coercion
+                        if !json_value.is_string() {
+                            field_error!("Resolver returned {json_value}, expected String")
+                        }
+                    }
+                    "Boolean" => {
+                        // https://spec.graphql.org/October2021/#sec-Boolean.Result-Coercion
+                        if !json_value.is_boolean() {
+                            field_error!("Resolver returned {json_value}, expected Boolean")
+                        }
+                    }
+                    "ID" => {
+                        // https://spec.graphql.org/October2021/#sec-ID.Result-Coercion
+                        if !(json_value.is_string() || json_value.is_i64()) {
+                            field_error!("Resolver returned {json_value}, expected ID")
+                        }
+                    }
+                    _ => {
+                        // Custom scalar: accept any JSON value (including an array or object,
+                        // despite this being a "leaf" as far as GraphQL resolution is concerned)
+                    }
+                },
+            };
+            return Ok(json_value);
+        }
+        ResolvedValue::Object(resolved_obj) => resolved_obj,
+    };
+    let (object_type_name, object_type) = match ty_def {
+        ExtendedType::InputObject(_) => unreachable!(), // early return above
+        ExtendedType::Enum(_) | ExtendedType::Scalar(_) => {
+            field_error!(
+                "Resolver returned a an object of type {}, expected {ty_name}",
+                resolved_obj.type_name()
+            )
+        }
+        ExtendedType::Interface(_) | ExtendedType::Union(_) => {
+            let object_type_name = resolved_obj.type_name();
+            if let Some(def) = schema.get_object(object_type_name) {
+                (object_type_name, def)
+            } else {
+                field_error!(
+                    "Resolver returned an object of type {object_type_name} \
+                     not defined in the schema"
+                )
+            }
+        }
+        ExtendedType::Object(def) => {
+            debug_assert_eq!(ty_name, resolved_obj.type_name());
+            (ty_name.as_str(), def)
+        }
+    };
+    execute_selection_set(
+        schema,
+        document,
+        variable_values,
+        errors,
+        path,
+        mode,
+        object_type_name,
+        object_type,
+        &*resolved_obj,
+        fields
+            .iter()
+            .flat_map(|field| &field.selection_set.selections),
+    )
+    .map(JsonValue::Object)
+}
diff --git a/crates/apollo-compiler/src/introspection/resolver.rs b/crates/apollo-compiler/src/introspection/resolver.rs
new file mode 100644
index 0000000..f00aa20
--- /dev/null
+++ b/crates/apollo-compiler/src/introspection/resolver.rs
@@ -0,0 +1,454 @@
+use crate::execution::resolver::ResolvedValue;
+use crate::schema;
+use crate::schema::Name;
+use crate::Node;
+use crate::Schema;
+use std::borrow::Cow;
+use std::collections::HashMap;
+use std::collections::HashSet;
+use std::sync::OnceLock;
+
+#[derive(Clone, Copy)]
+pub(super) struct SchemaWithCache<'a> {
+    pub(super) schema: &'a Schema,
+    pub(super) implementers_map: &'a OnceLock<HashMap<Name, HashSet<Name>>>,
+}
+
+impl<'a> SchemaWithCache<'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()
+            .flatten()
+    }
+}
+
+impl<'a> std::ops::Deref for SchemaWithCache<'a> {
+    type Target = &'a Schema;
+
+    fn deref(&self) -> &Self::Target {
+        &self.schema
+    }
+}
+
+pub(super) struct IntrospectionRootResolver<'a>(pub(super) SchemaWithCache<'a>);
+
+struct TypeDefResolver<'a> {
+    schema: SchemaWithCache<'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>,
+    ty: Cow<'a, schema::Type>,
+}
+
+struct DirectiveResolver<'a> {
+    schema: SchemaWithCache<'a>,
+    def: &'a schema::DirectiveDefinition,
+}
+
+struct FieldResolver<'a> {
+    schema: SchemaWithCache<'a>,
+    def: &'a schema::FieldDefinition,
+}
+
+struct EnumValueResolver<'a> {
+    def: &'a schema::EnumValueDefinition,
+}
+
+struct InputValueResolver<'a> {
+    schema: SchemaWithCache<'a>,
+    def: &'a schema::InputValueDefinition,
+}
+
+fn type_def(schema: SchemaWithCache<'_>, name: impl AsRef<str>) -> ResolvedValue<'_> {
+    ResolvedValue::opt_object(
+        schema
+            .types
+            .get_key_value(name.as_ref())
+            .map(|(name, def)| TypeDefResolver { schema, name, def }),
+    )
+}
+
+fn type_def_opt<'a>(
+    schema: SchemaWithCache<'a>,
+    name: &Option<impl AsRef<str>>,
+) -> ResolvedValue<'a> {
+    if let Some(name) = name.as_ref() {
+        type_def(schema, name)
+    } else {
+        ResolvedValue::null()
+    }
+}
+
+fn ty<'a>(schema: SchemaWithCache<'a>, ty: &'a schema::Type) -> ResolvedValue<'a> {
+    if let schema::Type::Named(name) = ty {
+        type_def(schema, name)
+    } else {
+        ResolvedValue::object(TypeResolver {
+            schema,
+            ty: Cow::Borrowed(ty),
+        })
+    }
+}
+
+fn deprecation_reason(opt_directive: Option<&Node<schema::Directive>>) -> ResolvedValue<'_> {
+    ResolvedValue::leaf(
+        opt_directive
+            .and_then(|directive| directive.argument_by_name("reason"))
+            .and_then(|arg| arg.as_str()),
+    )
+}
+
+impl_resolver! {
+    for IntrospectionRootResolver<'_>:
+
+    __typename = unreachable!();
+
+    fn __schema(&self_) {
+        Ok(ResolvedValue::object(self_.0))
+    }
+
+    fn __type(&self_, args) {
+        let name = args["name"].as_str().unwrap();
+        Ok(type_def(self_.0, name))
+    }
+}
+
+impl_resolver! {
+    for SchemaWithCache<'_>:
+
+    __typename = "__Schema";
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.schema_definition.description.as_deref()))
+    }
+
+    fn types(&self_) {
+        Ok(ResolvedValue::list(self_.types.iter().map(|(name, def)| {
+            ResolvedValue::object(TypeDefResolver { schema: *self_, name, def })
+        })))
+    }
+
+    fn directives(&self_) {
+        Ok(ResolvedValue::list(self_.directive_definitions.values().map(|def| {
+            ResolvedValue::object(DirectiveResolver { schema: *self_, def })
+        })))
+    }
+
+    fn queryType(&self_) {
+        Ok(type_def_opt(*self_, &self_.schema_definition.query))
+    }
+
+    fn mutationType(&self_) {
+        Ok(type_def_opt(*self_, &self_.schema_definition.mutation))
+    }
+
+    fn subscriptionType(&self_) {
+        Ok(type_def_opt(*self_, &self_.schema_definition.subscription))
+    }
+}
+
+impl_resolver! {
+    for TypeDefResolver<'_>:
+
+    __typename = "__Type";
+
+    fn kind(&self_) {
+        Ok(ResolvedValue::leaf(match self_.def {
+            schema::ExtendedType::Scalar(_) => "SCALAR",
+            schema::ExtendedType::Object(_) => "OBJECT",
+            schema::ExtendedType::Interface(_) => "INTERFACE",
+            schema::ExtendedType::Union(_) => "UNION",
+            schema::ExtendedType::Enum(_) => "ENUM",
+            schema::ExtendedType::InputObject(_) => "INPUT_OBJECT",
+        }))
+    }
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.name))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description().map(|desc| desc.as_str())))
+    }
+
+    fn fields(&self_, args) {
+        let fields = match self_.def {
+            schema::ExtendedType::Object(def) => &def.fields,
+            schema::ExtendedType::Interface(def) => &def.fields,
+            schema::ExtendedType::Scalar(_) |
+            schema::ExtendedType::Union(_) |
+            schema::ExtendedType::Enum(_) |
+            schema::ExtendedType::InputObject(_) => return Ok(ResolvedValue::null()),
+        };
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(fields
+            .values()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(FieldResolver { schema: self_.schema, def })
+            })
+        ))
+    }
+
+    fn interfaces(&self_) {
+        let implements_interfaces = match self_.def {
+            schema::ExtendedType::Object(def) => &def.implements_interfaces,
+            schema::ExtendedType::Interface(def) => &def.implements_interfaces,
+            schema::ExtendedType::Scalar(_) |
+            schema::ExtendedType::Union(_) |
+            schema::ExtendedType::Enum(_) |
+            schema::ExtendedType::InputObject(_) => return Ok(ResolvedValue::null()),
+        };
+        Ok(ResolvedValue::list(implements_interfaces.iter().filter_map(|name| {
+            self_.schema.types.get(&name.name).map(|def| {
+                ResolvedValue::object(TypeDefResolver { schema: self_.schema, name, def })
+            })
+        })))
+    }
+
+    fn possibleTypes(&self_) {
+        macro_rules! types {
+            ($names: expr) => {
+                Ok(ResolvedValue::list($names.filter_map(move |name| {
+                    self_.schema.types.get(name).map(move |def| {
+                        ResolvedValue::object(TypeDefResolver { schema: self_.schema, name, def })
+                    })
+                })))
+            }
+        }
+        match self_.def {
+            schema::ExtendedType::Interface(_) => types!(self_.schema.implementers_of(self_.name)),
+            schema::ExtendedType::Union(def) => types!(def.members.iter().map(|c| &c.name)),
+            schema::ExtendedType::Object(_) |
+            schema::ExtendedType::Scalar(_) |
+            schema::ExtendedType::Enum(_) |
+            schema::ExtendedType::InputObject(_) => Ok(ResolvedValue::null()),
+        }
+    }
+
+    fn enumValues(&self_, args) {
+        let schema::ExtendedType::Enum(def) = self_.def else {
+            return Ok(ResolvedValue::null());
+        };
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(def
+            .values
+            .values()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(EnumValueResolver { def })
+            })
+        ))
+    }
+
+    fn inputFields(&self_, args) {
+        let schema::ExtendedType::InputObject(def) = self_.def else {
+            return Ok(ResolvedValue::null());
+        };
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(def
+            .fields
+            .values()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(InputValueResolver { schema: self_.schema, def })
+            })
+        ))
+    }
+
+    fn ofType() {
+        Ok(ResolvedValue::null())
+    }
+
+    fn specifiedByURL(&self_) {
+        let schema::ExtendedType::Scalar(def) = self_.def else {
+            return Ok(ResolvedValue::null())
+        };
+        Ok(ResolvedValue::leaf(def
+            .directives.get("specifiedBy")
+            .and_then(|dir| dir.argument_by_name("url"))
+            .and_then(|arg| arg.as_str())
+        ))
+    }
+}
+
+// Only used for non-null and list types
+impl_resolver! {
+    for TypeResolver<'_>:
+
+    __typename = "__Type";
+
+    fn kind(&self_) {
+        Ok(ResolvedValue::leaf(match &*self_.ty {
+            schema::Type::Named(_) => unreachable!(),
+            schema::Type::List(_) => "LIST",
+            schema::Type::NonNullNamed(_) |
+            schema::Type::NonNullList(_) => "NON_NULL",
+        }))
+    }
+
+    fn ofType(&self_) {
+        Ok(match &*self_.ty {
+            schema::Type::Named(_) => unreachable!(),
+            schema::Type::List(inner) => ty(self_.schema, inner),
+            schema::Type::NonNullNamed(inner) => type_def(self_.schema, inner),
+            schema::Type::NonNullList(inner) => ResolvedValue::object(Self {
+                schema: self_.schema,
+                ty: Cow::Owned(schema::Type::List(inner.clone()))
+            }),
+        })
+    }
+
+    fn name() { Ok(ResolvedValue::null()) }
+    fn description() { Ok(ResolvedValue::null()) }
+    fn fields() { Ok(ResolvedValue::null()) }
+    fn interfaces() { Ok(ResolvedValue::null()) }
+    fn possibleTypes() { Ok(ResolvedValue::null()) }
+    fn enumValues() { Ok(ResolvedValue::null()) }
+    fn inputFields() { Ok(ResolvedValue::null()) }
+    fn specifiedBy() { Ok(ResolvedValue::null()) }
+}
+
+impl_resolver! {
+    for DirectiveResolver<'_>:
+
+    __typename = "__Directive";
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.name.as_str()))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description.as_deref()))
+    }
+
+    fn args(&self_, args) {
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(self_
+            .def
+            .arguments
+            .iter()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(InputValueResolver { schema: self_.schema, def })
+            })
+        ))
+    }
+
+    fn locations(&self_) {
+        Ok(ResolvedValue::list(self_.def.locations.iter().map(|loc| {
+            ResolvedValue::leaf(loc.name())
+        })))
+    }
+
+    fn isRepeatable(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.repeatable))
+    }
+}
+
+impl_resolver! {
+    for FieldResolver<'_>:
+
+    __typename = "__Field";
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.name.as_str()))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description.as_deref()))
+    }
+
+    fn args(&self_, args) {
+        let include_deprecated = args["includeDeprecated"].as_bool().unwrap();
+        Ok(ResolvedValue::list(self_
+            .def
+            .arguments
+            .iter()
+            .filter(move |def| {
+                include_deprecated || def.directives.get("deprecated").is_none()
+            })
+            .map(|def| {
+                ResolvedValue::object(InputValueResolver { schema: self_.schema, def })
+            })
+        ))
+    }
+
+    fn type(&self_) {
+        Ok(ty(self_.schema, &self_.def.ty))
+    }
+
+    fn isDeprecated(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some()))
+    }
+
+    fn deprecationReason(&self_) {
+        Ok(deprecation_reason(self_.def.directives.get("deprecated")))
+    }
+}
+
+impl_resolver! {
+    for EnumValueResolver<'_>:
+
+    __typename = "__EnumValue";
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.value.as_str()))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description.as_deref()))
+    }
+
+    fn isDeprecated(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some()))
+    }
+
+    fn deprecationReason(&self_) {
+        Ok(deprecation_reason(self_.def.directives.get("deprecated")))
+    }
+}
+
+impl_resolver! {
+    for InputValueResolver<'_>:
+
+    __typename = "__InputValue";
+
+    fn name(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.name.as_str()))
+    }
+
+    fn description(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.description.as_deref()))
+    }
+
+    fn type(&self_) {
+        Ok(ty(self_.schema, &self_.def.ty))
+    }
+
+    fn defaultValue(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.default_value.as_ref().map(|val| val.to_string())))
+    }
+
+    fn isDeprecated(&self_) {
+        Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some()))
+    }
+
+    fn deprecationReason(&self_) {
+        Ok(deprecation_reason(self_.def.directives.get("deprecated")))
+    }
+}
diff --git a/crates/apollo-compiler/src/node.rs b/crates/apollo-compiler/src/node.rs
index e213436..b058579 100644
--- a/crates/apollo-compiler/src/node.rs
+++ b/crates/apollo-compiler/src/node.rs
@@ -1,6 +1,8 @@
+use crate::execution::GraphQLLocation;
 use crate::schema::Component;
 use crate::schema::ComponentOrigin;
 use crate::validation::FileId;
+use crate::SourceMap;
 use apollo_parser::SyntaxNode;
 use rowan::TextRange;
 use std::collections::hash_map::RandomState;
@@ -77,6 +79,11 @@ impl<T> Node<T> {
         self.location().map(|l| l.file_id()) == Some(FileId::BUILT_IN)
     }
 
+    /// If this node contains a location, convert it to line and column numbers
+    pub fn line_column(&self, sources: &SourceMap) -> Option<GraphQLLocation> {
+        GraphQLLocation::from_node(sources, self.location())
+    }
+
     /// Returns the given `node` at the same location as `self` (e.g. for a type conversion).
     pub fn same_location<U>(&self, node: U) -> Node<U> {
         Node::new_opt_location(node, self.0.location)
diff --git a/crates/apollo-compiler/src/node_str.rs b/crates/apollo-compiler/src/node_str.rs
index f2553b4..a8f8b82 100644
--- a/crates/apollo-compiler/src/node_str.rs
+++ b/crates/apollo-compiler/src/node_str.rs
@@ -1,6 +1,8 @@
 #![allow(unstable_name_collisions)] // for `sptr::Strict`
 
+use crate::execution::GraphQLLocation;
 use crate::validation::NodeLocation;
+use crate::SourceMap;
 use sptr::Strict;
 use std::marker::PhantomData;
 use std::mem::size_of;
@@ -155,6 +157,11 @@ impl NodeStr {
         self.with_heap(|maybe_heap| maybe_heap?.header.header)
     }
 
+    /// If this string contains a location, convert it to line and column numbers
+    pub fn line_column(&self, sources: &SourceMap) -> Option<GraphQLLocation> {
+        GraphQLLocation::from_node(sources, self.location())
+    }
+
     #[inline]
     pub fn as_str(&self) -> &str {
         self.with_heap(|maybe_heap| {
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index f4ebd5c..3045d6b 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -23,7 +23,7 @@ mod variable;
 
 use crate::ast::Name;
 use crate::executable::BuildError as ExecutableBuildError;
-use crate::execution::{GraphQLError, GraphQLLocation};
+use crate::execution::{GraphQLError, GraphQLLocation, Response};
 use crate::schema::BuildError as SchemaBuildError;
 use crate::Node;
 use crate::SourceFile;
@@ -47,6 +47,7 @@ pub use crate::node::NodeLocation;
 /// * [`Schema::validate`]
 /// * [`ExecutableDocument::parse_and_validate`]
 /// * [`ExecutableDocument::validate`]
+/// * [`coerce_variable_values`][crate::execution::coerce_variable_values]
 ///
 /// … or by explicitly skipping it with [`Valid::assume_valid`].
 ///
@@ -143,6 +144,40 @@ impl<T> fmt::Display for WithErrors<T> {
     }
 }
 
+/// Returned as an error for situtations that should not happen with a valid schema or document.
+///
+/// Since the relevant APIs take [`Valid<_>`][crate::validation::Valid] parameters,
+/// either apollo-compiler has a validation bug
+/// or [`assume_valid`][crate::validation::Valid::assume_valid] was used incorrectly.
+///
+/// Can be [converted][std::convert] to [`GraphQLError`],
+/// which populates [`extensions`][GraphQLError::extensions]
+/// with a `"APOLLO_SUSPECTED_VALIDATION_BUG": true` entry.
+#[derive(Debug, Clone)]
+pub struct SuspectedValidationBug {
+    pub message: String,
+    pub location: Option<NodeLocation>,
+}
+
+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 {
@@ -376,14 +411,7 @@ impl DiagnosticData {
 impl<'a> Diagnostic<'a> {
     /// Get the line and column number where this diagnostic was raised.
     pub fn get_line_column(&self) -> Option<GraphQLLocation> {
-        let loc = self.data.location?;
-        let source = self.sources.get(&loc.file_id)?;
-        source
-            .get_line_column(loc.offset())
-            .map(|(line, column)| GraphQLLocation {
-                line: line + 1,
-                column: column + 1,
-            })
+        GraphQLLocation::from_node(self.sources, self.data.location)
     }
 
     /// Get serde_json serialisable version of the current diagnostic.
@@ -393,6 +421,8 @@ impl<'a> Diagnostic<'a> {
         GraphQLError {
             message: self.message().to_string(),
             locations,
+            path: Default::default(),
+            extensions: Default::default(),
         }
     }
 
diff --git a/crates/apollo-compiler/src/validation/variable.rs b/crates/apollo-compiler/src/validation/variable.rs
index 8c5d4c9..defc5d3 100644
--- a/crates/apollo-compiler/src/validation/variable.rs
+++ b/crates/apollo-compiler/src/validation/variable.rs
@@ -107,12 +107,13 @@ pub(crate) fn validate_variable_definitions(
     diagnostics
 }
 
+type NamedFragments = HashMap<ast::Name, Node<ast::FragmentDefinition>>;
+
 fn walk_selections(
     document: &ast::Document,
     selections: &[ast::Selection],
-    mut f: impl FnMut(&ast::Selection),
+    mut f: impl FnMut(&NamedFragments, &ast::Selection),
 ) -> Result<(), RecursionLimitError> {
-    type NamedFragments = HashMap<ast::Name, Node<ast::FragmentDefinition>>;
     let named_fragments: NamedFragments = document
         .definitions
         .iter()
@@ -128,10 +129,10 @@ fn walk_selections(
         named_fragments: &'ast NamedFragments,
         selections: &'ast [ast::Selection],
         guard: &mut RecursionGuard<'guard>,
-        f: &mut dyn FnMut(&ast::Selection),
+        f: &mut dyn FnMut(&NamedFragments, &ast::Selection),
     ) -> Result<(), RecursionLimitError> {
         for selection in selections {
-            f(selection);
+            f(named_fragments, selection);
             match selection {
                 ast::Selection::Field(field) => {
                     walk_selections_inner(named_fragments, &field.selection_set, guard, f)?;
@@ -226,23 +227,25 @@ pub(crate) fn validate_unused_variables(
         })
         .collect();
     let mut used_vars = HashSet::<ast::Name>::new();
-    let walked =
-        walk_selections(
-            &db.ast(file_id),
-            &operation.selection_set,
-            |selection| match selection {
-                ast::Selection::Field(field) => {
-                    used_vars.extend(variables_in_directives(&field.directives));
-                    used_vars.extend(variables_in_arguments(&field.arguments));
-                }
-                ast::Selection::FragmentSpread(fragment) => {
-                    used_vars.extend(variables_in_directives(&fragment.directives));
-                }
-                ast::Selection::InlineFragment(fragment) => {
-                    used_vars.extend(variables_in_directives(&fragment.directives));
+    let walked = walk_selections(
+        &db.ast(file_id),
+        &operation.selection_set,
+        |named_fragments, selection| match selection {
+            ast::Selection::Field(field) => {
+                used_vars.extend(variables_in_directives(&field.directives));
+                used_vars.extend(variables_in_arguments(&field.arguments));
+            }
+            ast::Selection::FragmentSpread(fragment) => {
+                if let Some(fragment_def) = named_fragments.get(&fragment.fragment_name) {
+                    used_vars.extend(variables_in_directives(&fragment_def.directives));
                 }
-            },
-        );
+                used_vars.extend(variables_in_directives(&fragment.directives));
+            }
+            ast::Selection::InlineFragment(fragment) => {
+                used_vars.extend(variables_in_directives(&fragment.directives));
+            }
+        },
+    );
     if walked.is_err() {
         diagnostics.push(ApolloDiagnostic::new(
             db,


Test patch:
diff --git a/crates/apollo-compiler/test_data/introspection/introspect_full_schema.graphql b/crates/apollo-compiler/test_data/introspection/introspect_full_schema.graphql
new file mode 100644
index 0000000..a0a3100
--- /dev/null
+++ b/crates/apollo-compiler/test_data/introspection/introspect_full_schema.graphql
@@ -0,0 +1,98 @@
+query IntrospectionQuery {
+  __schema {
+    queryType {
+      name
+    }
+    mutationType {
+      name
+    }
+    subscriptionType {
+      name
+    }
+    types {
+      ...FullType
+    }
+    directives {
+      name
+      description
+      locations
+      args(includeDeprecated: true) {
+        ...InputValue
+      }
+    }
+  }
+}
+fragment FullType on __Type {
+  kind
+  name
+  description
+  fields(includeDeprecated: true) {
+    name
+    description
+    args(includeDeprecated: true) {
+      ...InputValue
+    }
+    type {
+      ...TypeRef
+    }
+    isDeprecated
+    deprecationReason
+  }
+  inputFields(includeDeprecated: true) {
+    ...InputValue
+  }
+  interfaces {
+    ...TypeRef
+  }
+  enumValues(includeDeprecated: true) {
+    name
+    description
+    isDeprecated
+    deprecationReason
+  }
+  possibleTypes {
+    ...TypeRef
+  }
+}
+fragment InputValue on __InputValue {
+  name
+  description
+  type {
+    ...TypeRef
+  }
+  defaultValue
+  isDeprecated
+  deprecationReason
+}
+fragment TypeRef on __Type {
+  kind
+  name
+  ofType {
+    kind
+    name
+    ofType {
+      kind
+      name
+      ofType {
+        kind
+        name
+        ofType {
+          kind
+          name
+          ofType {
+            kind
+            name
+            ofType {
+              kind
+              name
+              ofType {
+                kind
+                name
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/crates/apollo-compiler/test_data/introspection/response_full.json b/crates/apollo-compiler/test_data/introspection/response_full.json
new file mode 100644
index 0000000..c18017a
--- /dev/null
+++ b/crates/apollo-compiler/test_data/introspection/response_full.json
@@ -0,0 +1,1179 @@
+{
+  "data": {
+    "__schema": {
+      "queryType": {
+        "name": "Query"
+      },
+      "mutationType": null,
+      "subscriptionType": null,
+      "types": [
+        {
+          "kind": "OBJECT",
+          "name": "__Schema",
+          "description": null,
+          "fields": [
+            {
+              "name": "description",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "types",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "LIST",
+                  "name": null,
+                  "ofType": {
+                    "kind": "NON_NULL",
+                    "name": null,
+                    "ofType": {
+                      "kind": "OBJECT",
+                      "name": "__Type",
+                      "ofType": null
+                    }
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "queryType",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "OBJECT",
+                  "name": "__Type",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "mutationType",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "OBJECT",
+                "name": "__Type",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "subscriptionType",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "OBJECT",
+                "name": "__Type",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "directives",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "LIST",
+                  "name": null,
+                  "ofType": {
+                    "kind": "NON_NULL",
+                    "name": null,
+                    "ofType": {
+                      "kind": "OBJECT",
+                      "name": "__Directive",
+                      "ofType": null
+                    }
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "inputFields": null,
+          "interfaces": [],
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "OBJECT",
+          "name": "__Type",
+          "description": null,
+          "fields": [
+            {
+              "name": "kind",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "ENUM",
+                  "name": "__TypeKind",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "name",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "description",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "fields",
+              "description": null,
+              "args": [
+                {
+                  "name": "includeDeprecated",
+                  "description": null,
+                  "type": {
+                    "kind": "SCALAR",
+                    "name": "Boolean",
+                    "ofType": null
+                  },
+                  "defaultValue": "false",
+                  "isDeprecated": false,
+                  "deprecationReason": null
+                }
+              ],
+              "type": {
+                "kind": "LIST",
+                "name": null,
+                "ofType": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "OBJECT",
+                    "name": "__Field",
+                    "ofType": null
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "interfaces",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "LIST",
+                "name": null,
+                "ofType": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "OBJECT",
+                    "name": "__Type",
+                    "ofType": null
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "possibleTypes",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "LIST",
+                "name": null,
+                "ofType": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "OBJECT",
+                    "name": "__Type",
+                    "ofType": null
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "enumValues",
+              "description": null,
+              "args": [
+                {
+                  "name": "includeDeprecated",
+                  "description": null,
+                  "type": {
+                    "kind": "SCALAR",
+                    "name": "Boolean",
+                    "ofType": null
+                  },
+                  "defaultValue": "false",
+                  "isDeprecated": false,
+                  "deprecationReason": null
+                }
+              ],
+              "type": {
+                "kind": "LIST",
+                "name": null,
+                "ofType": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "OBJECT",
+                    "name": "__EnumValue",
+                    "ofType": null
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "inputFields",
+              "description": null,
+              "args": [
+                {
+                  "name": "includeDeprecated",
+                  "description": null,
+                  "type": {
+                    "kind": "SCALAR",
+                    "name": "Boolean",
+                    "ofType": null
+                  },
+                  "defaultValue": "false",
+                  "isDeprecated": false,
+                  "deprecationReason": null
+                }
+              ],
+              "type": {
+                "kind": "LIST",
+                "name": null,
+                "ofType": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "OBJECT",
+                    "name": "__InputValue",
+                    "ofType": null
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "ofType",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "OBJECT",
+                "name": "__Type",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "specifiedByURL",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "inputFields": null,
+          "interfaces": [],
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "ENUM",
+          "name": "__TypeKind",
+          "description": null,
+          "fields": null,
+          "inputFields": null,
+          "interfaces": null,
+          "enumValues": [
+            {
+              "name": "SCALAR",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "OBJECT",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "INTERFACE",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "UNION",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "ENUM",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "INPUT_OBJECT",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "LIST",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "NON_NULL",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "possibleTypes": null
+        },
+        {
+          "kind": "OBJECT",
+          "name": "__Field",
+          "description": null,
+          "fields": [
+            {
+              "name": "name",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "String",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "description",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "args",
+              "description": null,
+              "args": [
+                {
+                  "name": "includeDeprecated",
+                  "description": null,
+                  "type": {
+                    "kind": "SCALAR",
+                    "name": "Boolean",
+                    "ofType": null
+                  },
+                  "defaultValue": "false",
+                  "isDeprecated": false,
+                  "deprecationReason": null
+                }
+              ],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "LIST",
+                  "name": null,
+                  "ofType": {
+                    "kind": "NON_NULL",
+                    "name": null,
+                    "ofType": {
+                      "kind": "OBJECT",
+                      "name": "__InputValue",
+                      "ofType": null
+                    }
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "type",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "OBJECT",
+                  "name": "__Type",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "isDeprecated",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "Boolean",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "deprecationReason",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "inputFields": null,
+          "interfaces": [],
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "OBJECT",
+          "name": "__InputValue",
+          "description": null,
+          "fields": [
+            {
+              "name": "name",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "String",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "description",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "type",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "OBJECT",
+                  "name": "__Type",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "defaultValue",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "isDeprecated",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "Boolean",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "deprecationReason",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "inputFields": null,
+          "interfaces": [],
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "OBJECT",
+          "name": "__EnumValue",
+          "description": null,
+          "fields": [
+            {
+              "name": "name",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "String",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "description",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "isDeprecated",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "Boolean",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "deprecationReason",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "inputFields": null,
+          "interfaces": [],
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "OBJECT",
+          "name": "__Directive",
+          "description": null,
+          "fields": [
+            {
+              "name": "name",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "String",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "description",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "locations",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "LIST",
+                  "name": null,
+                  "ofType": {
+                    "kind": "NON_NULL",
+                    "name": null,
+                    "ofType": {
+                      "kind": "ENUM",
+                      "name": "__DirectiveLocation",
+                      "ofType": null
+                    }
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "args",
+              "description": null,
+              "args": [
+                {
+                  "name": "includeDeprecated",
+                  "description": null,
+                  "type": {
+                    "kind": "SCALAR",
+                    "name": "Boolean",
+                    "ofType": null
+                  },
+                  "defaultValue": "false",
+                  "isDeprecated": false,
+                  "deprecationReason": null
+                }
+              ],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "LIST",
+                  "name": null,
+                  "ofType": {
+                    "kind": "NON_NULL",
+                    "name": null,
+                    "ofType": {
+                      "kind": "OBJECT",
+                      "name": "__InputValue",
+                      "ofType": null
+                    }
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "isRepeatable",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "Boolean",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "inputFields": null,
+          "interfaces": [],
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "ENUM",
+          "name": "__DirectiveLocation",
+          "description": null,
+          "fields": null,
+          "inputFields": null,
+          "interfaces": null,
+          "enumValues": [
+            {
+              "name": "QUERY",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "MUTATION",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "SUBSCRIPTION",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "FIELD",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "FRAGMENT_DEFINITION",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "FRAGMENT_SPREAD",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "INLINE_FRAGMENT",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "VARIABLE_DEFINITION",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "SCHEMA",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "SCALAR",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "OBJECT",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "FIELD_DEFINITION",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "ARGUMENT_DEFINITION",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "INTERFACE",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "UNION",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "ENUM",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "ENUM_VALUE",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "INPUT_OBJECT",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "INPUT_FIELD_DEFINITION",
+              "description": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "possibleTypes": null
+        },
+        {
+          "kind": "SCALAR",
+          "name": "Int",
+          "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int\ncan represent values between -(2^31) and 2^31 - 1.",
+          "fields": null,
+          "inputFields": null,
+          "interfaces": null,
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "SCALAR",
+          "name": "Float",
+          "description": "The `Float` scalar type represents signed double-precision fractional values as\nspecified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).",
+          "fields": null,
+          "inputFields": null,
+          "interfaces": null,
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "SCALAR",
+          "name": "String",
+          "description": "The `String` scalar type represents textual data, represented as UTF-8 character\nsequences. The String type is most often used by GraphQL to represent free-form\nhuman-readable text.",
+          "fields": null,
+          "inputFields": null,
+          "interfaces": null,
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "SCALAR",
+          "name": "Boolean",
+          "description": "The `Boolean` scalar type represents `true` or `false`.",
+          "fields": null,
+          "inputFields": null,
+          "interfaces": null,
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "SCALAR",
+          "name": "ID",
+          "description": "The `ID` scalar type represents a unique identifier, often used to refetch an\nobject or as key for a cache. The ID type appears in a JSON response as a\nString; however, it is not intended to be human-readable. When expected as an\ninput type, any string (such as `\\\"4\\\"`) or integer (such as `4`) input value\nwill be accepted as an ID.",
+          "fields": null,
+          "inputFields": null,
+          "interfaces": null,
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "OBJECT",
+          "name": "Query",
+          "description": null,
+          "fields": [
+            {
+              "name": "id",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "ID",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "int",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "Int",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": true,
+              "deprecationReason": "…"
+            },
+            {
+              "name": "url",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "Url",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "inputFields": null,
+          "interfaces": [
+            {
+              "kind": "INTERFACE",
+              "name": "I",
+              "ofType": null
+            }
+          ],
+          "enumValues": null,
+          "possibleTypes": null
+        },
+        {
+          "kind": "INTERFACE",
+          "name": "I",
+          "description": null,
+          "fields": [
+            {
+              "name": "id",
+              "description": null,
+              "args": [],
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "ID",
+                  "ofType": null
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ],
+          "inputFields": null,
+          "interfaces": [],
+          "enumValues": null,
+          "possibleTypes": [
+            {
+              "kind": "OBJECT",
+              "name": "Query",
+              "ofType": null
+            }
+          ]
+        },
+        {
+          "kind": "SCALAR",
+          "name": "Url",
+          "description": null,
+          "fields": null,
+          "inputFields": null,
+          "interfaces": null,
+          "enumValues": null,
+          "possibleTypes": null
+        }
+      ],
+      "directives": [
+        {
+          "name": "skip",
+          "description": "Directs the executor to skip this field or fragment when the `if` argument is true.",
+          "locations": [
+            "FIELD",
+            "FRAGMENT_SPREAD",
+            "INLINE_FRAGMENT"
+          ],
+          "args": [
+            {
+              "name": "if",
+              "description": "Skipped when true.",
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "Boolean",
+                  "ofType": null
+                }
+              },
+              "defaultValue": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ]
+        },
+        {
+          "name": "include",
+          "description": "Directs the executor to include this field or fragment only when the `if` argument is true.",
+          "locations": [
+            "FIELD",
+            "FRAGMENT_SPREAD",
+            "INLINE_FRAGMENT"
+          ],
+          "args": [
+            {
+              "name": "if",
+              "description": "Included when true.",
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "Boolean",
+                  "ofType": null
+                }
+              },
+              "defaultValue": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ]
+        },
+        {
+          "name": "deprecated",
+          "description": "Marks an element of a GraphQL schema as no longer supported.",
+          "locations": [
+            "FIELD_DEFINITION",
+            "ARGUMENT_DEFINITION",
+            "INPUT_FIELD_DEFINITION",
+            "ENUM_VALUE"
+          ],
+          "args": [
+            {
+              "name": "reason",
+              "description": "Explains why this element was deprecated, usually also including a\nsuggestion for how to access supported similar data. Formatted using\nthe Markdown syntax, as specified by\n[CommonMark](https://commonmark.org/).",
+              "type": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              },
+              "defaultValue": "\"No longer supported\"",
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ]
+        },
+        {
+          "name": "specifiedBy",
+          "description": "Exposes a URL that specifies the behaviour of this scalar.",
+          "locations": [
+            "SCALAR"
+          ],
+          "args": [
+            {
+              "name": "url",
+              "description": "The URL that specifies the behaviour of this scalar.",
+              "type": {
+                "kind": "NON_NULL",
+                "name": null,
+                "ofType": {
+                  "kind": "SCALAR",
+                  "name": "String",
+                  "ofType": null
+                }
+              },
+              "defaultValue": null,
+              "isDeprecated": false,
+              "deprecationReason": null
+            }
+          ]
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/crates/apollo-compiler/tests/introspection.rs b/crates/apollo-compiler/tests/introspection.rs
new file mode 100644
index 0000000..18a2623
--- /dev/null
+++ b/crates/apollo-compiler/tests/introspection.rs
@@ -0,0 +1,136 @@
+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::ExecutableDocument;
+use apollo_compiler::Schema;
+use expect_test::expect;
+use expect_test::expect_file;
+
+#[test]
+fn test() {
+    let schema = r#"
+        type Query implements I {
+            id: ID!
+            int: Int! @deprecated(reason: "…")
+            url: Url
+        }
+
+        interface I {
+            id: ID!
+        }
+
+        scalar Url @specifiedBy(url: "https://url.spec.whatwg.org/")
+    "#;
+    let schema = Schema::parse_and_validate(schema, "schema.graphql").unwrap();
+
+    let introspect = |query, variables: JsonMap| {
+        let document =
+            ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
+        let operation = document.get_operation(None).unwrap();
+        let variables = coerce_variable_values(&schema, operation, &variables).unwrap();
+        let response = SchemaIntrospectionQuery::split_and_execute(
+            &schema,
+            &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(),
+            },
+        );
+        serde_json::to_string_pretty(&response).unwrap()
+    };
+
+    let query = r#"
+        query WithVarible($verbose: Boolean!) {
+            I: __type(name: "I") {
+                possibleTypes {
+                    name
+                    fields @skip(if: $verbose) {
+                        name
+                    }
+                    verboseFields: fields(includeDeprecated: true) @include(if: $verbose) {
+                        name
+                        deprecationReason
+                    }
+                }
+            }
+            Url: __type(name: "Url") @include(if: $verbose) {
+                specifiedByURL
+            }
+        }
+    "#;
+    let expected = expect!([r#"
+        {
+          "data": {
+            "I": {
+              "possibleTypes": [
+                {
+                  "name": "Query",
+                  "fields": [
+                    {
+                      "name": "id"
+                    },
+                    {
+                      "name": "url"
+                    }
+                  ]
+                }
+              ]
+            }
+          }
+        }"#]);
+    let variables = [("verbose".into(), false.into())].into_iter().collect();
+    let response = introspect(query, variables);
+    expected.assert_eq(&response);
+
+    let variables = [("verbose".into(), true.into())].into_iter().collect();
+    let response = introspect(query, variables);
+    let expected = expect!([r#"
+        {
+          "data": {
+            "I": {
+              "possibleTypes": [
+                {
+                  "name": "Query",
+                  "verboseFields": [
+                    {
+                      "name": "id",
+                      "deprecationReason": null
+                    },
+                    {
+                      "name": "int",
+                      "deprecationReason": "…"
+                    },
+                    {
+                      "name": "url",
+                      "deprecationReason": null
+                    }
+                  ]
+                }
+              ]
+            },
+            "Url": {
+              "specifiedByURL": "https://url.spec.whatwg.org/"
+            }
+          }
+        }"#]);
+    expected.assert_eq(&response);
+
+    let response = introspect(
+        include_str!("../test_data/introspection/introspect_full_schema.graphql"),
+        Default::default(),
+    );
+    expect_file!("../test_data/introspection/response_full.json").assert_eq(&response);
+}
diff --git a/crates/apollo-compiler/tests/introspection_split.rs b/crates/apollo-compiler/tests/introspection_split.rs
new file mode 100644
index 0000000..c5f058d
--- /dev/null
+++ b/crates/apollo-compiler/tests/introspection_split.rs
@@ -0,0 +1,382 @@
+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: [
+                GraphQLLocation {
+                    line: 2,
+                    column: 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: [
+                GraphQLLocation {
+                    line: 3,
+                    column: 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: [
+                GraphQLLocation {
+                    line: 2,
+                    column: 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: [
+                GraphQLLocation {
+                    line: 2,
+                    column: 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.get_operation(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 030fff9..ea2f9e3 100644
--- a/crates/apollo-compiler/tests/main.rs
+++ b/crates/apollo-compiler/tests/main.rs
@@ -2,6 +2,8 @@ mod executable;
 mod extensions;
 mod field_set;
 mod field_type;
+mod introspection;
+mod introspection_split;
 mod merge_schemas;
 /// Formerly in src/lib.rs
 mod misc;