Skip to content

Commit a15e774

Browse files
authored
Introduce crud.min and crud.max (#156)
1 parent 9caf4b3 commit a15e774

14 files changed

+648
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
3131
* Functions ``stop()`` for the roles ``crud-storage`` and ``crud-router``.
3232
* Option flag `force_map_call` for `select()`/`pairs()`
3333
to disable the `bucket_id` computation from primary key.
34+
* `crud.min` and `crud.max` functions to find the minimum and maximum values in the specified index.
3435

3536
## [0.6.0] - 2021-03-29
3637

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,32 @@ end
394394

395395
See more examples of pairs queries [here.](https://github.com/tarantool/crud/blob/master/doc/pairs.md)
396396

397+
### Min and max
398+
399+
```lua
400+
-- Find the minimum value in the specified index
401+
local result, err = crud.min(space_name, 'age', opts)
402+
---
403+
- metadata:
404+
- {'name': 'id', 'type': 'unsigned'}
405+
- {'name': 'bucket_id', 'type': 'unsigned'}
406+
- {'name': 'name', 'type': 'string'}
407+
- {'name': 'age', 'type': 'number'}
408+
rows:
409+
- [1, 477, 'Elizabeth', 12]
410+
411+
-- Find the maximum value in the specified index
412+
local result, err = crud.min(space_name, 'age', opts)
413+
---
414+
- metadata:
415+
- {'name': 'id', 'type': 'unsigned'}
416+
- {'name': 'bucket_id', 'type': 'unsigned'}
417+
- {'name': 'name', 'type': 'string'}
418+
- {'name': 'age', 'type': 'number'}
419+
rows:
420+
- [5, 1172, 'Jack', 35]
421+
```
422+
397423
### Cut extra rows
398424

399425
You could use `crud.cut_rows` function to cut off scan key and primary key values that were merged to the select/pairs partial result (select/pairs with `fields` option).

crud.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ local upsert = require('crud.upsert')
1010
local delete = require('crud.delete')
1111
local select = require('crud.select')
1212
local truncate = require('crud.truncate')
13+
local borders = require('crud.borders')
1314
local utils = require('crud.common.utils')
1415

1516
local crud = {}
@@ -69,6 +70,14 @@ crud.unflatten_rows = utils.unflatten_rows
6970
-- @function truncate
7071
crud.truncate = truncate.call
7172

73+
-- @refer borders.min
74+
-- @function min
75+
crud.min = borders.min
76+
77+
-- @refer borders.max
78+
-- @function max
79+
crud.max = borders.max
80+
7281
-- @refer utils.cut_rows
7382
-- @function cut_rows
7483
crud.cut_rows = utils.cut_rows
@@ -97,6 +106,7 @@ function crud.init_storage()
97106
delete.init()
98107
select.init()
99108
truncate.init()
109+
borders.init()
100110
end
101111

102112
function crud.init_router()

crud/borders.lua

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
local checks = require('checks')
2+
local errors = require('errors')
3+
local vshard = require('vshard')
4+
5+
local dev_checks = require('crud.common.dev_checks')
6+
local call = require('crud.common.call')
7+
local utils = require('crud.common.utils')
8+
local schema = require('crud.common.schema')
9+
local Keydef = require('crud.compare.keydef')
10+
11+
local BorderError = errors.new_class('Border', {capture_stack = false})
12+
13+
local borders = {}
14+
15+
local STAT_FUNC_NAME = '_crud.get_border_on_storage'
16+
17+
18+
local function get_border_on_storage(border_name, space_name, index_id, field_names)
19+
dev_checks('string', 'string', 'number', '?table')
20+
21+
assert(border_name == 'min' or border_name == 'max')
22+
23+
local space = box.space[space_name]
24+
if space == nil then
25+
return nil, BorderError:new("Space %q doesn't exist", space_name)
26+
end
27+
28+
local index = space.index[index_id]
29+
if index == nil then
30+
return nil, BorderError:new("Index %q of space doesn't exist", index_id, space_name)
31+
end
32+
33+
local function get_index_border(index)
34+
return index[border_name](index)
35+
end
36+
37+
return schema.wrap_func_result(space, get_index_border, {index}, {
38+
add_space_schema_hash = true,
39+
field_names = field_names,
40+
})
41+
end
42+
43+
function borders.init()
44+
_G._crud.get_border_on_storage = get_border_on_storage
45+
end
46+
47+
local function is_closer(compare_sign, keydef, tuple, res_tuple)
48+
if res_tuple == nil then
49+
return true
50+
end
51+
52+
local cmp = keydef:compare(tuple, res_tuple)
53+
54+
return cmp * compare_sign > 0
55+
end
56+
57+
local function call_get_border_on_router(border_name, space_name, index_name, opts)
58+
checks('string', 'string', '?string|number', {
59+
timeout = '?number',
60+
fields = '?table',
61+
})
62+
63+
opts = opts or {}
64+
65+
local space = utils.get_space(space_name, vshard.router.routeall())
66+
if space == nil then
67+
return nil, BorderError:new("Space %q doesn't exist", space_name), true
68+
end
69+
70+
local index
71+
if index_name == nil then
72+
index = space.index[0]
73+
else
74+
index = space.index[index_name]
75+
end
76+
77+
if index == nil then
78+
return nil, BorderError:new("Index %q of space %q doesn't exist", index_name, space_name), true
79+
end
80+
81+
local primary_index = space.index[0]
82+
83+
local cmp_key_parts = utils.merge_primary_key_parts(index.parts, primary_index.parts)
84+
local field_names = utils.enrich_field_names_with_cmp_key(opts.fields, cmp_key_parts, space:format())
85+
86+
local replicasets = vshard.router.routeall()
87+
local call_opts = {
88+
mode = 'read',
89+
replicasets = replicasets,
90+
timeout = opts.timeout,
91+
}
92+
local results, err = call.map(
93+
STAT_FUNC_NAME,
94+
{border_name, space_name, index.id, field_names},
95+
call_opts
96+
)
97+
98+
if err ~= nil then
99+
return nil, BorderError:new("Failed to get %s: %s", border_name, err)
100+
end
101+
102+
local keydef = Keydef.new(space, field_names, index.id)
103+
local compare_sign = border_name == 'max' and 1 or -1
104+
105+
local res_tuple = nil
106+
for _, storage_result in pairs(results) do
107+
local storage_result = storage_result[1]
108+
if storage_result.err ~= nil then
109+
local need_reload = schema.result_needs_reload(space, storage_result)
110+
return nil, BorderError:new("Failed to get %s: %s", border_name, storage_result.err), need_reload
111+
end
112+
113+
local tuple = storage_result.res
114+
if tuple ~= nil and is_closer(compare_sign, keydef, tuple, res_tuple) then
115+
res_tuple = tuple
116+
end
117+
end
118+
119+
local result = utils.format_result({res_tuple}, space, field_names)
120+
121+
if opts.fields ~= nil then
122+
result = utils.cut_rows(result.rows, result.metadata, opts.fields)
123+
end
124+
125+
return result
126+
end
127+
128+
local function get_border(border_name, space_name, index_name, opts)
129+
return schema.wrap_func_reload(
130+
call_get_border_on_router, border_name, space_name, index_name, opts
131+
)
132+
end
133+
134+
--- Find the minimum value in the specified index
135+
--
136+
-- @function min
137+
--
138+
-- @param string space_name
139+
-- A space name
140+
--
141+
-- @param ?string index_name
142+
-- An index name (by default, primary index is used)
143+
--
144+
-- @tparam ?number opts.timeout
145+
-- Function call timeout
146+
--
147+
-- @tparam ?table opts.fields
148+
-- Field names for getting only a subset of fields
149+
--
150+
-- @return[1] result
151+
-- @treturn[2] nil
152+
-- @treturn[2] table Error description
153+
function borders.min(space_name, index_id, opts)
154+
return get_border('min', space_name, index_id, opts)
155+
end
156+
157+
--- Find the maximum value in the specified index
158+
--
159+
-- @function min
160+
--
161+
-- @param string space_name
162+
-- A space name
163+
--
164+
-- @param ?string index_name
165+
-- An index name (by default, primary index is used)
166+
--
167+
-- @tparam ?number opts.timeout
168+
-- Function call timeout
169+
--
170+
-- @tparam ?table opts.fields
171+
-- Field names for getting only a subset of fields
172+
--
173+
-- @return[1] result
174+
-- @treturn[2] nil
175+
-- @treturn[2] table Error description
176+
function borders.max(space_name, index_id, opts)
177+
return get_border('max', space_name, index_id, opts)
178+
end
179+
180+
return borders

crud/common/schema.lua

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,19 +197,14 @@ function schema.truncate_row_trailing_fields(tuple, field_names)
197197
return tuple
198198
end
199199

200-
-- schema.wrap_box_space_func_result pcalls some box.space function
201-
-- and returns its result as a table
202-
-- `{res = ..., err = ..., space_schema_hash = ...}`
203-
-- space_schema_hash is computed if function failed and
204-
-- `add_space_schema_hash` is true
205-
function schema.wrap_box_space_func_result(space, func_name, args, opts)
206-
dev_checks('table', 'string', 'table', 'table')
200+
function schema.wrap_func_result(space, func, args, opts)
201+
dev_checks('table', 'function', 'table', 'table')
207202

208203
local result = {}
209204

210205
opts = opts or {}
211206

212-
local ok, func_res = pcall(space[func_name], space, unpack(args))
207+
local ok, func_res = pcall(func, unpack(args))
213208
if not ok then
214209
result.err = func_res
215210
if opts.add_space_schema_hash then
@@ -222,6 +217,20 @@ function schema.wrap_box_space_func_result(space, func_name, args, opts)
222217
return result
223218
end
224219

220+
-- schema.wrap_box_space_func_result pcalls some box.space function
221+
-- and returns its result as a table
222+
-- `{res = ..., err = ..., space_schema_hash = ...}`
223+
-- space_schema_hash is computed if function failed and
224+
-- `add_space_schema_hash` is true
225+
function schema.wrap_box_space_func_result(space, box_space_func_name, box_space_func_args, opts)
226+
dev_checks('table', 'string', 'table', 'table')
227+
local function func(space, box_space_func_name, box_space_func_args)
228+
return space[box_space_func_name](space, unpack(box_space_func_args))
229+
end
230+
231+
return schema.wrap_func_result(space, func, {space, box_space_func_name, box_space_func_args}, opts)
232+
end
233+
225234
-- schema.result_needs_reload checks that schema reload can
226235
-- be helpful to avoid storage error.
227236
-- It checks if space_schema_hash returned by storage

crud/common/utils.lua

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,30 @@ function utils.merge_primary_key_parts(key_parts, pk_parts)
166166
return merged_parts
167167
end
168168

169+
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
170+
if field_names == nil then
171+
return nil
172+
end
173+
174+
local enriched_field_names = {}
175+
local key_field_names = {}
176+
177+
for _, field_name in ipairs(field_names) do
178+
table.insert(enriched_field_names, field_name)
179+
key_field_names[field_name] = true
180+
end
181+
182+
for _, part in ipairs(key_parts) do
183+
local field_name = space_format[part.fieldno].name
184+
if not key_field_names[field_name] then
185+
table.insert(enriched_field_names, field_name)
186+
key_field_names[field_name] = true
187+
end
188+
end
189+
190+
return enriched_field_names
191+
end
192+
169193
local enabled_tarantool_features = {}
170194

171195
local function determine_enabled_features()

crud/compare/keydef.lua

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,10 @@ end
2828
local keydef_cache = {}
2929
setmetatable(keydef_cache, {__mode = 'k'})
3030

31-
local function new(replicasets, space_name, field_names, index_name)
31+
local function new(space, field_names, index_id)
3232
-- Get requested and primary index metainfo.
33-
local conn = select(2, next(replicasets)).master.conn
34-
local space = conn.space[space_name]
35-
local index = space.index[index_name]
36-
local key = msgpack.encode({index_name, field_names})
33+
local index = space.index[index_id]
34+
local key = msgpack.encode({index_id, field_names})
3735

3836
if keydef_cache[key] ~= nil then
3937
return keydef_cache[key]

crud/select/compat/select.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ local function build_select_iterator(space_name, user_conditions, opts)
101101
field_names = plan.field_names,
102102
}
103103

104-
local merger = Merger.new(replicasets_to_select, space_name, plan.index_id,
104+
local merger = Merger.new(replicasets_to_select, space, plan.index_id,
105105
common.SELECT_FUNC_NAME,
106106
{space_name, plan.index_id, plan.conditions, select_opts},
107107
{tarantool_iter = plan.tarantool_iter, field_names = plan.field_names, call_opts = opts.call_opts}

crud/select/merger.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ local reverse_tarantool_iters = {
133133
[box.index.REQ] = true,
134134
}
135135

136-
local function new(replicasets, space_name, index_id, func_name, func_args, opts)
136+
local function new(replicasets, space, index_id, func_name, func_args, opts)
137137
opts = opts or {}
138138
local call_opts = opts.call_opts
139139

@@ -164,7 +164,7 @@ local function new(replicasets, space_name, index_id, func_name, func_args, opts
164164
table.insert(merger_sources, source)
165165
end
166166

167-
local keydef = Keydef.new(replicasets, space_name, opts.field_names, index_id)
167+
local keydef = Keydef.new(space, opts.field_names, index_id)
168168
local merger = merger_lib.new(keydef, merger_sources, {
169169
reverse = reverse_tarantool_iters[opts.tarantool_iter],
170170
})

0 commit comments

Comments
 (0)