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

Commit f79bdaf

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

8 files changed

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

0 commit comments

Comments
 (0)