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

Commit 8ce3276

Browse files
committed
Support lookup by a partial set of index parts
* Fixes #30. * Fixes #38.
1 parent 109cb5d commit 8ce3276

File tree

5 files changed

+242
-44
lines changed

5 files changed

+242
-44
lines changed

graphql/accessor_general.lua

Lines changed: 101 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,42 @@ local function filter_names_fingerprint(filter)
121121
return name_list_str
122122
end
123123

124+
--- Get an index using parts tree built by @{build_index_parts_tree}.
125+
---
126+
--- @tparam table node root of the prefix tree for certain collection
127+
---
128+
--- @tparam table filter map of key-value to filter objects against
129+
---
130+
--- @treturn string `index_name` or `nil` is found index
131+
---
132+
--- @treturn number `len` is a number of index parts will be used at lookup
133+
local function get_best_matched_index(node, filter)
134+
local index_name = (node.index_names or {})[1]
135+
local max_len = 0
136+
137+
for k, v in pairs(filter) do
138+
local successor_node = (node.successors or {})[k]
139+
if successor_node ~= nil then
140+
local new_filter = table.copy(filter)
141+
new_filter[k] = nil
142+
local branch_index_name, branch_len =
143+
get_best_matched_index(successor_node, new_filter)
144+
if branch_index_name ~= nil and branch_len > max_len then
145+
index_name = branch_index_name
146+
max_len = branch_len
147+
end
148+
end
149+
end
150+
151+
return index_name, max_len + 1
152+
end
153+
124154
-- XXX: raw idea: we can store field-to-field_no mapping when creating
125155
-- `lookup_index_name` to faster form the value_list
126156

127157
--- Flatten filter values (transform to a list) against specific index to
128158
--- passing it to index:pairs().
129159
---
130-
--- Only full keys are supported for a compound index for now.
131-
---
132160
--- @tparam table self the data accessor
133161
---
134162
--- @tparam table filter filter for objects, its values will ordered to form
@@ -144,6 +172,9 @@ end
144172
--- passed index
145173
---
146174
--- @treturn table `value_list` the value to pass to index:pairs()
175+
---
176+
--- @treturn table `new_filter` the `filter` value w/o values extracted to
177+
--- `value_list`
147178
local function flatten_filter(self, filter, collection_name, index_name)
148179
assert(type(self) == 'table',
149180
'self must be a table, got ' .. type(self))
@@ -153,6 +184,7 @@ local function flatten_filter(self, filter, collection_name, index_name)
153184
'index_name must be a string, got ' .. type(index_name))
154185

155186
local value_list = {}
187+
local new_filter = table.copy(filter)
156188

157189
-- fill value_list
158190
local index_meta = self.indexes[collection_name][index_name]
@@ -163,6 +195,7 @@ local function flatten_filter(self, filter, collection_name, index_name)
163195
local value = filter[field_name]
164196
if value == nil then break end
165197
value_list[#value_list + 1] = value
198+
new_filter[field_name] = nil
166199
end
167200

168201
-- check for correctness: non-empty value_list
@@ -172,26 +205,11 @@ local function flatten_filter(self, filter, collection_name, index_name)
172205
json.encode(filter), index_name))
173206
end
174207

175-
-- check for correctness: all filter fields are used
176-
local count = 0
177-
for k, v in pairs(filter) do
178-
count = count + 1
179-
end
180-
if count ~= #value_list then -- avoid extra json.encode()
181-
assert(count ~= #value_list,
182-
('filter items count does not match index fields count: ' ..
183-
'filter: %s, index_name: %s'):format(json.encode(filter),
184-
index_name))
185-
end
186-
187-
local full_match = #value_list == #index_meta.fields
188-
return full_match, value_list
208+
local full_match = #value_list == #index_meta.fields and
209+
next(new_filter) == nil
210+
return full_match, value_list, new_filter
189211
end
190212

191-
-- XXX: support partial match for primary/secondary indexes and support to skip
192-
-- fields to get an index (full_match must be false in the case because
193-
-- returned items will be additionally filtered after unflatten).
194-
195213
--- Choose an index for lookup tuple(s) by a 'filter'. The filter holds fields
196214
--- values of object(s) we want to find. It uses prebuilt `lookup_index_name`
197215
--- table representing available indexes, which created by the
@@ -230,9 +248,12 @@ end
230248
---
231249
--- @treturn string `index_name` is name of the found index or nil
232250
---
233-
--- @treturn table `value_list` is values list from the `filter` argument
234-
--- ordered in the such way that can be passed to the found index (has some
235-
--- meaning only when `index_name ~= nil`)
251+
--- @treturn table `new_filter` is the filter value w/o values extracted into
252+
--- `value_list`
253+
---
254+
--- @treturn table `value_list` (optional) is values list from the `filter`
255+
--- argument ordered in the such way that can be passed to the found index (has
256+
--- some meaning only when `index_name ~= nil`)
236257
---
237258
--- @treturn table `pivot` (optional) an offset argument represented depending
238259
--- of a case: whether we'll lookup for the offset by an index; it is either
@@ -259,6 +280,10 @@ local get_index_name = function(self, collection_name, from, filter, args)
259280
assert(type(lookup_index_name) == 'table',
260281
'lookup_index_name must be a table, got ' .. type(lookup_index_name))
261282

