From 81123dcbcce5c9cf6e400c50c590c69bece77e90 Mon Sep 17 00:00:00 2001
From: Roman Tsisyk <roman@tsisyk.com>
Date: Mon, 8 Jun 2015 16:56:00 +0300
Subject: [PATCH] Add Lua/C version of index:select{} for Sophia

A part of #863
---
 src/box/lua/call.cc      |  49 ++++++++++++++----
 src/box/lua/schema.lua   |  31 +++++++++--
 test/box/select.result   |  99 ++++++++++++++++++++---------------
 test/box/select.test.lua | 108 ++++++++++++++++++++++-----------------
 4 files changed, 185 insertions(+), 102 deletions(-)

diff --git a/src/box/lua/call.cc b/src/box/lua/call.cc
index 846a7fa3d4..46b7e85bc8 100644
--- a/src/box/lua/call.cc
+++ b/src/box/lua/call.cc
@@ -83,6 +83,7 @@ struct port_lua
 {
 	struct port_vtab *vtab;
 	struct lua_State *L;
+	size_t size; /* for port_lua_add_tuple */
 };
 
 static inline struct port_lua *
@@ -122,30 +123,26 @@ port_lua_table_add_tuple(struct port *port, struct tuple *tuple)
 {
 	lua_State *L = port_lua(port)->L;
 	try {
-		int idx = luaL_getn(L, -1);	/* TODO: can be optimized */
 		lbox_pushtuple(L, tuple);
-		lua_rawseti(L, -2, idx + 1);
-
+		lua_rawseti(L, -2, ++port_lua(port)->size);
 	} catch (...) {
 		tnt_raise(ClientError, ER_PROC_LUA, lua_tostring(L, -1));
 	}
 }
 
 /** Add all tuples to a Lua table. */
-static struct port *
-port_lua_table_create(struct lua_State *L)
+void
+port_lua_table_create(struct port_lua *port, struct lua_State *L)
 {
 	static struct port_vtab port_lua_vtab = {
 		port_lua_table_add_tuple,
 		null_port_eof,
 	};
-	struct port_lua *port = (struct port_lua *)
-			region_alloc(&fiber()->gc, sizeof(struct port_lua));
 	port->vtab = &port_lua_vtab;
 	port->L = L;
+	port->size = 0;
 	/* The destination table to append tuples to. */
 	lua_newtable(L);
-	return (struct port *) port;
 }
 
 /* }}} */
@@ -179,12 +176,12 @@ lbox_process(lua_State *L)
 		return luaL_error(L, "box.process(CALL, ...) is not allowed");
 	}
 	/* Capture all output into a Lua table. */
-	struct port *port_lua = port_lua_table_create(L);
+	struct port_lua port_lua;
 	struct request request;
 	request_create(&request, op);
 	request_decode(&request, req, sz);
-	box_process(&request, port_lua);
-
+	port_lua_table_create(&port_lua, L);
+	box_process(&request, (struct port *) &port_lua);
 	return 1;
 }
 
@@ -291,6 +288,27 @@ boxffi_select(struct port_ffi *port, uint32_t space_id, uint32_t index_id,
 	}
 }
 
