Skip to content

Commit 5a7bee4

Browse files
committed
Fix serial execution not always executing in order.
The executor will now choose to use an ordered dict implementation when collecting the root fields of query/mutation, rather than an unordered-dict implementation to guarantee the order of execution when being executed serially is the same as it is defined in the query. No order guarantees are given for non-serial execution, as per the spec.
1 parent 0c83799 commit 5a7bee4

File tree

2 files changed

+36
-17
lines changed

2 files changed

+36
-17
lines changed

graphql/core/execution/base.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,24 +82,31 @@ def __init__(self, data=None, errors=None, invalid=False):
8282
error.value if isinstance(error, DeferredException) else error
8383
for error in errors
8484
]
85+
8586
self.errors = errors
87+
8688
if invalid:
8789
assert data is None
90+
8891
self.invalid = invalid
8992

9093

9194
def get_operation_root_type(schema, operation):
9295
op = operation.operation
9396
if op == 'query':
9497
return schema.get_query_type()
98+
9599
elif op == 'mutation':
96100
mutation_type = schema.get_mutation_type()
101+
97102
if not mutation_type:
98103
raise GraphQLError(
99104
'Schema is not configured for mutations',
100105
[operation]
101106
)
107+
102108
return mutation_type
109+
103110
raise GraphQLError(
104111
'Can only execute queries and mutations',
105112
[operation]
@@ -109,35 +116,36 @@ def get_operation_root_type(schema, operation):
109116
def collect_fields(ctx, type, selection_set, fields, prev_fragment_names):
110117
for selection in selection_set.selections:
111118
directives = selection.directives
119+
112120
if isinstance(selection, ast.Field):
113121
if not should_include_node(ctx, directives):
114122
continue
123+
115124
name = get_field_entry_key(selection)
116-
if name not in fields:
117-
fields[name] = []
118125
fields[name].append(selection)
126+
119127
elif isinstance(selection, ast.InlineFragment):
120-
if not should_include_node(ctx, directives) or \
121-
not does_fragment_condition_match(ctx, selection, type):
128+
if not should_include_node(ctx, directives) or not does_fragment_condition_match(ctx, selection, type):
122129
continue
123-
collect_fields(
124-
ctx, type, selection.selection_set,
125-
fields, prev_fragment_names)
130+
131+
collect_fields(ctx, type, selection.selection_set, fields, prev_fragment_names)
132+
126133
elif isinstance(selection, ast.FragmentSpread):
127134
frag_name = selection.name.value
128-
if frag_name in prev_fragment_names or \
129-
not should_include_node(ctx, directives):
135+
136+
if frag_name in prev_fragment_names or not should_include_node(ctx, directives):
130137
continue
138+
131139
prev_fragment_names.add(frag_name)
132140
fragment = ctx.fragments.get(frag_name)
133141
frag_directives = fragment.directives
134-
if not fragment or \
135-
not should_include_node(ctx, frag_directives) or \
136-
not does_fragment_condition_match(ctx, fragment, type):
142+
if not fragment or not \
143+
should_include_node(ctx, frag_directives) or not \
144+
does_fragment_condition_match(ctx, fragment, type):
137145
continue
138-
collect_fields(
139-
ctx, type, fragment.selection_set,
140-
fields, prev_fragment_names)
146+
147+
collect_fields(ctx, type, fragment.selection_set, fields, prev_fragment_names)
148+
141149
return fields
142150

143151

@@ -146,10 +154,12 @@ def should_include_node(ctx, directives):
146154
@skip directives, where @skip has higher precidence than @include."""
147155
if directives:
148156
skip_ast = None
157+
149158
for directive in directives:
150159
if directive.name.value == GraphQLSkipDirective.name:
151160
skip_ast = directive
152161
break
162+
153163
if skip_ast:
154164
args = get_argument_values(
155165
GraphQLSkipDirective.args,
@@ -159,16 +169,19 @@ def should_include_node(ctx, directives):
159169
return not args.get('if')
160170

161171
include_ast = None
172+
162173
for directive in directives:
163174
if directive.name.value == GraphQLIncludeDirective.name:
164175
include_ast = directive
165176
break
177+
166178
if include_ast:
167179
args = get_argument_values(
168180
GraphQLIncludeDirective.args,
169181
include_ast.arguments,
170182
ctx.variables,
171183
)
184+
172185
return bool(args.get('if'))
173186

174187
return True

graphql/core/execution/executor.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ..language import ast
77
from ..language.parser import parse
88
from ..language.source import Source
9+
from ..pyutils.default_ordered_dict import DefaultOrderedDict
910
from ..type import GraphQLEnumType, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, \
1011
GraphQLScalarType, GraphQLUnionType
1112
from ..utils.is_nullish import is_nullish
@@ -76,9 +77,14 @@ def _execute_graphql_query(self, root, ast, operation_name, args, request_contex
7677

7778
def _execute_operation(self, ctx, root, operation, execute_serially):
7879
type = get_operation_root_type(ctx.schema, operation)
79-
fields = collect_fields(ctx, type, operation.selection_set, {}, set())
8080

8181
if operation.operation == 'mutation' or execute_serially:
82+
execute_serially = True
83+
84+
field_map = DefaultOrderedDict(list) if execute_serially else collections.defaultdict(list)
85+
fields = collect_fields(ctx, type, operation.selection_set, field_map, set())
86+
87+
if execute_serially:
8288
return self._execute_fields_serially(ctx, type, root, fields)
8389

8490
return self._execute_fields(ctx, type, root, fields)
@@ -277,7 +283,7 @@ def complete_value(self, ctx, return_type, field_asts, info, result):
277283
)
278284

279285
# Collect sub-fields to execute to complete this value.
280-
subfield_asts = {}
286+
subfield_asts = collections.defaultdict(list)
281287
visited_fragment_names = set()
282288
for field_ast in field_asts:
283289
selection_set = field_ast.selection_set

0 commit comments

Comments
 (0)