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

Commit 143c35e

Browse files
authored
Merge pull request #178 from tarantool/gh-135-relax-1-1-connection-constraint
Remove 1:1* connections, relax constraints of 1:1 ones
2 parents 08704ed + 11d5e4e commit 143c35e

10 files changed

+205
-146
lines changed

graphql/accessor_general.lua

+53-5
Original file line numberDiff line numberDiff line change
@@ -883,8 +883,7 @@ local function process_tuple(self, state, tuple, opts)
883883
if obj[k] == nil then
884884
local field_name = k
885885
local sub_filter = v
886-
local sub_opts = {dont_force_nullability = true}
887-
local field = resolveField(field_name, obj, sub_filter, sub_opts)
886+
local field = resolveField(field_name, obj, sub_filter)
888887
if field == nil then return true end
889888
obj[k] = field
890889
-- XXX: Remove the value from a filter? But then we need to copy
@@ -991,6 +990,7 @@ local function select_internal(self, collection_name, from, filter, args, extra)
991990
-- XXX: save type of args.offset at parsing and check here
992991
-- check(args.offset, 'args.offset', ...)
993992
check(args.pcre, 'args.pcre', 'table', 'nil')
993+
check(extra.exp_tuple_count, 'extra.exp_tuple_count', 'number', 'nil')
994994

995995
local collection = self.collections[collection_name]
996996
assert(collection ~= nil,
@@ -1044,6 +1044,18 @@ local function select_internal(self, collection_name, from, filter, args, extra)
10441044
resolveField = extra.resolveField,
10451045
}
10461046