+static int
+lbox_select(lua_State *L)
+{
+	if (lua_gettop(L) != 6 || !lua_isnumber(L, 1) || !lua_isnumber(L, 2) ||
+		!lua_isnumber(L, 3) || !lua_isnumber(L, 4) || !lua_isnumber(L, 5)) {
+		return luaL_error(L, "Usage index:select(space_id, index_id,"
+			"iterator, offset, limit, key)");
+	}
+
+	struct request request;
+	struct port_lua port;
+	lbox_request_create(&request, L, IPROTO_SELECT, 6, -1);
+	request.index_id = lua_tointeger(L, 2);
+	request.iterator = lua_tointeger(L, 3);
+	request.offset = lua_tointeger(L, 4);
+	request.limit = lua_tointeger(L, 5);
+	port_lua_table_create(&port, L);
+	box_process(&request, (struct port *) &port);
+	return 1;
+}
+
 static int
 lbox_insert(lua_State *L)
 {
@@ -731,6 +749,7 @@ static const struct luaL_reg boxlib[] = {
 static const struct luaL_reg boxlib_internal[] = {
 	{"process", lbox_process},
 	{"call_loadproc",  lbox_call_loadproc},
+	{"select", lbox_select},
 	{"insert", lbox_insert},
 	{"replace", lbox_replace},
 	{"update", lbox_update},
@@ -747,6 +766,14 @@ box_lua_init(struct lua_State *L)
 	luaL_register(L, "box.internal", boxlib_internal);
 	lua_pop(L, 1);
 
+#if 0
+	/* Get CTypeID for `struct port *' */
+	int rc = luaL_cdef(L, "struct port;");
+	assert(rc == 0);
+	(void) rc;
+	CTID_STRUCT_PORT_PTR = luaL_ctypeid(L, "struct port *");
+	assert(CTID_CONST_STRUCT_TUPLE_REF != 0);
+#endif
 	box_lua_error_init(L);
 	box_lua_tuple_init(L);
 	box_lua_index_init(L);
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 5461756fb9..f1e1c364e1 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -683,13 +683,10 @@ function box.schema.space.bless(space)
         end
     end
 
-    index_mt.select = function(index, key, opts)
+    local function check_select_opts(opts, key_is_nil)
         local offset = 0
         local limit = 4294967295
-
-        local key, key_end = msgpackffi.encode_tuple(key)
-        local iterator = check_iterator_type(opts, key + 1 >= key_end)
-
+        local iterator = check_iterator_type(opts, key_is_nil)
         if opts ~= nil then
             if opts.offset ~= nil then
                 offset = opts.offset
@@ -698,6 +695,12 @@ function box.schema.space.bless(space)
                 limit = opts.limit
             end
         end
+        return iterator, offset, limit
+    end
+
+    index_mt.select_ffi = function(index, key, opts)
+        local key, key_end = msgpackffi.encode_tuple(key)
+        local iterator, offset, limit = check_select_opts(opts, key + 1 >= key_end)
 
         if builtin.boxffi_select(port, index.space_id,
             index.id, iterator, offset, limit, key, key_end) ~=0 then
@@ -711,6 +714,14 @@ function box.schema.space.bless(space)
         end
         return ret
     end
+
+    index_mt.select_luac = function(index, key, opts)
+        local key = keify(key)
+        local iterator, offset, limit = check_select_opts(opts, #key == 0)
+        return internal.select(index.space_id, index.id, iterator,
+            offset, limit, key)
+    end
+
     index_mt.update = function(index, key, ops)
         return internal.update(index.space_id, index.id, keify(key), ops);
     end
@@ -729,6 +740,16 @@ function box.schema.space.bless(space)
         end
         return box.schema.index.alter(index.space_id, index.id, options)
     end
+
+    -- true if reading operations may yield
+    local read_yields = space.engine == 'sophia'
+    if read_yields then
+        -- use Lua/C implmenetation
+        index_mt.select = index_mt.select_luac
+    else
+        -- use FFI implementation
+        index_mt.select = index_mt.select_ffi
+    end
     --
     local space_mt = {}
     space_mt.len = function(space)
diff --git a/test/box/select.result b/test/box/select.result
index cc4522a46f..5266bfcfee 100644
--- a/test/box/select.result
+++ b/test/box/select.result
@@ -1,3 +1,6 @@
+msgpack = require('msgpack')
+---
+...
 s = box.schema.space.create('select', { temporary = true })
 ---
 ...
@@ -66,7 +69,23 @@ s.index[1]:get({1, 2})
 --------------------------------------------------------------------------------
 -- select tests
 --------------------------------------------------------------------------------
-s.index[0]:select()
+--# setopt delimiter ';'
+function test(idx, ...)
+    local t1 = idx:select_ffi(...)
+    local t2 = idx:select_luac(...)
+    if msgpack.encode(t1) ~= msgpack.encode(t2) then
+        return 'different result from select_ffi and select_luac', t1, t2
+    end
+    return t1
+end;
+---
+...
+--# setopt delimiter ''
+s.index[0].select == s.index[0].select_ffi or s.index[0].select == s.index[0].select_luac
+---
+- true
+...
+test(s.index[0])
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -89,7 +108,7 @@ s.index[0]:select()
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({})
+test(s.index[0], {})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -112,7 +131,7 @@ s.index[0]:select({})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(nil)
+test(s.index[0], nil)
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -135,7 +154,7 @@ s.index[0]:select(nil)
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = 'ALL'})
+test(s.index[0], {}, {iterator = 'ALL'})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -158,7 +177,7 @@ s.index[0]:select({}, {iterator = 'ALL'})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = box.index.ALL })
+test(s.index[0], nil, {iterator = box.index.ALL })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -181,7 +200,7 @@ s.index[0]:select(nil, {iterator = box.index.ALL })
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = box.index.ALL, limit = 10})
+test(s.index[0], {}, {iterator = box.index.ALL, limit = 10})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -194,15 +213,15 @@ s.index[0]:select({}, {iterator = box.index.ALL, limit = 10})
   - [9, 1, 2, 3]
   - [10, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = box.index.ALL, limit = 0})
