Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit ca1e0e7

Browse files
committed
WIP: Implement breadth-first executor
1 parent 38b8504 commit ca1e0e7

File tree

3 files changed

+248
-4
lines changed

3 files changed

+248
-4
lines changed

graphql/execute.lua

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
local utils = require('graphql.utils')
2+
local core_util = require('graphql.core.util')
3+
4+
-- short name
5+
local check = utils.check
6+
7+
local execute = {}
8+
9+
--- Get graphql-lua object representing operation.
10+
---
11+
--- @tparam table query_ast
12+
--- @tparam string operation_name
13+
---
14+
--- @treturn table `operation`
15+
local function get_operation(query_ast, operation_name)
16+
-- XXX: make operation name optional in case of an one operation in the
17+
-- schema, see query_util.buildContext
18+
check(operation_name, 'operation_name', 'string')
19+
20+
local operation
21+
22+
for _, definition in ipairs(query_ast.definitions) do
23+
if definition.kind == 'operation' then
24+
if definition.name.value == operation_name then
25+
operation = definition
26+
end
27+
end
28+
end
29+
30+
assert(operation ~= nil,
31+
('cannot find operation "%s"'):format(operation_name))
32+
return operation
33+
end
34+
35+
--- XXX
36+
local function get_argument_type(field_type, argument_name)
37+
for name, argument_type in pairs(field_type.arguments) do
38+
if name == argument_name then
39+
return argument_type
40+
end
41+
end
42+
error(('cannot find argument "%s"'):format(argument_name))
43+
end
44+
45+
local function get_argument_values(field_type, selection, variables)
46+
local args = {}
47+
for _, argument in ipairs(selection.arguments or {}) do
48+
local argument_name = argument.name.value
49+
assert(argument_name ~= nil)
50+
local argument_type = get_argument_type(field_type, argument_name)
51+
local value = core_util.coerceValue(argument.value, argument_type,
52+
variables)
53+
args[argument_name] = value
54+
end
55+
return args
56+
end
57+
58+
local function filter_object(object, object_type, selections, qcontext,
59+
variables)
60+
local required_fields = {}
61+
local selections_per_fields = {}
62+
63+
-- XXX: allow null for object if object_type is nullable (we should not
64+
-- skip NonNull below for an element)
65+
66+
-- XXX: support aliases, fragments, directives
67+
for _, selection in ipairs(selections) do
68+
assert(selection.alias == nil, 'NIY: aliases')
69+
local field_name = selection.name.value
70+
required_fields[field_name] = true
71+
selections_per_fields[field_name] = selection
72+
end
73+
74+
local filtered_object = {}
75+
local fields_info = {}
76+
77+
for field_name, _ in pairs(required_fields) do
78+
local field_type = object_type.fields[field_name]
79+
assert(field_type ~= nil)
80+
81+
if field_type.prepare_resolve then
82+
-- XXX: attend NonNull somehow
83+
-- set deduce inner_type and set is_list
84+
local is_list = false
85+
local inner_type = field_type.kind
86+
while inner_type.ofType ~= nil do
87+
if inner_type.__type == 'List' then
88+
is_list = true
89+
inner_type = inner_type.ofType
90+
elseif inner_type.__type == 'NonNull' then
91+
inner_type = inner_type.ofType
92+
else
93+
error('unknown __type: ' .. tostring(inner_type.__type))
94+
end
95+
end
96+
97+
local selection = selections_per_fields[field_name]
98+
local args = get_argument_values(field_type, selection, variables)
99+
100+
local info = {qcontext = qcontext}
101+
local prepared_resolve = field_type.prepare_resolve(
102+
object, args, info)
103+
fields_info[field_name] = {
104+
is_list = is_list,
105+
kind = inner_type,
106+
prepared_resolve = prepared_resolve,
107+
selections = selection.selectionSet.selections,
108+
}
109+
else
110+
-- XXX: is call to coerceValue needed here?
111+
if object[field_name] ~= nil then
112+
filtered_object[field_name] = object[field_name]
113+
end
114+
end
115+
end
116+
117+
local prepared_object = {
118+
filtered_object = filtered_object,
119+
fields_info = fields_info,
120+
}
121+
return prepared_object
122+
end
123+
124+
local function filter_object_list(object_list, object_type, selections,
125+
qcontext, variables)
126+
local prepared_object_list = {}
127+
128+
for _, object in ipairs(object_list) do
129+
local prepared_object = filter_object(object, object_type, selections,
130+
qcontext, variables)
131+
table.insert(prepared_object_list, prepared_object)
132+
end
133+
134+
return prepared_object_list
135+
end
136+
137+
local function invoke_resolve(prepared_object, qcontext, variables)
138+
local open_set = {}
139+
140+
for field_name, field_info in pairs(prepared_object.fields_info) do
141+
local object_or_list = field_info.prepared_resolve:invoke()
142+
local object_type = field_info.kind
143+
local selections = field_info.selections
144+
145+
-- XXX: maybe we can always process lists and deduce is_list from
146+
-- graphql types
147+
148+
local child
149+
if field_info.is_list then
150+
local child_prepared_object_list = filter_object_list(
151+
object_or_list, object_type, selections, qcontext,
152+
variables)
153+
154+
-- construction
155+
prepared_object.filtered_object[field_name] = {}
156+
for _, child_prepared_object in
157+
ipairs(child_prepared_object_list) do
158+
table.insert(prepared_object.filtered_object[field_name],
159+
child_prepared_object.filtered_object)
160+
end
161+
162+
child = {
163+
prepared_object_list = child_prepared_object_list,
164+
}
165+
else
166+
local child_prepared_object = filter_object(object_or_list,
167+
object_type, selections, qcontext, variables)
168+
169+
-- construction
170+
prepared_object.filtered_object[field_name] =
171+
child_prepared_object.filtered_object
172+
173+
child = {
174+
prepared_object = child_prepared_object,
175+
}
176+
end
177+
178+
table.insert(open_set, child)
179+
end
180+
181+
return open_set
182+
end
183+
184+
local function invoke_resolve_list(prepared_object_list, qcontext, variables)
185+
local open_set = {}
186+
187+
for _, prepared_object in ipairs(prepared_object_list) do
188+
local child_open_set = invoke_resolve(prepared_object, qcontext,
189+
variables)
190+
utils.expand_list(open_set, child_open_set)
191+
end
192+
193+
return open_set
194+
end
195+
196+
--- XXX
197+
function execute.execute(schema, query_ast, variables, operation_name)
198+
local operation = get_operation(query_ast, operation_name)
199+
local root_object_type = schema[operation.operation]
200+
assert(root_object_type ~= nil,
201+
('cannot find root type for operation "%s"'):format(operation_name))
202+
local root_selections = operation.selectionSet.selections
203+
204+
local qcontext = {}
205+
local root_object = {}
206+
207+
local prepared_root_object = filter_object(
208+
root_object, root_object_type, root_selections, qcontext, variables)
209+
local filtered_root_object = prepared_root_object.filtered_object
210+
local open_set = invoke_resolve(prepared_root_object, qcontext, variables)
211+
212+
-- XXX: don't mix requests to different fields, make all requests for an
213+
-- one field, the for the next: add requests resort stage
214+
215+
while true do
216+
local item = table.remove(open_set, 1)
217+
if item == nil then break end
218+
local child_open_set
219+
if item.prepared_object ~= nil then
220+
child_open_set = invoke_resolve(item.prepared_object, qcontext,
221+
variables)
222+
elseif item.prepared_object_list ~= nil then
223+
child_open_set = invoke_resolve_list(item.prepared_object_list,
224+
qcontext, variables)
225+
else
226+
assert(false,
227+
'cannot find prepared_object nor prepared_object_list')
228+
end
229+
230+
utils.expand_list(open_set, child_open_set)
231+
end
232+
233+
return filtered_root_object
234+
end
235+
236+
return execute