283+
local parts_tree = index_cache.parts_tree
284+
assert(type(parts_tree) == 'table',
285+
'parts_tree must be a table, got ' .. type(parts_tree))
286+
262287
local connection_indexes = index_cache.connection_indexes
263288
assert(type(connection_indexes) == 'table',
264289
'connection_indexes must be a table, got ' .. type(connection_indexes))
@@ -276,6 +301,7 @@ local get_index_name = function(self, collection_name, from, filter, args)
276301
assert(connection_type ~= nil, 'connection_type must not be nil')
277302
local full_match = connection_type == '1:1' and next(filter) == nil
278303
local value_list = from.destination_args_values
304+
local new_filter = filter
279305

280306
local pivot
281307
if args.offset ~= nil then
@@ -296,21 +322,22 @@ local get_index_name = function(self, collection_name, from, filter, args)
296322
pivot = {filter = pivot_filter}
297323
end
298324

299-
return full_match, index_name, value_list, pivot
325+
return full_match, index_name, new_filter, value_list, pivot
300326
end
301327

302328
-- The 'fast offset' case. Here we fetch top-level objects starting from
303329
-- passed offset. Select will be performed by the primary index and
304330
-- corresponding offset in `pivot.value_list`, then the result will be
305-
-- postprocessed using `filter`, if necessary.
331+
-- postprocessed using `new_filter`, if necessary.
306332
if args.offset ~= nil then
307333
local index_name, index_meta = get_primary_index_meta(self,
308334
collection_name)
309335
local full_match
310336
local pivot_value_list
337+
local new_filter = filter
311338
if type(args.offset) == 'table' then
312-
full_match, pivot_value_list = flatten_filter(self, args.offset,
313-
collection_name, index_name)
339+
full_match, pivot_value_list, new_filter = flatten_filter(self,
340+
args.offset, collection_name, index_name)
314341
assert(full_match == true, 'offset by a partial key is forbidden')
315342
else
316343
assert(#index_meta.fields == 1,
@@ -320,22 +347,34 @@ local get_index_name = function(self, collection_name, from, filter, args)
320347
end
321348
local pivot = {value_list = pivot_value_list}
322349
full_match = full_match and next(filter) == nil
323-
return full_match, index_name, filter, pivot
350+
return full_match, index_name, new_filter, nil, pivot
324351
end
325352

326353
-- The 'no offset' case. Here we fetch top-level object either by found
327354
-- index or using full scan (if the index was not found).
355+
356+
-- try to find full index
328357
local name_list_str = filter_names_fingerprint(filter)
329358
assert(lookup_index_name[collection_name] ~= nil,
330359
('cannot find any index for collection "%s"'):format(collection_name))
331360
local index_name = lookup_index_name[collection_name][name_list_str]
332361
local full_match = false
333362
local value_list = nil
363+
local new_filter = filter
364+
365+
-- try to find partial index
366+
if index_name == nil then
367+
local root = parts_tree[collection_name]
368+
index_name = get_best_matched_index(root, filter)
369+
end
370+
371+
-- fill full_match and value_list appropriatelly
334372
if index_name ~= nil then
335-
full_match, value_list = flatten_filter(self, filter, collection_name,
336-
index_name)
373+
full_match, value_list, new_filter = flatten_filter(self, filter,
374+
collection_name, index_name)
337375
end
338-
return full_match, index_name, value_list
376+
377+
return full_match, index_name, new_filter, value_list
339378
end
340379

341380
--- Build `lookup_index_name` table (part of `index_cache`) to use in the
@@ -402,6 +441,34 @@ local function build_lookup_index_name(indexes)
402441
return lookup_index_name
403442
end
404443

444+
--- Build `parts_tree` to use in @{get_index_name} for lookup best matching
445+
--- index.
446+
---
447+
--- @tparam table indexes indexes metainformation as defined in the @{new}
448+
--- function
449+
---
450+
--- @treturn table `roots` resulting parts prefix tree
451+
local function build_index_parts_tree(indexes)
452+
local roots = {}
453+
454+
for collection_name, indexes_meta in pairs(indexes) do
455+
local root = {}
456+
roots[collection_name] = root
457+
for index_name, index_meta in pairs(indexes_meta) do
458+
local cur = root
459+
for _, field in ipairs(index_meta.fields) do
460+
cur.successors = cur.successors or {}
461+
cur.successors[field] = cur.successors[field] or {}
462+
cur = cur.successors[field]
463+
cur.index_names = cur.index_names or {}
464+
cur.index_names[#cur.index_names + 1] = index_name
465+
end
466+
end
467+
end
468+
469+
return roots
470+
end
471+
405472
--- Build `connection_indexes` table (part of `index_cache`) to use in the
406473
--- @{get_index_name} function.
407474
---
@@ -491,6 +558,7 @@ end
491558
local function build_index_cache(indexes, collections)
492559
return {
493560
lookup_index_name = build_lookup_index_name(indexes),
561+
parts_tree = build_index_parts_tree(indexes),
494562
connection_indexes = build_connection_indexes(indexes, collections),
495563
}
496564
end
@@ -682,8 +750,8 @@ local function select_internal(self, collection_name, from, filter, args, extra)
682750
('cannot find collection "%s"'):format(collection_name))
683751

684752
-- search for suitable index
685-
local full_match, index_name, index_value, pivot = get_index_name(
686-
self, collection_name, from, filter, args)
753+
local full_match, index_name, filter, index_value, pivot = get_index_name(
754+
self, collection_name, from, filter, args) -- we redefine filter here
687755
local index = index_name ~= nil and
688756
self.funcs.get_index(collection_name, index_name) or nil
689757
if from ~= nil then

test/local/space_compound_index.result

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ user_collection:
77
user_num: 12
88
...
99

10+
RESULT
11+
---
12+
user_collection: []
13+
...
14+
1015
RESULT
1116
---
1217
user_collection:
@@ -117,6 +122,16 @@ user_collection:
117122
user_num: 20
118123
...
119124

125+
RESULT
126+
---
127+
user_collection: []
128+
...
129+
130+
RESULT
131+
---
132+
user_collection: []
133+
...
134+
120135
RESULT
121136
---
122137
user_collection:
@@ -157,6 +172,16 @@ user_collection:
157172
last_name: last name b
158173
...
159174

175+
RESULT
176+
---
177+
user_collection:
178+
- order_connection: []
179+
user_str: user_str_b
180+
first_name: first name b
181+
user_num: 12
182+
last_name: last name b
183+
...
184+
160185
RESULT
161186
---
162187
user_collection:

test/shard_no_redundancy/shard_compound_index.result

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ testdata.run_queries(gql_wrapper)
123123
user_num: 12
124124
...
125125

126+
RESULT
127+
---
128+
user_collection: []
129+
...
130+
126131
RESULT
127132
---
128133
user_collection:
@@ -233,6 +238,16 @@ testdata.run_queries(gql_wrapper)
233238
user_num: 20
234239
...
235240

241+
RESULT
242+
---
243+
user_collection: []
244+
...
245+
246+
RESULT
247+
---
248+
user_collection: []
249+
...
250+
236251
RESULT
237252
---
238253
user_collection:
@@ -273,6 +288,16 @@ testdata.run_queries(gql_wrapper)
273288
last_name: last name b
274289
...
275290

291+
RESULT
292+
---
293+
user_collection:
294+
- order_connection: []
295+
user_str: user_str_b
296+
first_name: first name b
297+
user_num: 12
298+
last_name: last name b
299+
...
300+
276301
RESULT
277302
---
278303
user_collection:

test/shard_redundancy/shard_compound_index.result

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ testdata.run_queries(gql_wrapper)
141141
user_num: 12
142142
...
143143

144+
RESULT
145+
---
146+
user_collection: []
147+
...
148+
144149
RESULT
145150
---
146151
user_collection:
@@ -251,6 +256,16 @@ testdata.run_queries(gql_wrapper)
251256
user_num: 20
252257
...
253258

259+
RESULT
260+
---
261+
user_collection: []
262+
...
263+
264+
RESULT
265+
---
266+
user_collection: []
267+
...
268+
254269
RESULT
255270
---
256271
user_collection:
@@ -291,6 +306,16 @@ testdata.run_queries(gql_wrapper)
291306
last_name: last name b
292307
...
293308

309+
RESULT
310+
---
311+
user_collection:
312+
- order_connection: []
313+
user_str: user_str_b
314+
first_name: first name b
315+
user_num: 12
316+
last_name: last name b
317+
...
318+
294319
RESULT
295320
---
296321
user_collection:

0 commit comments

Comments
 (0)