+test(s.index[0], nil, {iterator = box.index.ALL, limit = 0})
 ---
 - []
 ...
-s.index[0]:select({}, {iterator = 'ALL', limit = 1, offset = 15})
+test(s.index[0], {}, {iterator = 'ALL', limit = 1, offset = 15})
 ---
 - - [16, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = 'ALL', limit = 20, offset = 15})
+test(s.index[0], nil, {iterator = 'ALL', limit = 20, offset = 15})
 ---
 - - [16, 1, 2, 3]
   - [17, 1, 2, 3]
@@ -210,7 +229,7 @@ s.index[0]:select(nil, {iterator = 'ALL', limit = 20, offset = 15})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = box.index.EQ})
+test(s.index[0], nil, {iterator = box.index.EQ})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -233,7 +252,7 @@ s.index[0]:select(nil, {iterator = box.index.EQ})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = 'EQ'})
+test(s.index[0], {}, {iterator = 'EQ'})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -256,7 +275,7 @@ s.index[0]:select({}, {iterator = 'EQ'})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = 'REQ'})
+test(s.index[0], nil, {iterator = 'REQ'})
 ---
 - - [20, 1, 2, 3]
   - [19, 1, 2, 3]
@@ -279,7 +298,7 @@ s.index[0]:select(nil, {iterator = 'REQ'})
   - [2, 1, 2, 3]
   - [1, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = box.index.REQ})
+test(s.index[0], {}, {iterator = box.index.REQ})
 ---
 - - [20, 1, 2, 3]
   - [19, 1, 2, 3]
@@ -302,45 +321,45 @@ s.index[0]:select({}, {iterator = box.index.REQ})
   - [2, 1, 2, 3]
   - [1, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = 'EQ', limit = 2, offset = 1})
+test(s.index[0], nil, {iterator = 'EQ', limit = 2, offset = 1})
 ---
 - - [2, 1, 2, 3]
   - [3, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = box.index.REQ, limit = 2, offset = 1})
+test(s.index[0], {}, {iterator = box.index.REQ, limit = 2, offset = 1})
 ---
 - - [19, 1, 2, 3]
   - [18, 1, 2, 3]
 ...
-s.index[0]:select(1)
+test(s.index[0], 1)
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select({1})
+test(s.index[0], {1})
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select({1, 2})
+test(s.index[0], {1, 2})
 ---
 - error: Invalid key part count (expected [0..1], got 2)
 ...
-s.index[0]:select(0)
+test(s.index[0], 0)
 ---
 - []
 ...
-s.index[0]:select({0})
+test(s.index[0], {0})
 ---
 - []
 ...
-s.index[0]:select("0")
+test(s.index[0], "0")
 ---
 - error: 'Supplied key type of part 0 does not match index part type: expected NUM'
 ...
-s.index[0]:select({"0"})
+test(s.index[0], {"0"})
 ---
 - error: 'Supplied key type of part 0 does not match index part type: expected NUM'
 ...
-s.index[1]:select(1)
+test(s.index[1], 1)
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -363,7 +382,7 @@ s.index[1]:select(1)
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[1]:select({1})
+test(s.index[1], {1})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -386,12 +405,12 @@ s.index[1]:select({1})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[1]:select({1}, {limit = 2})
+test(s.index[1], {1}, {limit = 2})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
 ...
