Skip to content

Commit 5e62d06

Browse files
authored
compiler: Refactor execution code to prepare for giving it a public API (#979)
1 parent 5d98260 commit 5e62d06

File tree

6 files changed

+724
-705
lines changed

6 files changed

+724
-705
lines changed

crates/apollo-compiler/src/execution/engine.rs

Lines changed: 109 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
use crate::ast::Value;
2+
use crate::collections::HashMap;
23
use crate::collections::HashSet;
34
use crate::collections::IndexMap;
45
use crate::executable::Field;
56
use crate::executable::Selection;
67
use crate::execution::input_coercion::coerce_argument_values;
78
use crate::execution::resolver::ObjectValue;
8-
use crate::execution::resolver::ResolverError;
9+
use crate::execution::resolver::ResolveError;
10+
use crate::execution::resolver::ResolvedValue;
911
use crate::execution::result_coercion::complete_value;
12+
use crate::introspection::resolvers::MaybeLazy;
13+
use crate::introspection::resolvers::SchemaWithImplementersMap;
1014
use crate::parser::SourceMap;
1115
use crate::parser::SourceSpan;
1216
use crate::response::GraphQLError;
@@ -15,6 +19,7 @@ use crate::response::JsonValue;
1519
use crate::response::ResponseDataPathSegment;
1620
use crate::schema::ExtendedType;
1721
use crate::schema::FieldDefinition;
22+
use crate::schema::Implementers;
1823
use crate::schema::ObjectType;
1924
use crate::schema::Type;
2025
use crate::validation::SuspectedValidationBug;
@@ -26,7 +31,7 @@ use crate::Schema;
2631
/// <https://spec.graphql.org/October2021/#sec-Normal-and-Serial-Execution>
2732
#[derive(Debug, Copy, Clone)]
2833
pub(crate) enum ExecutionMode {
29-
/// Allowed to resolve fields in any order, including in parellel
34+
/// Allowed to resolve fields in any order, including in parallel
3035
Normal,
3136
/// Top-level fields of a mutation operation must be executed in order
3237
#[allow(unused)]
@@ -46,134 +51,118 @@ pub(crate) struct LinkedPathElement<'a> {
4651
pub(crate) next: LinkedPath<'a>,
4752
}
4853

54+
pub(crate) struct ExecutionContext<'a> {
55+
pub(crate) schema: &'a Valid<Schema>,
56+
pub(crate) document: &'a Valid<ExecutableDocument>,
57+
pub(crate) variable_values: &'a Valid<JsonMap>,
58+
pub(crate) errors: &'a mut Vec<GraphQLError>,
59+
pub(crate) implementers_map: MaybeLazy<'a, HashMap<Name, Implementers>>,
60+
}
61+
4962
/// <https://spec.graphql.org/October2021/#ExecuteSelectionSet()>
50-
#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
63+
///
64+
/// `object_value: None` is a special case for top-level of `introspection::partial_execute`
5165
pub(crate) fn execute_selection_set<'a>(
52-
schema: &Valid<Schema>,
53-
document: &'a Valid<ExecutableDocument>,
54-
variable_values: &Valid<JsonMap>,
55-
errors: &mut Vec<GraphQLError>,
66+
ctx: &mut ExecutionContext<'a>,
5667
path: LinkedPath<'_>,
5768
mode: ExecutionMode,
5869
object_type: &ObjectType,
59-
object_value: &ObjectValue<'_>,
70+
object_value: Option<&dyn ObjectValue>,
6071
selections: impl IntoIterator<Item = &'a Selection>,
6172
) -> Result<JsonMap, PropagateNull> {
6273
let mut grouped_field_set = IndexMap::default();
6374
collect_fields(
64-
schema,
65-
document,
66-
variable_values,
75+
ctx,
6776
object_type,
68-
object_value,
6977
selections,
7078
&mut HashSet::default(),
7179
&mut grouped_field_set,
7280
);
7381

7482
match mode {
75-
ExecutionMode::Normal => {}
76-
ExecutionMode::Sequential => {
77-
// If we want parallelism, use `futures::future::join_all` (async)
83+
ExecutionMode::Normal => {
84+
// If we want parallelism, use `StreamExt::buffer_unordered` (async)
7885
// or Rayon’s `par_iter` (sync) here.
7986
}
87+
ExecutionMode::Sequential => {}
8088
}
8189

8290
let mut response_map = JsonMap::with_capacity(grouped_field_set.len());
8391
for (&response_key, fields) in &grouped_field_set {
8492
// Indexing should not panic: `collect_fields` only creates a `Vec` to push to it
8593
let field_name = &fields[0].name;
86-
let Ok(field_def) = schema.type_field(&object_type.name, field_name) else {
94+
let Ok(field_def) = ctx.schema.type_field(&object_type.name, field_name) else {
8795
// TODO: Return a `validation_bug`` field error here?
8896
// The spec specifically has a “If fieldType is defined” condition,
8997
// but it being undefined would make the request invalid, right?
9098
continue;
9199
};
92-
let value = if field_name == "__typename" {
93-
JsonValue::from(object_type.name.as_str())
94-
} else {
95-
let field_path = LinkedPathElement {
96-
element: ResponseDataPathSegment::Field(response_key.clone()),
97-
next: path,
98-
};
99-
execute_field(
100-
schema,
101-
document,
102-
variable_values,
103-
errors,
104-
Some(&field_path),
105-
mode,
106-
object_value,
107-
field_def,
108-
fields,
109-
)?
100+
let field_path = LinkedPathElement {
101+
element: ResponseDataPathSegment::Field(response_key.clone()),
102+
next: path,
110103
};
111-
response_map.insert(response_key.as_str(), value);
104+
if let Some(value) = execute_field(
105+
ctx,
106+
Some(&field_path),
107+
mode,
108+
object_type,
109+
object_value,
110+
field_def,
111+
fields,
112+
)? {
113+
response_map.insert(response_key.as_str(), value);
114+
}
112115
}
113116
Ok(response_map)
114117
}
115118

116119
/// <https://spec.graphql.org/October2021/#CollectFields()>
117-
#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
118120
fn collect_fields<'a>(
119-
schema: &Schema,
120-
document: &'a ExecutableDocument,
121-
variable_values: &Valid<JsonMap>,
121+
ctx: &mut ExecutionContext<'a>,
122122
object_type: &ObjectType,
123-
object_value: &ObjectValue<'_>,
124123
selections: impl IntoIterator<Item = &'a Selection>,
125124
visited_fragments: &mut HashSet<&'a Name>,
126125
grouped_fields: &mut IndexMap<&'a Name, Vec<&'a Field>>,
127126
) {
128127
for selection in selections {
129-
if eval_if_arg(selection, "skip", variable_values).unwrap_or(false)
130-
|| !eval_if_arg(selection, "include", variable_values).unwrap_or(true)
128+
if eval_if_arg(selection, "skip", ctx.variable_values).unwrap_or(false)
129+
|| !eval_if_arg(selection, "include", ctx.variable_values).unwrap_or(true)
131130
{
132131
continue;
133132
}
134133
match selection {
135-
Selection::Field(field) => {
136-
if !object_value.skip_field(&field.name) {
137-
grouped_fields
138-
.entry(field.response_key())
139-
.or_default()
140-
.push(field.as_ref())
141-
}
142-
}
134+
Selection::Field(field) => grouped_fields
135+
.entry(field.response_key())
136+
.or_default()
137+
.push(field.as_ref()),
143138
Selection::FragmentSpread(spread) => {
144139
let new = visited_fragments.insert(&spread.fragment_name);
145140
if !new {
146141
continue;
147142
}
148-
let Some(fragment) = document.fragments.get(&spread.fragment_name) else {
143+
let Some(fragment) = ctx.document.fragments.get(&spread.fragment_name) else {
149144
continue;
150145
};
151-
if !does_fragment_type_apply(schema, object_type, fragment.type_condition()) {
146+
if !does_fragment_type_apply(ctx.schema, object_type, fragment.type_condition()) {
152147
continue;
153148
}
154149
collect_fields(
155-
schema,
156-
document,
157-
variable_values,
150+
ctx,
158151
object_type,
159-
object_value,
160152
&fragment.selection_set.selections,
161153
visited_fragments,
162154
grouped_fields,
163155
)
164156
}
165157
Selection::InlineFragment(inline) => {
166158
if let Some(condition) = &inline.type_condition {
167-
if !does_fragment_type_apply(schema, object_type, condition) {
159+
if !does_fragment_type_apply(ctx.schema, object_type, condition) {
168160
continue;
169161
}
170162
}
171163
collect_fields(
172-
schema,
173-
document,
174-
variable_values,
164+
ctx,
175165
object_type,
176-
object_value,
177166
&inline.selection_set.selections,
178167
visited_fragments,
179168
grouped_fields,
@@ -218,55 +207,79 @@ fn eval_if_arg(
218207
}
219208

220209
/// <https://spec.graphql.org/October2021/#ExecuteField()>
221-
#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal
222-
fn execute_field(
223-
schema: &Valid<Schema>,
224-
document: &Valid<ExecutableDocument>,
225-
variable_values: &Valid<JsonMap>,
226-
errors: &mut Vec<GraphQLError>,
210+
///
211+
/// `object_value: None` is a special case for top-level of `introspection::partial_execute`
212+
///
213+
/// Return `Ok(None)` for silently skipping that field.
214+
fn execute_field<'a>(
215+
ctx: &mut ExecutionContext<'a>,
227216
path: LinkedPath<'_>,
228217
mode: ExecutionMode,
229-
object_value: &ObjectValue<'_>,
218+
object_type: &ObjectType,
219+
object_value: Option<&dyn ObjectValue>,
230220
field_def: &FieldDefinition,
231-
fields: &[&Field],
232-
) -> Result<JsonValue, PropagateNull> {
221+
fields: &[&'a Field],
222+
) -> Result<Option<JsonValue>, PropagateNull> {
233223
let field = fields[0];
234-
let argument_values = match coerce_argument_values(
235-
schema,
236-
document,
237-
variable_values,
238-
errors,
239-
path,
240-
field_def,
241-
field,
242-
) {
224+
let argument_values = match coerce_argument_values(ctx, path, field_def, field) {
243225
Ok(argument_values) => argument_values,
244-
Err(PropagateNull) => return try_nullify(&field_def.ty, Err(PropagateNull)),
226+
Err(PropagateNull) if field_def.ty.is_non_null() => return Err(PropagateNull),
227+
Err(PropagateNull) => return Ok(Some(JsonValue::Null)),
228+
};
229+
let is_field_of_root_query = || {
230+
ctx.schema
231+
.schema_definition
232+
.query
233+
.as_ref()
234+
.is_some_and(|q| q.name == object_type.name)
235+
};
236+
let resolved_result = match field.name.as_str() {
237+
"__typename" => Ok(ResolvedValue::leaf(object_type.name.as_str())),
238+
"__schema" if is_field_of_root_query() => {
239+
let schema = SchemaWithImplementersMap {
240+
schema: ctx.schema,
241+
implementers_map: ctx.implementers_map,
242+
};
243+
Ok(ResolvedValue::object(schema))
244+
}
245+
"__type" if is_field_of_root_query() => {
246+
let schema = SchemaWithImplementersMap {
247+
schema: ctx.schema,
248+
implementers_map: ctx.implementers_map,
249+
};
250+
if let Some(name) = argument_values.get("name").and_then(|v| v.as_str()) {
251+
Ok(crate::introspection::resolvers::type_def(schema, name))
252+
} else {
253+
// This should never happen: `coerce_argument_values()` returns a map that conforms
254+
// to the `__type(name: String!): __Type` definition
255+
// Still, in case of a bug prefer returning an error than panicking
256+
Err(ResolveError {
257+
message: "expected string argument `name`".into(),
258+
})
259+
}
260+
}
261+
_ => {
262+
if let Some(obj) = object_value {
263+
obj.resolve_field(field, &argument_values)
264+
} else {
265+
// Skip non-introspection root fields for `introspection::partial_execute`
266+
return Ok(None);
267+
}
268+
}
245269
};
246-
let resolved_result = object_value.resolve_field(&field.name, &argument_values);
247270
let completed_result = match resolved_result {
248-
Ok(resolved) => complete_value(
249-
schema,
250-
document,
251-
variable_values,
252-
errors,
253-
path,
254-
mode,
255-
field.ty(),
256-
resolved,
257-
fields,
258-
),
259-
Err(ResolverError { message }) => {
260-
errors.push(GraphQLError::field_error(
271+
Ok(resolved) => complete_value(ctx, path, mode, field.ty(), resolved, fields),
272+
Err(ResolveError { message }) => {
273+
ctx.errors.push(GraphQLError::field_error(
261274
format!("resolver error: {message}"),
262275
path,
263276
field.name.location(),
264-
&document.sources,
277+
&ctx.document.sources,
265278
));
266279
Err(PropagateNull)
267280
}
268281
};
269-
try_nullify(&field_def.ty, completed_result)
282+
try_nullify(&field_def.ty, completed_result).map(Some)
270283
}
271284

272285
/// Try to insert a propagated null if possible, or keep propagating it.

0 commit comments

Comments
 (0)