Skip to content

Commit 7b1fd02

Browse files
authored
Support partial results for select/pairs (#122)
User may need to select a subset of the query fields, but receives the values of all fields, which leads to unnecessary network load. Using `fields` parameters allows to reduce amount of data transferred from storage. Result contains only fields specified by `fields` parameter, but scan key and primary key values are merged to the result fields to support pagination.
1 parent 1f78ce1 commit 7b1fd02

File tree

12 files changed

+794
-12
lines changed

12 files changed

+794
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1919
### Added
2020

2121
* Support for UUID field types and UUID values
22-
* `fields` option for simple operations to get partial result
22+
* `fields` option for simple operations and select/pairs calls with pagination support to get partial result
2323

2424
## [0.4.0] - 2020-12-02
2525

crud/common/schema.lua

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ local function get_space_schema_hash(space)
135135
return digest.murmur(msgpack.encode(space_info))
136136
end
137137

138-
local function filter_result_fields(tuple, field_names)
138+
local function filter_tuple_fields(tuple, field_names)
139139
if field_names == nil or tuple == nil then
140140
return tuple
141141
end
@@ -154,6 +154,28 @@ local function filter_result_fields(tuple, field_names)
154154
return result
155155
end
156156

157+
function schema.filter_tuples_fields(tuples, field_names)
158+
dev_checks('?table', '?table')
159+
160+
if field_names == nil then
161+
return tuples
162+
end
163+
164+
local result = {}
165+
166+
for _, tuple in ipairs(tuples) do
167+
local filtered_tuple, err = filter_tuple_fields(tuple, field_names)
168+
169+
if err ~= nil then
170+
return nil, err
171+
end
172+
173+
table.insert(result, filtered_tuple)
174+
end
175+
176+
return result
177+
end
178+
157179
-- schema.wrap_box_space_func_result pcalls some box.space function
158180
-- and returns its result as a table
159181
-- `{res = ..., err = ..., space_schema_hash = ...}`
@@ -174,7 +196,7 @@ function schema.wrap_box_space_func_result(space, func_name, args, opts)
174196
result.space_schema_hash = get_space_schema_hash(space)
175197
end
176198
else
177-
result.res, err = filter_result_fields(func_res, opts.field_names)
199+
result.res, err = filter_tuple_fields(func_res, opts.field_names)
178200
if err ~= nil then
179201
return nil, err
180202
end

crud/common/utils.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,22 @@ local function filter_format_fields(space_format, field_names)
377377
return filtered_space_format
378378
end
379379

380+
function utils.get_fields_format(space_format, field_names)
381+
dev_checks('table', '?table')
382+
383+
if field_names == nil then
384+
return table.copy(space_format)
385+
end
386+
387+
local filtered_space_format, err = filter_format_fields(space_format, field_names)
388+
389+
if err ~= nil then
390+
return nil, err
391+
end
392+
393+
return filtered_space_format
394+
end
395+
380396
function utils.format_result(rows, space, field_names)
381397
local result = {}
382398
local err

crud/select.lua

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ local function select_on_storage(space_name, index_id, conditions, opts)
3333
iter = 'number',
3434
limit = 'number',
3535
scan_condition_num = '?number',
36+
field_names = '?table',
3637
})
3738

3839
local space = box.space[space_name]
@@ -64,7 +65,9 @@ local function select_on_storage(space_name, index_id, conditions, opts)
6465
return nil, SelectError:new("Failed to execute select: %s", err)
6566
end
6667

67-
return tuples
68+
-- getting tuples with user defined fields (if `fields` option is specified)
69+
-- and fields that are needed for comparison on router (primary key + scan key)
70+
return schema.filter_tuples_fields(tuples, opts.field_names)
6871
end
6972

7073
function select_module.init()
@@ -86,6 +89,7 @@ local function select_iteration(space_name, plan, opts)
8689
iter = plan.iter,
8790
limit = opts.limit,
8891
scan_condition_num = plan.scan_condition_num,
92+
field_names = plan.field_names,
8993
}
9094

9195
local storage_select_args = {
@@ -125,6 +129,7 @@ local function build_select_iterator(space_name, user_conditions, opts)
125129
timeout = '?number',
126130
batch_size = '?number',
127131
bucket_id = '?number|cdata',
132+
field_names = '?table',
128133
})
129134

130135
opts = opts or {}
@@ -157,6 +162,7 @@ local function build_select_iterator(space_name, user_conditions, opts)
157162
local plan, err = select_plan.new(space, conditions, {
158163
first = opts.first,
159164
after_tuple = opts.after,
165+
field_names = opts.field_names,
160166
})
161167

