Skip to content

Commit dcb04ff

Browse files
authored
Reverse pagination (#41)
1 parent 5bbe1c0 commit dcb04ff

File tree

10 files changed

+361
-40
lines changed

10 files changed

+361
-40
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2323
### Changed
2424

2525
* `checks` is disabled for internal functions by default
26+
* `limit` option is renamed to `first`
27+
* Reverse pagination (negative `first`) is supported
2628

2729
## [0.1.0] - 2020-09-23
2830

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,9 @@ where:
222222
* `space_name` (`string`) - name of the space
223223
* `conditions` (`?table`) - array of [select conditions](#select-conditions)
224224
* `opts`:
225-
* `limit` (`?number`) - the maximum limit of the objects to return
225+
* `first` (`?number`) - the maximum count of the objects to return.
226+
If negative value is specified, the last objects are returned
227+
(`after` option is required in this case).
226228
* `after` (`?table`) - object after which objects should be selected
227229
* `batch_size` (`?number`) - number of tuples to process per one request to storage
228230
* `timeout` (`?number`) - `vshard.call` timeout (in seconds)
@@ -261,7 +263,8 @@ crud.select('customers', {{'<=', 'age', 35}})
261263
### Pairs
262264

263265
You can iterate across a distributed space using the `crud.pairs` function.
264-
Its arguments are the same as [`crud.select`](#select) arguments.
266+
Its arguments are the same as [`crud.select`](#select) arguments,
267+
but negative `first` values aren't allowed.
265268

266269
**Example:**
267270

crud/common/utils.lua

+22
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,26 @@ function utils.unflatten_rows(rows, metadata)
173173
return result
174174
end
175175

176+
local inverted_tarantool_iters = {
177+
[box.index.EQ] = box.index.REQ,
178+
[box.index.GT] = box.index.LT,
179+
[box.index.GE] = box.index.LE,
180+
[box.index.LT] = box.index.GT,
181+
[box.index.LE] = box.index.GE,
182+
[box.index.REQ] = box.index.EQ,
183+
}
184+
185+
function utils.invert_tarantool_iter(iter)
186+
local inverted_iter = inverted_tarantool_iters[iter]
187+
assert(inverted_iter ~= nil, "Unsupported Tarantool iterator: " .. tostring(iter))
188+
return inverted_iter
189+
end
190+
191+
function utils.reverse_inplace(t)
192+
for i = 1,#t - 1 do
193+
t[i], t[#t - i + 1] = t[#t - i + 1], t[i]
194+
end
195+
return t
196+
end
197+
176198
return utils

crud/select.lua

+28-18
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ end
119119
local function build_select_iterator(space_name, user_conditions, opts)
120120
dev_checks('string', '?table', {
121121
after = '?table',
122-
limit = '?number',
122+
first = '?number',
123123
timeout = '?number',
124124
batch_size = '?number',
125125
})
@@ -132,10 +132,6 @@ local function build_select_iterator(space_name, user_conditions, opts)
132132

133133
local batch_size = opts.batch_size or DEFAULT_BATCH_SIZE
134134

135-
if opts.limit ~= nil and opts.limit < 0 then
136-
return nil, SelectError:new("limit should be >= 0")
137-
end
138-
139135
-- check conditions
140136
local conditions, err = select_conditions.parse(user_conditions)
141137
if err ~= nil then
@@ -153,25 +149,26 @@ local function build_select_iterator(space_name, user_conditions, opts)
153149
end
154150
local space_format = space:format()
155151

152+
-- set after tuple
153+
local after_tuple = utils.flatten(opts.after, space_format)
154+
156155
-- plan select
157156
local plan, err = select_plan.new(space, conditions, {
158-
limit = opts.limit,
157+
first = opts.first,
158+
after_tuple = after_tuple,
159159
})
160160

161161
if err ~= nil then
162162
return nil, SelectError:new("Failed to plan select: %s", err)
163163
end
164164

165-
-- set limit and replicasets to select from
165+
-- set replicasets to select from
166166
local replicasets_to_select = replicasets
167167

168168
if plan.sharding_key ~= nil then
169169
replicasets_to_select = get_replicasets_by_sharding_key(plan.sharding_key)
170170
end
171171

172-
-- set after tuple
173-
local after_tuple = utils.flatten(opts.after, space_format)
174-
175172
-- generate tuples comparator
176173
local scan_index = space.index[plan.index_id]
177174
local primary_index = space.index[0]
@@ -191,7 +188,6 @@ local function build_select_iterator(space_name, user_conditions, opts)
191188
comparator = tuples_comparator,
192189

193190
plan = plan,
194-
after_tuple = after_tuple,
195191

196192
batch_size = batch_size,
197193
replicasets = replicasets_to_select,
@@ -205,16 +201,20 @@ end
205201
function select_module.pairs(space_name, user_conditions, opts)
206202
checks('string', '?table', {
207203
after = '?table',
208-
limit = '?number',
204+
first = '?number',
209205
timeout = '?number',
210206
batch_size = '?number',
211207
})
212208

213209
opts = opts or {}
214210

211+
if opts.first ~= nil and opts.first < 0 then
212+
error(string.format("Negative first isn't allowed for pairs"))
213+
end
214+
215215
local iter, err = build_select_iterator(space_name, user_conditions, {
216216
after = opts.after,
217-
limit = opts.limit,
217+
first = opts.first,
218218
timeout = opts.timeout,
219219
batch_size = opts.batch_size,
220220
})
@@ -247,16 +247,22 @@ end
247247
function select_module.call(space_name, user_conditions, opts)
248248
checks('string', '?table', {
249249
after = '?table',
250-
limit = '?number',
250+
first = '?number',
251251
timeout = '?number',
252252
batch_size = '?number',
253253
})
254254

255255
opts = opts or {}
256256

257+
if opts.first ~= nil and opts.first < 0 then
258+
if opts.after == nil then
259+
return nil, SelectError:new("Negative first should be specified only with after option")
260+
end
261+
end
262+
257263
local iter, err = build_select_iterator(space_name, user_conditions, {
258264
after = opts.after,
259-
limit = opts.limit,
265+
first = opts.first,
260266
timeout = opts.timeout,
261267
batch_size = opts.batch_size,
262268
})
@@ -268,16 +274,20 @@ function select_module.call(space_name, user_conditions, opts)
268274
local tuples = {}
269275

270276
while iter:has_next() do
271-
local obj, err = iter:get()
277+
local tuple, err = iter:get()
272278
if err ~= nil then
273279
return nil, SelectError:new("Failed to get next object: %s", err)
274280
end
275281

276-
if obj == nil then
282+
if tuple == nil then
277283
break
278284
end
279285

280-
table.insert(tuples, obj)
286+
table.insert(tuples, tuple)
287+
end
288+
289+
if opts.first ~= nil and opts.first < 0 then
290+
utils.reverse_inplace(tuples)
281291
end
282292

283293
return {

crud/select/comparators.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ local array_cmp_funcs_by_operators = {
177177
--]=]
178178
function comparators.get_cmp_operator(tarantool_iter)
179179
local cmp_operator = cmp_operators_by_tarantool_iter[tarantool_iter]
180-
assert(cmp_operator ~= nil, 'Unsupported Tarantool iterator %q', tarantool_iter)
180+
assert(cmp_operator ~= nil, 'Unsupported Tarantool iterator: ' .. tostring(tarantool_iter))
181181

182182
return cmp_operator
183183
end

crud/select/iterator.lua

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ function Iterator.new(opts)
2020
iteration_func = 'function',
2121

2222
plan = 'table',
23-
after_tuple = '?table',
2423

2524
batch_size = 'number',
2625
replicasets = 'table',
@@ -34,7 +33,7 @@ function Iterator.new(opts)
3433
iteration_func = opts.iteration_func,
3534

3635
plan = opts.plan,
37-
after_tuple = opts.after_tuple,
36+
3837
timeout = opts.timeout,
3938

4039
replicasets = table.copy(opts.replicasets),
@@ -56,7 +55,7 @@ function Iterator.new(opts)
5655

5756
setmetatable(iter, Iterator)
5857

59-
iter:_update_replicasets_tuples(iter.after_tuple)
58+
iter:_update_replicasets_tuples(iter.plan.after_tuple)
6059

6160
return iter
6261
end

crud/select/plan.lua

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local errors = require('errors')
22

33
local select_conditions = require('crud.select.conditions')
4+
local utils = require('crud.common.utils')
45
local dev_checks = require('crud.common.dev_checks')
56

67
local select_plan = {}
@@ -86,8 +87,10 @@ end
8687

8788
function select_plan.new(space, conditions, opts)
8889
dev_checks('table', '?table', {
89-
limit = '?number',
90+
first = '?number',
91+
after_tuple = '?table',
9092
})
93+
9194
conditions = conditions ~= nil and conditions or {}
9295
opts = opts or {}
9396

@@ -135,8 +138,26 @@ function select_plan.new(space, conditions, opts)
135138
scan_value = {}
136139
end
137140

138-
-- set total_tuples_count
139-
local total_tuples_count = opts.limit
141+
-- handle opts.first
142+
local total_tuples_count
143+
local scan_after_tuple = opts.after_tuple
144+
145+
if opts.first ~= nil then
146+
total_tuples_count = math.abs(opts.first)
147+
148+
if opts.first < 0 then
149+
scan_iter = utils.invert_tarantool_iter(scan_iter)
150+
151+
-- scan condition becomes border consition
152+
scan_condition_num = nil
153+
154+
if scan_after_tuple ~= nil then
155+
scan_value = utils.extract_key(scan_after_tuple, scan_index.parts)
156+
else
157+
scan_value = nil
158+
end
159+
end
160+
end
140161

141162
local sharding_index = primary_index -- XXX: only sharding by primary key is supported
142163

@@ -156,6 +177,7 @@ function select_plan.new(space, conditions, opts)
156177
space_name = space_name,
157178
index_id = scan_index.id,
158179
scan_value = scan_value,
180+
after_tuple = scan_after_tuple,
159181
scan_condition_num = scan_condition_num,
160182
iter = scan_iter,
161183
total_tuples_count = total_tuples_count,

test/integration/pairs_test.lua

+25
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,28 @@ add('test_le_condition_with_index', function(g)
296296
t.assert_equals(err, nil)
297297
t.assert_equals(objects, get_by_ids(customers, {1})) -- in age order
298298
end)
299+
300+
add('test_negative_first', function(g)
301+
local customers = insert_customers(g,{
302+
{
303+
id = 1, name = "Elizabeth", last_name = "Jackson",
304+
age = 12, city = "New York",
305+
}, {
306+
id = 2, name = "Mary", last_name = "Brown",
307+
age = 46, city = "Los Angeles",
308+
}, {
309+
id = 3, name = "David", last_name = "Smith",
310+
age = 33, city = "Los Angeles",
311+
},
312+
})
313+
314+
table.sort(customers, function(obj1, obj2) return obj1.id < obj2.id end)
315+
316+
-- negative first
317+
t.assert_error_msg_contains("Negative first isn't allowed for pairs", function()
318+
g.cluster.main_server.net_box:eval([[
319+
local crud = require('crud')
320+
crud.pairs('customers', nil, {first = -10})
321+
]])
322+
end)
323+
end)

0 commit comments

Comments
 (0)