-s.index[1]:select(1, {iterator = 'EQ'})
+test(s.index[1], 1, {iterator = 'EQ'})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -414,29 +433,29 @@ s.index[1]:select(1, {iterator = 'EQ'})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[1]:select({1}, {iterator = box.index.EQ, offset = 16, limit = 2})
+test(s.index[1], {1}, {iterator = box.index.EQ, offset = 16, limit = 2})
 ---
 - - [17, 1, 2, 3]
   - [18, 1, 2, 3]
 ...
-s.index[1]:select({1}, {iterator = box.index.REQ, offset = 16, limit = 2 })
+test(s.index[1], {1}, {iterator = box.index.REQ, offset = 16, limit = 2 })
 ---
 - - [4, 1, 2, 3]
   - [3, 1, 2, 3]
 ...
-s.index[1]:select({1, 2}, {iterator = 'EQ'})
+test(s.index[1], {1, 2}, {iterator = 'EQ'})
 ---
 - - [2, 1, 2, 3]
 ...
-s.index[1]:select({1, 2}, {iterator = box.index.REQ})
+test(s.index[1], {1, 2}, {iterator = box.index.REQ})
 ---
 - - [2, 1, 2, 3]
 ...
-s.index[1]:select({1, 2})
+test(s.index[1], {1, 2})
 ---
 - - [2, 1, 2, 3]
 ...
-s.index[0]:select(nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
+test(s.index[0], nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -459,7 +478,7 @@ s.index[0]:select(nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
+test(s.index[0], {}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -482,19 +501,19 @@ s.index[0]:select({}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(1)
+test(s.index[0], 1)
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = box.index.EQ })
+test(s.index[0], 1, { iterator = box.index.EQ })
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'EQ' })
+test(s.index[0], 1, { iterator = 'EQ' })
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'GE' })
+test(s.index[0], 1, { iterator = 'GE' })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -517,16 +536,16 @@ s.index[0]:select(1, { iterator = 'GE' })
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'GE', limit = 2 })
+test(s.index[0], 1, { iterator = 'GE', limit = 2 })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'LE', limit = 2 })
+test(s.index[0], 1, { iterator = 'LE', limit = 2 })
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'GE', offset = 10, limit = 2 })
+test(s.index[0], 1, { iterator = 'GE', offset = 10, limit = 2 })
 ---
 - - [11, 1, 2, 3]
   - [12, 1, 2, 3]
diff --git a/test/box/select.test.lua b/test/box/select.test.lua
index 2ac00390ff..cfde112f12 100644
--- a/test/box/select.test.lua
+++ b/test/box/select.test.lua
@@ -1,3 +1,5 @@
+msgpack = require('msgpack')
+
 s = box.schema.space.create('select', { temporary = true })
 index1 = s:create_index('primary', { type = 'tree' })
 index2 = s:create_index('second', { type = 'tree', unique = true,  parts = {2, 'num', 1, 'num'}})
@@ -26,52 +28,66 @@ s.index[1]:get({1, 2})
 -- select tests
 --------------------------------------------------------------------------------
 