162168
if err ~= nil then
@@ -181,16 +187,27 @@ local function build_select_iterator(space_name, user_conditions, opts)
181187
local primary_index = space.index[0]
182188
local cmp_key_parts = utils.merge_primary_key_parts(scan_index.parts, primary_index.parts)
183189
local cmp_operator = select_comparators.get_cmp_operator(plan.iter)
190+
191+
-- generator of tuples comparator needs field_names and space_format
192+
-- to update fieldno in each part in cmp_key_parts because storage result contains
193+
-- fields in order specified by field_names
184194
local tuples_comparator, err = select_comparators.gen_tuples_comparator(
185-
cmp_operator, cmp_key_parts
195+
cmp_operator, cmp_key_parts, opts.field_names, space_format
186196
)
187197
if err ~= nil then
188198
return nil, SelectError:new("Failed to generate comparator function: %s", err)
189199
end
190200

201+
-- filter space format by plan.field_names (user defined fields + primary key + scan key)
202+
-- to pass it user as metadata
203+
local filtered_space_format, err = utils.get_fields_format(space_format, plan.field_names)
204+
if err ~= nil then
205+
return nil, err
206+
end
207+
191208
local iter = Iterator.new({
192209
space_name = space_name,
193-
space_format = space_format,
210+
space_format = filtered_space_format,
194211
iteration_func = select_iteration,
195212
comparator = tuples_comparator,
196213

@@ -213,6 +230,7 @@ function select_module.pairs(space_name, user_conditions, opts)
213230
batch_size = '?number',
214231
use_tomap = '?boolean',
215232
bucket_id = '?number|cdata',
233+
fields = '?table',
216234
})
217235

218236
opts = opts or {}
@@ -227,6 +245,7 @@ function select_module.pairs(space_name, user_conditions, opts)
227245
timeout = opts.timeout,
228246
batch_size = opts.batch_size,
229247
bucket_id = opts.bucket_id,
248+
field_names = opts.fields,
230249
}
231250

232251
local iter, err = schema.wrap_func_reload(
@@ -268,6 +287,7 @@ function select_module.call(space_name, user_conditions, opts)
268287
timeout = '?number',
269288
batch_size = '?number',
270289
bucket_id = '?number|cdata',
290+
fields = '?table',
271291
})
272292

273293
opts = opts or {}
@@ -284,6 +304,7 @@ function select_module.call(space_name, user_conditions, opts)
284304
timeout = opts.timeout,
285305
batch_size = opts.batch_size,
286306
bucket_id = opts.bucket_id,
307+
field_names = opts.fields,
287308
}
288309

289310
local iter, err = schema.wrap_func_reload(
@@ -313,7 +334,7 @@ function select_module.call(space_name, user_conditions, opts)
313334
end
314335

315336
return {
316-
metadata = table.copy(iter.space_format),
337+
metadata = iter.space_format,
317338
rows = tuples,
318339
}
319340
end

crud/select/comparators.lua

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,37 @@ local function gen_array_cmp_func(target, key_parts)
8383
end
8484
end
8585

86+
-- if `fields` option is specified then tuples to compare contains
87+
-- fields in order specified by field_names
88+
-- for comparison tuples we need to change fieldno in key_parts according to field_names
89+
local function update_key_parts_by_field_names(space_format, field_names, key_parts)
90+
if field_names == nil then
91+
return key_parts
92+
end
93+
94+
local fields_positions = {}
95+
local updated_key_parts = {}
96+
local last_position = #field_names + 1
97+
98+
for i, field_name in ipairs(field_names) do
99+
fields_positions[field_name] = i
100+
end
101+
102+
for _, part in ipairs(key_parts) do
103+
local field_name = space_format[part.fieldno].name
104+
if not fields_positions[field_name] then
105+
fields_positions[field_name] = last_position
106+
last_position = last_position + 1
107+
end
108+
local updated_part = {type = part.type,
109+
fieldno = fields_positions[field_name],
110+
is_nullable = part.is_nullable}
111+
table.insert(updated_key_parts, updated_part)
112+
end
113+
114+
return updated_key_parts
115+
end
116+
86117
local cmp_operators_by_tarantool_iter = {
87118
[box.index.GT] = operators.GT,
88119
[box.index.GE] = operators.GT,
@@ -127,15 +158,18 @@ function comparators.gen_func(cmp_operator, key_parts)
127158
return func
128159
end
129160

130-
function comparators.gen_tuples_comparator(cmp_operator, key_parts)
131-
local keys_comparator, err = comparators.gen_func(cmp_operator, key_parts)
161+
function comparators.gen_tuples_comparator(cmp_operator, key_parts, field_names, space_format)
162+
local updated_key_parts = update_key_parts_by_field_names(
163+
space_format, field_names, key_parts
164+
)
165+
local keys_comparator, err = comparators.gen_func(cmp_operator, updated_key_parts)
132166
if err ~= nil then
133167
return nil, ComparatorsError:new("Failed to generate comparator function: %s", err)
134168
end
135169

136170
return function(lhs, rhs)
137-
local lhs_key = utils.extract_key(lhs, key_parts)
138-
local rhs_key = utils.extract_key(rhs, key_parts)
171+
local lhs_key = utils.extract_key(lhs, updated_key_parts)
172+
local rhs_key = utils.extract_key(rhs, updated_key_parts)
139173

140174
return keys_comparator(lhs_key, rhs_key)
141175
end

crud/select/iterator.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function Iterator.new(opts)
2525
replicasets = 'table',
2626

2727
timeout = '?number',
28+
field_names = '?table',
2829
})
2930

