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

Commit 59e28ff

Browse files
committed
Nullable 1:1 connections
Fixes #44.
1 parent b046ce8 commit 59e28ff

8 files changed

+1526
-13
lines changed

graphql/tarantool_graphql.lua

+70-13
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ end
230230
--- Convert each field of an avro-schema to a graphql type.
231231
---
232232
--- @tparam table state for read state.accessor and previously filled
233-
--- state.types
233+
--- state.nullable_collection_types
234234
--- @tparam table fields fields part from an avro-schema
235235
---
236236
--- @treturn table `res` -- map with type names as keys and graphql types as
@@ -253,7 +253,7 @@ end
253253
--- The function converts passed avro-schema to a GraphQL type.
254254
---
255255
--- @tparam table state for read state.accessor and previously filled
256-
--- state.types (state.types are gql types)
256+
--- state.nullable_collection_types (those are gql types)
257257
--- @tparam table avro_schema input avro-schema
258258
--- @tparam[opt] table collection table with schema_name, connections fields
259259
--- described a collection (e.g. tarantool's spaces)
@@ -307,8 +307,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
307307
for _, c in ipairs((collection or {}).connections or {}) do
308308
assert(type(c.type) == 'string',
309309
'connection.type must be a string, got ' .. type(c.type))
310-
assert(c.type == '1:1' or c.type == '1:N',
311-
'connection.type must be 1:1 or 1:N, got ' .. c.type)
310+
assert(c.type == '1:1' or c.type == '1:1*' or c.type == '1:N',
311+
'connection.type must be 1:1, 1:1* or 1:N, got ' .. c.type)
312312
assert(type(c.name) == 'string',
313313
'connection.name must be a string, got ' .. type(c.name))
314314
assert(type(c.destination_collection) == 'string',
@@ -319,16 +319,20 @@ gql_type = function(state, avro_schema, collection, collection_name)
319319

320320
-- gql type of connection field
321321
local destination_type =
322-
state.types[c.destination_collection]
322+
state.nullable_collection_types[c.destination_collection]
323323
assert(destination_type ~= nil,
324324
('destination_type (named %s) must not be nil'):format(
325325
c.destination_collection))
326326

327327
local c_args
328328
if c.type == '1:1' then
329+
destination_type = types.nonNull(destination_type)
330+
c_args = state.object_arguments[c.destination_collection]
331+
elseif c.type == '1:1*' then
329332
c_args = state.object_arguments[c.destination_collection]
330333
elseif c.type == '1:N' then
331-
destination_type = types.nonNull(types.list(destination_type))
334+
destination_type = types.nonNull(types.list(types.nonNull(
335+
destination_type)))
332336
c_args = state.all_arguments[c.destination_collection]
333337
else
334338
error('unknown connection type: ' .. tostring(c.type))
@@ -343,6 +347,8 @@ gql_type = function(state, avro_schema, collection, collection_name)
343347
resolve = function(parent, args_instance, info)
344348
local destination_args_names = {}
345349
local destination_args_values = {}
350+
local are_all_parts_non_null = true
351+
local are_all_parts_null = true
346352

347353
for _, part in ipairs(c.parts) do
348354
assert(type(part.source_field) == 'string',
@@ -354,8 +360,43 @@ gql_type = function(state, avro_schema, collection, collection_name)
354360

355361
destination_args_names[#destination_args_names + 1] =
356362
part.destination_field
363+
364+
local value = parent[part.source_field]
357365
destination_args_values[#destination_args_values + 1] =
358-
parent[part.source_field]
366+
value
367+
368+
if value ~= nil then -- nil or box.NULL
369+
are_all_parts_null = false
370+
else
371+
are_all_parts_non_null = false
372+
end
373+
end
374+
375+
-- check FULL match constraint before request of
376+
-- destination object(s)
377+
--
378+
-- note that connection key parts can be prefix of index
379+
-- key parts
380+
--
381+
-- zero parts count considered as ok by this check
382+
local is_full_match_ok = are_all_parts_null or
383+
are_all_parts_non_null
384+
if not is_full_match_ok then -- avoid extra json.encode()
385+
assert(is_full_match_ok,
386+
'FULL MATCH constraint was failed: connection ' ..
387+
'key parts must be all non-nulls or all nulls; ' ..
388+
'object: ' .. json.encode(parent))
389+
end
390+
391+
-- When all parts are null:
392+
-- * return null for 1:1* connection;
393+
-- * avoid non-needed index lookup for 1:N connection and
394+
-- return the empty list.
395+
if are_all_parts_null then
396+
assert(c.type == '1:1*' or c.type == '1:N',
397+
'only 1:1* or 1:N connections can have ' ..
398+
'all key parts null')
399+
return c.type == '1:N' and {} or nil
359400
end
360401

361402
local from = {
@@ -386,7 +427,10 @@ gql_type = function(state, avro_schema, collection, collection_name)
386427
assert(type(objs) == 'table',
387428
'objs list received from an accessor ' ..
388429
'must be a table, got ' .. type(objs))
389-
if c.type == '1:1' then
430+
if c.type == '1:1' or c.type == '1:1*' then
431+
-- we expect here exactly one object even for 1:1*
432+
-- connections because we processed all-parts-are-null
433+
-- situation above
390434
assert(#objs == 1,
391435
'expect one matching object, got ' ..
392436
tostring(#objs))
@@ -405,7 +449,7 @@ gql_type = function(state, avro_schema, collection, collection_name)
405449
avro_schema.name,
406450
fields = fields,
407451
})
408-
return avro_t == 'enum' and types.nonNull(res) or res
452+
return avro_t == 'record' and types.nonNull(res) or res
409453
elseif avro_t == 'enum' then
410454
error('enums not implemented yet') -- XXX
411455
elseif avro_t == 'array' or avro_t == 'array*' then
@@ -445,7 +489,12 @@ end
445489

446490
local function parse_cfg(cfg)
447491
local state = {}
448-
state.types = utils.gen_booking_table({})
492+
493+
-- collection type is always record, so always non-null; we can lazily
494+
-- evaluate non-null type from nullable type, but not vice versa, so we
495+
-- collect nullable types here and evaluate non-null ones where needed
496+
state.nullable_collection_types = utils.gen_booking_table({})
497+
449498
state.object_arguments = utils.gen_booking_table({})
450499
state.list_arguments = utils.gen_booking_table({})
451500
state.all_arguments = utils.gen_booking_table({})
@@ -481,8 +530,15 @@ local function parse_cfg(cfg)
481530
assert(schema.type == 'record',
482531
'top-level schema must have record avro type, got ' ..
483532
tostring(schema.type))
484-
state.types[collection_name] = gql_type(state, schema, collection,
485-
collection_name)
533+
local collection_type =
534+
gql_type(state, schema, collection, collection_name)
535+
-- we utilize the fact that collection type is always non-null and
536+
-- don't store this information; see comment above for
537+
-- `nullable_collection_types` variable definition
538+
assert(collection_type.__type == 'NonNull',
539+
'collection must always has non-null type')
540+
state.nullable_collection_types[collection_name] =
541+
nullable(collection_type)
486542

487543
-- prepare arguments' types
488544
local object_args = convert_record_fields_to_args(schema.fields,
@@ -497,7 +553,8 @@ local function parse_cfg(cfg)
497553

498554
-- create entry points from collection names
499555
fields[collection_name] = {
500-
kind = types.nonNull(types.list(state.types[collection_name])),
556+
kind = types.nonNull(types.list(types.nonNull(
557+
state.nullable_collection_types[collection_name]))),
501558
arguments = state.all_arguments[collection_name],
502559
resolve = function(rootValue, args_instance, info)
503560
local object_args_instance = {} -- passed to 'filter'
+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
+---------------------+
2+
| a-+ h x y |
3+
| |\ \ |\ |
4+
| b c d k l |
5+
| | |\ \ |
6+
| e f g m |
7+
+---------------------+
8+
RUN downside_a {{{
9+
QUERY
10+
query emails_tree_downside($body: String) {
11+
email(body: $body) {
12+
body
13+
successors {
14+
body
15+
successors {
16+
body
17+
successors {
18+
body
19+
}
20+
}
21+
}
22+
}
23+
}
24+
VARIABLES
25+
---
26+
body: a
27+
...
28+
29+
RESULT
30+
---
31+
email:
32+
- successors:
33+
- successors: &0 []
34+
body: c
35+
- successors:
36+
- successors: *0
37+
body: e
38+
body: b
39+
- successors:
40+
- successors: *0
41+
body: f
42+
- successors: *0
43+
body: g
44+
body: d
45+
body: a
46+
...
47+
48+
}}}
49+
50+
RUN downside_h {{{
51+
QUERY
52+
query emails_tree_downside($body: String) {
53+
email(body: $body) {
54+
body
55+
successors {
56+
body
57+
successors {
58+
body
59+
successors {
60+
body
61+
}
62+
}
63+
}
64+
}
65+
}
66+
VARIABLES
67+
---
68+
body: h
69+
...
70+
71+
RESULT
72+
---
73+
email:
74+
- successors:
75+
- successors:
76+
- successors: &0 []
77+
body: m
78+
body: l
79+
- successors: *0
80+
body: k
81+
body: h
82+
...
83+
84+
}}}
85+
86+
RUN upside {{{
87+
QUERY
88+
query emails_trace_upside($body: String) {
89+
email(body: $body) {
90+
body
91+
in_reply_to {
92+
body
93+
in_reply_to {
94+
body
95+
in_reply_to {
96+
body
97+
}
98+
}
99+
}
100+
}
101+
}
102+
VARIABLES
103+
---
104+
body: f
105+
...
106+
107+
RESULT
108+
---
109+
email:
110+
- body: f
111+
in_reply_to:
112+
body: d
113+
in_reply_to:
114+
body: a
115+
...
116+
117+
}}}
118+
119+
RUN upside_x {{{
120+
QUERY
121+
query emails_trace_upside($body: String) {
122+
email(body: $body) {
123+
body
124+
in_reply_to {
125+
body
126+
in_reply_to {
127+
body
128+
in_reply_to {
129+
body
130+
}
131+
}
132+
}
133+
}
134+
}
135+
VARIABLES
136+
---
137+
body: x
138+
...
139+
140+
RESULT
141+
---
142+
ok: false
143+
err: 'FULL MATCH constraint was failed: connection key parts must be all non-nulls
144+
or all nulls; object: {"domain":"graphql.tarantool.org","localpart":"ea0ff0e87b09df64de0919184347c10b","body":"x","in_reply_to_domain":"graphql.tarantool.org","in_reply_to_localpart":null}'
145+
...
146+
147+
}}}
148+
149+
RUN upside_y {{{
150+
QUERY
151+
query emails_trace_upside($body: String) {
152+
email(body: $body) {
153+
body
154+
in_reply_to {
155+
body
156+
in_reply_to {
157+
body
158+
in_reply_to {
159+
body
160+
}
161+
}
162+
}
163+
}
164+
}
165+
VARIABLES
166+
---
167+
body: y
168+
...
169+
170+
RESULT
171+
---
172+
ok: false
173+
err: 'FULL MATCH constraint was failed: connection key parts must be all non-nulls
174+
or all nulls; object: {"domain":"graphql.tarantool.org","localpart":"ca7a63a65f4bf77e1dd7c5d55aa64e1e","body":"y","in_reply_to_domain":null,"in_reply_to_localpart":"ca7a63a65f4bf77e1dd7c5d55aa64e1e"}'
175+
...
176+
177+
}}}
178+

0 commit comments

Comments
 (0)