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

Commit 13c5c7b

Browse files
committed
Nullable 1:1 connections
Fixes #44.
1 parent 096fead commit 13c5c7b

8 files changed

+1554
-14
lines changed

graphql/tarantool_graphql.lua

+72-14
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,45 @@ 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). Note that connection key parts
377+
-- can be prefix of index key parts. Zero parts count
378+
-- considered as ok by this check.
379+
local ok = are_all_parts_null or are_all_parts_non_null
380+
if not ok then -- avoid extra json.encode()
381+
assert(ok,
382+
'FULL MATCH constraint was failed: connection ' ..
383+
'key parts must be all non-nulls or all nulls; ' ..
384+
'object: ' .. json.encode(parent))
385+
end
386+
387+
-- Avoid non-needed index lookup on a destination
388+
-- collection when all connection parts are null:
389+
-- * return null for 1:1* connection;
390+
-- * return {} for 1:N connection (except the case when
391+
-- source collection is the Query pseudo-collection).
392+
if collection_name ~= 'Query' and are_all_parts_null then
393+
if c.type ~= '1:1*' and c.type ~= '1:N' then
394+
-- `if` is to avoid extra json.encode
395+
assert(c.type == '1:1*' or c.type == '1:N',
396+
('only 1:1* or 1:N connections can have ' ..
397+
'all key parts null; parent is %s from ' ..
398+
'collection "%s"'):format(json.encode(parent),
399+
tostring(collection_name)))
400+
end
401+
return c.type == '1:N' and {} or nil
359402
end
360403

361404
local from = {
@@ -386,7 +429,10 @@ gql_type = function(state, avro_schema, collection, collection_name)
386429
assert(type(objs) == 'table',
387430
'objs list received from an accessor ' ..
388431
'must be a table, got ' .. type(objs))
389-
if c.type == '1:1' then
432+
if c.type == '1:1' or c.type == '1:1*' then
433+
-- we expect here exactly one object even for 1:1*
434+
-- connections because we processed all-parts-are-null
435+
-- situation above
390436
assert(#objs == 1,
391437
'expect one matching object, got ' ..
392438
tostring(#objs))
@@ -405,7 +451,7 @@ gql_type = function(state, avro_schema, collection, collection_name)
405451
avro_schema.name,
406452
fields = fields,
407453
})
408-
return avro_t == 'enum' and types.nonNull(res) or res
454+
return avro_t == 'record' and types.nonNull(res) or res
409455
elseif avro_t == 'enum' then
410456
error('enums not implemented yet') -- XXX
411457
elseif avro_t == 'array' or avro_t == 'array*' then
@@ -476,15 +522,21 @@ local function create_root_collection(state)
476522

477523
-- `gql_type` is designed to create GQL type corresponding to a real schema
478524
-- and connections. However it also works with the fake schema.
525+
-- Query type must be the Object, so it cannot be nonNull.
479526
local root_type = gql_type(state, root_schema, root_collection, "Query")
480527
state.schema = schema.create({
481-
query = root_type
528+
query = nullable(root_type),
482529
})
483530
end
484531

485532
local function parse_cfg(cfg)
486533
local state = {}
487-
state.types = utils.gen_booking_table({})
534+
535+
-- collection type is always record, so always non-null; we can lazily
536+
-- evaluate non-null type from nullable type, but not vice versa, so we
537+
-- collect nullable types here and evaluate non-null ones where needed
538+
state.nullable_collection_types = utils.gen_booking_table({})
539+
488540
state.object_arguments = utils.gen_booking_table({})
489541
state.list_arguments = utils.gen_booking_table({})
490542
state.all_arguments = utils.gen_booking_table({})
@@ -523,8 +575,15 @@ local function parse_cfg(cfg)
523575
assert(schema.type == 'record',
524576
'top-level schema must have record avro type, got ' ..
525577
tostring(schema.type))
526-
state.types[collection_name] = gql_type(state, schema, collection,
527-
collection_name)
578+
local collection_type =
579+
gql_type(state, schema, collection, collection_name)
580+
-- we utilize the fact that collection type is always non-null and
581+
-- don't store this information; see comment above for
582+
-- `nullable_collection_types` variable definition
583+
assert(collection_type.__type == 'NonNull',
584+
'collection must always has non-null type')
585+
state.nullable_collection_types[collection_name] =
586+
nullable(collection_type)
528587

529588
-- prepare arguments' types
530589
local object_args = convert_record_fields_to_args(schema.fields,
@@ -536,7 +595,6 @@ local function parse_cfg(cfg)
536595
state.object_arguments[collection_name] = object_args
537596
state.list_arguments[collection_name] = list_args
538597
state.all_arguments[collection_name] = args
539-
540598
end
541599
-- create fake root `Query` collection
542600
create_root_collection(state)
+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: g
40+
- successors: *0
41+
body: f
42+
body: d
43+
- successors:
44+
- successors: *0
45+
body: e
46+
body: b
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":"062b56b1885c71c51153ccb880ac7315","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":"1f70391f6ba858129413bd801b12acbf","body":"y","in_reply_to_domain":null,"in_reply_to_localpart":"1f70391f6ba858129413bd801b12acbf"}'
177+
...
178+
179+
}}}
180+

0 commit comments

Comments
 (0)