3031
local iter = {
@@ -35,6 +36,7 @@ function Iterator.new(opts)
3536
plan = opts.plan,
3637

3738
timeout = opts.timeout,
39+
field_names = opts.field_names,
3840

3941
replicasets = table.copy(opts.replicasets),
4042
replicasets_count = utils.table_count(opts.replicasets),
@@ -99,6 +101,7 @@ local function update_replicasets_tuples(iter, after_tuple, replicaset_uuid)
99101
replicasets = replicasets,
100102
timeout = iter.timeout,
101103
limit = limit_per_storage_call,
104+
field_names = iter.field_names,
102105
})
103106
if err ~= nil then
104107
return false, UpdateTuplesError:new('Failed to select tuples from storages: %s', err)

crud/select/plan.lua

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ local select_plan = {}
99
local SelectPlanError = errors.new_class('SelectPlanError', {capture_stack = false})
1010
local IndexTypeError = errors.new_class('IndexTypeError', {capture_stack = false})
1111
local ValidateConditionsError = errors.new_class('ValidateConditionsError', {capture_stack = false})
12+
local FilterFieldsError = errors.new_class('FilterFieldsError', {capture_stack = false})
1213

1314
local function index_is_allowed(index)
1415
return index.type == 'TREE'
@@ -106,10 +107,70 @@ local function extract_sharding_key_from_scan_value(scan_value, scan_index, shar
106107
return sharding_key
107108
end
108109

110+
-- We need to construct after_tuple by field_names
111+
-- because if `fields` option is specified we have after_tuple with partial fields
112+
-- and these fields are ordered by field_names + primary key + scan key
113+
-- this order can be differ from order in space format
114+
-- so we need to cast after_tuple to space format for scrolling tuples on storage
115+
local function construct_after_tuple_by_fields(space_format, field_names, tuple)
116+
if tuple == nil then
117+
return nil
118+
end
119+
120+
if field_names == nil then
121+
return tuple
122+
end
123+
124+
local positions = {}
125+
local transformed_tuple = {}
126+
127+
for i, field in ipairs(space_format) do
128+
positions[field.name] = i
129+
end
130+
131+
for i, field_name in ipairs(field_names) do
132+
local fieldno = positions[field_name]
133+
if fieldno == nil then
134+
return nil, FilterFieldsError:new(
135+
'Space format doesn\'t contain field named %q', field_name
136+
)
137+
end
138+
139+
transformed_tuple[fieldno] = tuple[i]
140+
end
141+
142+
return transformed_tuple
143+
end
144+
145+
local function enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
146+
if field_names == nil then
147+
return nil
148+
end
149+
150+
local enriched_field_names = {}
151+
local key_field_names = {}
152+
153+
for _, field_name in ipairs(field_names) do
154+
table.insert(enriched_field_names, field_name)
155+
key_field_names[field_name] = true
156+
end
157+
158+
for _, part in ipairs(key_parts) do
159+
local field_name = space_format[part.fieldno].name
160+
if not key_field_names[field_name] then
161+
table.insert(enriched_field_names, field_name)
162+
key_field_names[field_name] = true
163+
end
164+
end
165+
166+
return enriched_field_names
167+
end
168+
109169
function select_plan.new(space, conditions, opts)
110170
dev_checks('table', '?table', {
111171
first = '?number',
112172
after_tuple = '?table',
173+
field_names = '?table',
113174
})
114175

115176
conditions = conditions ~= nil and conditions or {}
@@ -159,9 +220,17 @@ function select_plan.new(space, conditions, opts)
159220
scan_value = {}
160221
end
161222

223+
local cmp_key_parts = utils.merge_primary_key_parts(scan_index.parts, primary_index.parts)
224+
local field_names = enrich_field_names_with_cmp_key(opts.field_names, cmp_key_parts, space_format)
225+
162226
-- handle opts.first
163227
local total_tuples_count
164-
local scan_after_tuple = opts.after_tuple
228+
local scan_after_tuple, err = construct_after_tuple_by_fields(
229+
space_format, field_names, opts.after_tuple
230+
)
231+
if err ~= nil then
232+
return nil, err
233+
end
165234

166235
if opts.first ~= nil then
167236
total_tuples_count = math.abs(opts.first)
@@ -203,6 +272,7 @@ function select_plan.new(space, conditions, opts)
203272
iter = scan_iter,
204273
total_tuples_count = total_tuples_count,
205274
sharding_key = sharding_key,
275+
field_names = field_names,
206276
}
207277

208278
return plan

0 commit comments

Comments
 (0)