1047+
-- assert that connection constraint applied only to objects got from the
1048+
-- index that underlies the connection
1049+
if extra.exp_tuple_count ~= nil then
1050+
local err = 'internal error: connection constraint (expected tuple ' ..
1051+
'count) cannot be applied to an index that is not under a ' ..
1052+
'connection'
1053+
assert(from.collection_name ~= nil, err)
1054+
assert(index ~= nil, err)
1055+
assert(pivot == nil or (pivot.value_list == nil and
1056+
pivot.filter ~= nil), err)
1057+
end
1058+
10471059
if index == nil then
10481060
-- fullscan
10491061
local primary_index = self.funcs.get_primary_index(self,
@@ -1088,21 +1100,38 @@ local function select_internal(self, collection_name, from, filter, args, extra)
10881100
iterator_opts.limit = args.limit
10891101
end
10901102

1103+
local tuple_count = 0
1104+
10911105
for _, tuple in index:pairs(index_value, iterator_opts) do
1106+
tuple_count = tuple_count + 1
1107+
-- check full match constraint
1108+
if extra.exp_tuple_count ~= nil and
1109+
tuple_count > extra.exp_tuple_count then
1110+
error(('FULL MATCH constraint was failed: we got more then ' ..
1111+
'%d tuples'):format(extra.exp_tuple_count))
1112+
end
10921113
local continue = process_tuple(self, select_state, tuple,
10931114
select_opts)
10941115
if not continue then break end
10951116
end
1117+
1118+
-- check full match constraint
1119+
if extra.exp_tuple_count ~= nil and
1120+
tuple_count ~= extra.exp_tuple_count then
1121+
error(('FULL MATCH constraint was failed: we expect %d tuples, ' ..
1122+
'got %d'):format(extra.exp_tuple_count, tuple_count))
1123+
end
10961124
end
10971125

10981126
local count = select_state.count
10991127
local objs = select_state.objs
11001128

11011129
assert(args.limit == nil or count <= args.limit,
1102-
('count[%d] exceeds limit[%s] (before return)'):format(
1103-
count, args.limit))
1130+
('internal error: selected objects count (%d) exceeds limit (%s)')
1131+
:format(count, args.limit))
11041132
assert(#objs == count,
1105-
('count[%d] is not equal to objs count[%d]'):format(count, #objs))
1133+
('internal error: selected objects count (%d) is not equal size of ' ..
1134+
'selected object list (%d)'):format(count, #objs))
11061135

11071136
return objs
11081137
end
@@ -1383,6 +1412,25 @@ end
13831412
--- @treturn table data accessor instance, a table with the two methods
13841413
--- (`select` and `arguments`) as described in the @{impl.new} function
13851414
--- description.
1415+
---
1416+
--- Brief explanation of some select function parameters:
1417+
---
1418+
--- * `from` (table or nil) is nil for a top-level collection or a table with
1419+
--- the following fields:
1420+
---
1421+
--- - collection_name
1422+
--- - connection_name
1423+
--- - destination_args_names
1424+
--- - destination_args_values
1425+
---
1426+
--- * `extra` (table) is a table which contains additional data for the query:
1427+
---
1428+
--- - `qcontext` (table) can be used by an accessor to store any
1429+
--- query-related data;
1430+
--- - `resolveField(field_name, object, filter, opts)` (function) for
1431+
--- performing a subrequest on a fields connected using a 1:1 connection.
1432+
--- - extra_args
1433+
--- - exp_tuple_count
13861434
function accessor_general.new(opts, funcs)
13871435
assert(type(opts) == 'table',
13881436
'opts must be a table, got ' .. type(opts))

graphql/config_complement.lua

+11-55
Original file line numberDiff line numberDiff line change
@@ -9,64 +9,25 @@
99
local json = require('json')
1010
local yaml = require('yaml')
1111
local log = require('log')
12+
1213
local utils = require('graphql.utils')
1314
local check = utils.check
14-
local get_spaces_formats = require('graphql.simple_config').get_spaces_formats
1515

1616
local config_complement = {}
1717

18-
--- The function determines connection type by connection.parts
19-
--- and source collection space format.
20-
---
21-
--- XXX Currently there are two possible situations when connection_parts form
22-
--- unique index - all source_fields are nullable (1:1*) or all source_fields
23-
--- are non nullable (1:1). In case of partially nullable connection_parts (which
24-
--- form unique index) the error is raised. There is an alternative: relax
25-
--- this requirement and deduce non-null connection type in the case.
26-
local function determine_connection_type(connection_parts, index, source_space_format)
27-
local type
28-
29-
if #connection_parts < #(index.fields) then
30-
type = '1:N'
31-
end
32-
33-
if #connection_parts == #(index.fields) then
34-
if index.unique then
35-
type = '1:1'
36-
else
37-
type = '1:N'
38-
end
39-
end
40-
41-
local is_all_nullable = true
42-
local is_all_not_nullable = true
43-
44-
for _, connection_part in pairs(connection_parts) do
45-
for _,field_format in ipairs(source_space_format) do
46-
if connection_part.source_field == field_format.name then
47-
if field_format.is_nullable == true then
48-
is_all_not_nullable = false
49-
else
50-
is_all_nullable = false
51-
end
52-
end
53-
end
54-
end
55-
56-
if is_all_nullable == is_all_not_nullable and type == '1:1' then
57-
error('source_fields in connection_parts must be all nullable or ' ..
58-
'not nullable at the same time')
18+
--- Determine connection type by connection.parts and index uniqueness.
19+
local function determine_connection_type(connection_parts, index)
20+
if #connection_parts < #index.fields then
21+
return '1:N'
22+
elseif #connection_parts == #index.fields then
23+
return index.unique and '1:1' or '1:N'
5924
end
6025

61-
if is_all_nullable and type == '1:1' then
62-
type = '1:1*'
63-
end
64-
65-
return type
26+
error(('Connection parts count is more then index parts count: %d > %d')
27+
:format(#connection_parts, #index.fields))
6628
end
6729

68-
-- The function returns connection_parts sorted by destination_fields as
69-
-- index_fields prefix.
30+
-- Return connection_parts sorted by destination_fields as index_fields prefix.
7031
local function sort_parts(connection_parts, index_fields)
7132
local sorted_parts = {}
7233

@@ -211,8 +172,6 @@ local function complement_connections(collections, connections, indexes)
211172
check(collections, 'collections', 'table')
212173
check(connections, 'connections', 'table')
213174

214-
local spaces_formats = get_spaces_formats()
215-
216175
for _, c in pairs(connections) do
217176
check(c.name, 'connection.name', 'string')
218177
check(c.source_collection, 'connection.source_collection', 'string')
@@ -230,10 +189,7 @@ local function complement_connections(collections, connections, indexes)
230189
result_c.destination_collection = c.destination_collection
231190
result_c.parts = determine_connection_parts(c.parts, index)
232191

233-
local source_space_format = spaces_formats[result_c.source_collection]
234-
235-
result_c.type = determine_connection_type(result_c.parts, index,
236-
source_space_format)
192+
result_c.type = determine_connection_type(result_c.parts, index)
237193
result_c.index_name = c.index_name
238194
result_c.name = c.name
239195

graphql/convert_schema/resolve.lua

+25-30
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ local function gen_from_parameter(collection_name, parent, connection)
2929
}
3030
end
3131

32-
-- Check FULL match constraint before request of
33-
-- destination object(s). Note that connection key parts
34-
-- can be prefix of index key parts. Zero parts count
35-
-- considered as ok by this check.
32+
--- Check FULL match constraint before request of destination object(s).
33+
---
34+
--- Note that connection key parts can be prefix of index key parts. Zero parts
35+
--- count considered as ok by this check.
3636
local function are_all_parts_null(parent, connection_parts)
3737
local are_all_parts_null = true
3838
local are_all_parts_non_null = true
@@ -84,8 +84,10 @@ local function separate_args_instance(args_instance, arguments)
8484
end
8585

8686
function resolve.gen_resolve_function(collection_name, connection,
87-
destination_type, arguments, accessor)
87+
destination_type, arguments, accessor, opts)
8888
local c = connection
89+
local opts = opts or {}
90+
local disable_dangling_check = opts.disable_dangling_check or false
8991
local bare_destination_type = core_types.bare(destination_type)
9092

9193
-- capture `bare_destination_type`
@@ -106,37 +108,30 @@ function resolve.gen_resolve_function(collection_name, connection,
106108
local opts = opts or {}
107109
assert(type(opts) == 'table',
108110
'opts must be nil or a table, got ' .. type(opts))
109-
local dont_force_nullability =
110-
opts.dont_force_nullability or false
111-
assert(type(dont_force_nullability) == 'boolean',
112-
'opts.dont_force_nullability ' ..
113-
'must be nil or a boolean, got ' ..
114-
type(dont_force_nullability))
111+
-- no opts for now
115112

116113
local from = gen_from_parameter(collection_name, parent, c)
117114

118115
-- Avoid non-needed index lookup on a destination collection when
119116
-- all connection parts are null:
120-
-- * return null for 1:1* connection;
117+
-- * return null for 1:1 connection;
121118
-- * return {} for 1:N connection (except the case when source
122119
-- collection is the query or the mutation pseudo-collection).
123120
if collection_name ~= nil and are_all_parts_null(parent, c.parts) then
124-
if c.type ~= '1:1*' and c.type ~= '1:N' then
125-
-- `if` is to avoid extra json.encode
126-
assert(c.type == '1:1*' or c.type == '1:N',
127-
('only 1:1* or 1:N connections can have ' ..
128-
'all key parts null; parent is %s from ' ..
129-
'collection "%s"'):format(json.encode(parent),
130-
tostring(collection_name)))
131-
end
132121
return c.type == '1:N' and {} or nil
133122
end
134123

124+
local exp_tuple_count
125+
if not disable_dangling_check and c.type == '1:1' then
126+
exp_tuple_count = 1
127+
end
128+
135129
local resolveField = genResolveField(info)
136130
local extra = {
137131
qcontext = info.qcontext,
138132
resolveField = resolveField, -- for subrequests
139133
extra_args = {},
134+
exp_tuple_count = exp_tuple_count,
140135
}
141136

142137
-- object_args_instance will be passed to 'filter'
@@ -152,22 +147,18 @@ function resolve.gen_resolve_function(collection_name, connection,
152147
assert(type(objs) == 'table',
153148
'objs list received from an accessor ' ..
154149
'must be a table, got ' .. type(objs))
155-
if c.type == '1:1' or c.type == '1:1*' then
156-
-- we expect here exactly one object even for 1:1*
157-
-- connections because we processed all-parts-are-null
158-
-- situation above
159-
assert(#objs == 1 or dont_force_nullability,
160-
'expect one matching object, got ' ..
161-
tostring(#objs))
162-
return objs[1]
150+
if c.type == '1:1' then
151+
return objs[1] -- nil for empty list of matching objects
163152
else -- c.type == '1:N'
164153
return objs
165154
end
166155
end
167156
end
168157

169158
function resolve.gen_resolve_function_multihead(collection_name, connection,
170-
union_types, var_num_to_box_field_name, accessor)
159+
union_types, var_num_to_box_field_name, accessor, opts)
160+
local opts = opts or {}
161+
local disable_dangling_check = opts.disable_dangling_check or false
171162
local c = connection
172163

173164
local determinant_keys = utils.get_keys(c.variants[1].determinant)
@@ -208,9 +199,13 @@ function resolve.gen_resolve_function_multihead(collection_name, connection,
208199
name = c.name,
209200
destination_collection = v.destination_collection,
210201
}
202+
local opts = {
203+
disable_dangling_check = disable_dangling_check,
204+
}
211205
-- XXX: generate a function for each variant at schema generation time
212206
local result = resolve.gen_resolve_function(collection_name,
213-
quazi_connection, destination_type, {}, accessor)(parent, {}, info)
207+
quazi_connection, destination_type, {}, accessor, opts)(
208+
parent, {}, info)
214209

215210
-- This 'wrapping' is needed because we use 'select' on 'collection'
216211
-- GraphQL type and the result of the resolve function must be in

graphql/convert_schema/schema.lua

+7-6
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ local schema = {}
2525
--- * DONE: Move avro-schema -> GraphQL arguments translating into its own
2626
--- module.
2727
--- * DONE: Support a sub-record arguments and others (union, array, ...).
28-
--- * TBD: Generate arguments for cartesian product of {1:1, 1:1*, 1:N, all} x
28+
--- * TBD: Generate arguments for cartesian product of {1:1, 1:N, all} x
2929
--- {query, mutation, all} x {top-level, nested, all} x {collections}.
3030
--- * TBD: Use generated arguments in GraphQL types (schema) generation.
3131
---
@@ -138,7 +138,8 @@ local function create_root_collection(state)
138138
})
139139
end
140140

141-
--- Execute a function for each 1:1 or 1:1* connection of each collection.
141+
--- Execute a function for each connection of one of specified types in each
142+
--- collection.
142143
---
143144
--- @tparam table state tarantool_graphql instance
144145
---
@@ -160,7 +161,7 @@ local function for_each_connection(state, connection_types, func)
160161
end
161162
end
162163

163-
--- Add arguments corresponding to 1:1 and 1:1* connections (nested filters).
164+
--- Add arguments corresponding to 1:1 connections (nested filters).
164165
---
165166
--- @tparam table state graphql_tarantool instance
166167
local function add_connection_arguments(state)
@@ -169,8 +170,8 @@ local function add_connection_arguments(state)
169170
-- map source collection and connection name to an input object
170171
local lookup_input_objects = {}
171172

172-
-- create InputObjects for each 1:1 or 1:1* connection of each collection
173-
for_each_connection(state, {'1:1', '1:1*'}, function(collection_name, c)
173+
-- create InputObjects for each 1:1 connection of each collection
174+
for_each_connection(state, {'1:1'}, function(collection_name, c)
174175
-- XXX: support multihead connections
175176
if c.variants ~= nil then return end
176177

@@ -195,7 +196,7 @@ local function add_connection_arguments(state)
195196

196197
-- update fields of collection arguments and input objects with other input
197198
-- objects
198-
for_each_connection(state, {'1:1', '1:1*'}, function(collection_name, c)
199+
for_each_connection(state, {'1:1'}, function(collection_name, c)
199200
-- XXX: support multihead connections
200201
if c.variants ~= nil then return end
201202

0 commit comments

Comments
 (0)