-s.index[0]:select()
-s.index[0]:select({})
-s.index[0]:select(nil)
-s.index[0]:select({}, {iterator = 'ALL'})
-s.index[0]:select(nil, {iterator = box.index.ALL })
-s.index[0]:select({}, {iterator = box.index.ALL, limit = 10})
-s.index[0]:select(nil, {iterator = box.index.ALL, limit = 0})
-s.index[0]:select({}, {iterator = 'ALL', limit = 1, offset = 15})
-s.index[0]:select(nil, {iterator = 'ALL', limit = 20, offset = 15})
-
-s.index[0]:select(nil, {iterator = box.index.EQ})
-s.index[0]:select({}, {iterator = 'EQ'})
-s.index[0]:select(nil, {iterator = 'REQ'})
-s.index[0]:select({}, {iterator = box.index.REQ})
-
-s.index[0]:select(nil, {iterator = 'EQ', limit = 2, offset = 1})
-s.index[0]:select({}, {iterator = box.index.REQ, limit = 2, offset = 1})
-
-s.index[0]:select(1)
-s.index[0]:select({1})
-s.index[0]:select({1, 2})
-s.index[0]:select(0)
-s.index[0]:select({0})
-s.index[0]:select("0")
-s.index[0]:select({"0"})
-
-s.index[1]:select(1)
-s.index[1]:select({1})
-s.index[1]:select({1}, {limit = 2})
-s.index[1]:select(1, {iterator = 'EQ'})
-s.index[1]:select({1}, {iterator = box.index.EQ, offset = 16, limit = 2})
-s.index[1]:select({1}, {iterator = box.index.REQ, offset = 16, limit = 2 })
-s.index[1]:select({1, 2}, {iterator = 'EQ'})
-s.index[1]:select({1, 2}, {iterator = box.index.REQ})
-s.index[1]:select({1, 2})
-
-s.index[0]:select(nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
-s.index[0]:select({}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
-
-s.index[0]:select(1)
-s.index[0]:select(1, { iterator = box.index.EQ })
-s.index[0]:select(1, { iterator = 'EQ' })
-s.index[0]:select(1, { iterator = 'GE' })
-s.index[0]:select(1, { iterator = 'GE', limit = 2 })
-s.index[0]:select(1, { iterator = 'LE', limit = 2 })
-s.index[0]:select(1, { iterator = 'GE', offset = 10, limit = 2 })
+--# setopt delimiter ';'
+function test(idx, ...)
+    local t1 = idx:select_ffi(...)
+    local t2 = idx:select_luac(...)
+    if msgpack.encode(t1) ~= msgpack.encode(t2) then
+        return 'different result from select_ffi and select_luac', t1, t2
+    end
+    return t1
+end;
+--# setopt delimiter ''
+
+s.index[0].select == s.index[0].select_ffi or s.index[0].select == s.index[0].select_luac
+
+test(s.index[0])
+test(s.index[0], {})
+test(s.index[0], nil)
+test(s.index[0], {}, {iterator = 'ALL'})
+
+test(s.index[0], nil, {iterator = box.index.ALL })
+test(s.index[0], {}, {iterator = box.index.ALL, limit = 10})
+test(s.index[0], nil, {iterator = box.index.ALL, limit = 0})
+test(s.index[0], {}, {iterator = 'ALL', limit = 1, offset = 15})
+test(s.index[0], nil, {iterator = 'ALL', limit = 20, offset = 15})
+
+test(s.index[0], nil, {iterator = box.index.EQ})
+test(s.index[0], {}, {iterator = 'EQ'})
+test(s.index[0], nil, {iterator = 'REQ'})
+test(s.index[0], {}, {iterator = box.index.REQ})
+
+test(s.index[0], nil, {iterator = 'EQ', limit = 2, offset = 1})
+test(s.index[0], {}, {iterator = box.index.REQ, limit = 2, offset = 1})
+
+test(s.index[0], 1)
+test(s.index[0], {1})
+test(s.index[0], {1, 2})
+test(s.index[0], 0)
+test(s.index[0], {0})
+test(s.index[0], "0")
+test(s.index[0], {"0"})
+
+test(s.index[1], 1)
+test(s.index[1], {1})
+test(s.index[1], {1}, {limit = 2})
+test(s.index[1], 1, {iterator = 'EQ'})
+test(s.index[1], {1}, {iterator = box.index.EQ, offset = 16, limit = 2})
+test(s.index[1], {1}, {iterator = box.index.REQ, offset = 16, limit = 2 })
+test(s.index[1], {1, 2}, {iterator = 'EQ'})
+test(s.index[1], {1, 2}, {iterator = box.index.REQ})
+test(s.index[1], {1, 2})
+
+test(s.index[0], nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
+test(s.index[0], {}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
+
+test(s.index[0], 1)
+test(s.index[0], 1, { iterator = box.index.EQ })
+test(s.index[0], 1, { iterator = 'EQ' })
+test(s.index[0], 1, { iterator = 'GE' })
+test(s.index[0], 1, { iterator = 'GE', limit = 2 })
+test(s.index[0], 1, { iterator = 'LE', limit = 2 })
+test(s.index[0], 1, { iterator = 'GE', offset = 10, limit = 2 })
 
 s:select(2)
 
-- 
GitLab