graphql/tarantool_graphql.lua

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ local parse = require('graphql.core.parse')
3939
local schema = require('graphql.core.schema')
4040
local types = require('graphql.core.types')
4141
local validate = require('graphql.core.validate')
42-
local execute = require('graphql.core.execute')
42+
local execute = require('graphql.execute')
4343
local query_to_avro = require('graphql.query_to_avro')
4444
local simple_config = require('graphql.simple_config')
4545
local config_complement = require('graphql.config_complement')
@@ -1323,13 +1323,11 @@ local function gql_execute(qstate, variables)
13231323
assert(type(variables) == 'table', 'variables must be table, got ' ..
13241324
type(variables))
13251325

1326-
local root_value = {}
13271326
local operation_name = qstate.operation_name
13281327
assert(type(operation_name) == 'string',
13291328
'operation_name must be a string, got ' .. type(operation_name))
13301329

1331-
return execute(state.schema, qstate.ast, root_value, variables,
1332-
operation_name)
1330+
return execute.execute(state.schema, qstate.ast, variables, operation_name)
13331331
end
13341332

13351333
local function compile_and_execute(state, query, variables)

graphql/utils.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,14 @@ function utils.value_in(value, array)
213213
return false
214214
end
215215

216+
--- Append all elements of the list `tail` to the end of list `list`.
217+
---
218+
--- @tparam table list list to add elements to
219+
--- @tparam table tail list to add elements from
220+
function utils.expand_list(list, tail)
221+
for _, item in ipairs(tail) do
222+
table.insert(list, item)
223+
end
224+
end
225+
216226
return utils

0 commit comments

Comments
 (0)