commit dc26e47e1bacb3148bc3fc5753cc1c82afb521ef from: Georgiy Lebedev via: Vladimir Davydov date: Mon Jul 03 08:51:58 2023 UTC box: introduce formats for standalone tuples Introduce `box.tuple.format` object, a Lua wrapper around tuple format: these objects own a tuple format, which is almost equivalent to `space:format`, except for check constraints and foreign key constraints being disabled (though they appear to be present for compatibility with `space:format`). Add an option table argument to `box.tuple.new` with 'format' option, allowing to create formatted spaceless tuples. Closes #4693 @TarantoolBot document Title: Formats for standalone tuples and `box_tuple_new_vararg` compat opt A new box.tuple.format library was added, with a tuple format constructor (`new`) and a tuple format validator (`is`). New tuple format objects (userdata) were added, which can be used with the same format clause as for the `space:format` method (except that check constraints and foreign keys are disabled for them): NO_WRAP ```lua f = box.tuple.format.new(box.space._space:format()) f = box.tuple.format.new{{name = 'field1', type = 'string', is_nullable = true, nullable_action = 'none', collation = 'unicode_uk_s2', default = 'UPPER("string")', constraint = {ck = 'box.schema.user.info'}, foreign_key = {fk = {space = '_space', field = 'name'}}}, {name = 'field2', nullable_action = 'ignore', foreign_key = {fk = {space = '_space', field = 1}}}} ``` NO_WRAP Format objects have several introspection methods: `:pairs`, `:ipairs`, `totable`, and also have a `__serialize` metamethod — these methods return the original (i.e., user-provided) format clause. `:pairs` is an alias to `ipairs` (since the format clause is an array by nature), and the `totable` method is an alias to the `__serialize` metamethod, which returns an array of field definitions. Format objects also have a `:tostring` method, which simply returns a "box.tuple.format" literal. The standalone tuple constructor, `box.tuple.new` was extended with an options parameter which currently has one available option, `format` (default value is `nil`, i.e., no format). The format option is either a tuple format object previously created using `box.tuple.format.new` or a format clause. Examples of standalone tuple creation with formats: NO_WRAP ```lua box.tuple.new({1}, {format = {{name = 'field', type = 'number'}}}) box.tuple.new({1}, {format = {{'field', type = 'number'}}}) box.tuple.new({1}, {format = {{'field', 'number'}}}) f = box.tuple.format.new({{name = 'field', type = 'number'}}) box.tuple.new({}, {format = f}) box.tuple.new({1}, {format = f}) box.tuple.new({'str'}, {format = f}) -- error: Tuple field 1 (field) type does not match one required by operation: expected number, got string box.tuple.new({'str'}, {format = f}) ``` NO_WRAP See also the design document https://www.notion.so/tarantool/Schemafull-IPROTO-cc315ad6bdd641dea66ad854992d8cbf?pvs=4#a33e2d7418d249679969e5f21ef2832c A new `box_tuple_new_vararg` compatibility option was introduced: a new page needs to be created for it (https://tarantool.io/compat/box_tuple_new_vararg) This option controls whether `box.tuple.new` should interpret an argument list as an array of tuple fields (i.e., vararg, old behaviour), or as a value plus a tuple format (new default behaviour). The value can be either a scalar, an array or a box tuple. The old behaviour does not allow creating formatted standalone tuples. Old behaviour examples: ```lua box.tuple.new(1) box.tuple.new{1} box.tuple.new(1, 2, 3) box.tuple.new{1, 2, 3} -- This won't create a formatted tuple: the format option will become the -- second tuple field. box.tuple.new({1, 2, 3}, {format = box.tuple.format.new{{'field'}}}) ``` New behaviour examples: ```lua box.tuple.new(1) box.tuple.new(1, {format = box.tuple.format.new{{'field'}}}) box.tuple.new{1} box.tuple.new({1}, {format = box.tuple.format.new{{'field'}}}) box.tuple.new(1, 2, 3) -- error box.tuple.new(1, 2, 3, {format = box.tuple.format.new{{'field'}}}) -- error box.tuple.new{1, 2, 3} box.tuple.new({1, 2, 3}, {format = box.tuple.format.new{{'field'}}}) ``` See also the design document https://www.notion.so/tarantool/Schemafull-IPROTO-cc315ad6bdd641dea66ad854992d8cbf?pvs=4#6f74f0c70005463b8438830edd1a0117. commit - 05e8b1de2150300c868d04e0f2a578be5eca9d27 commit + dc26e47e1bacb3148bc3fc5753cc1c82afb521ef blob - /dev/null blob + 91de7c82362d26709d101a72e203bbb93fa78ef8 (mode 644) --- /dev/null +++ changelogs/unreleased/gh-4693-formats-for-standalone-tuples.md @@ -0,0 +1,7 @@ +## feature/box + +* Introduced `box.tuple.format` that enables format definition for tuples + created via `box.tuple.new` (standalone tuples) (gh-4693). +* **[Breaking change]** Disabled argument list syntax of `box.tuple.new` (this + was needed for gh-4693). It is possible to switch to the old behavior using + the compatibility option `box_tuple_new_vararg`. blob - 7081a1e27de25bcc5476eaba12109895bf853cc9 blob + 234a8e81c83c60c679f787fa5ab324fb1e0c3284 --- src/box/CMakeLists.txt +++ src/box/CMakeLists.txt @@ -10,6 +10,7 @@ set(lua_sources) lua_source(lua_sources lua/load_cfg.lua load_cfg_lua) lua_source(lua_sources lua/schema.lua schema_lua) lua_source(lua_sources lua/tuple.lua tuple_lua) +lua_source(lua_sources lua/tuple_format.lua tuple_format_lua) lua_source(lua_sources lua/session.lua session_lua) if (ENABLE_FEEDBACK_DAEMON) lua_source(lua_sources lua/feedback_daemon.lua feedback_daemon_lua) blob - a04222964e2fbc5598d875d2a1fd46bab82fdb23 blob + b0a3b49c4326ce2fa7ab5c5245efac3434bc7d72 --- src/box/lua/init.c +++ src/box/lua/init.c @@ -83,6 +83,7 @@ static uint32_t CTID_STRUCT_TXN_SAVEPOINT_PTR = 0; extern char session_lua[], tuple_lua[], + tuple_format_lua[], key_def_lua[], schema_lua[], load_cfg_lua[], @@ -179,6 +180,7 @@ extern char session_lua[], static const char *lua_sources[] = { "box/session", NULL, session_lua, "box/tuple", NULL, tuple_lua, + "box/tuple_format", NULL, tuple_format_lua, "box/schema", NULL, schema_lua, #if ENABLE_FEEDBACK_DAEMON /* blob - 3b96806d57d9e905502fc90365f78c2f5744661f blob + a1e9b705d1dd32fcce9a4be8f28ecc6f7f4070e4 --- src/box/lua/schema.lua +++ src/box/lua/schema.lua @@ -668,7 +668,7 @@ local function normalize_foreign_key(space_id, space_n fkey.space == space_name) fkey = normalize_foreign_key_one(fkey, error_prefix, is_complex, fkey_same_space) - local fkey_name = fkey_same_space and space_name or + local fkey_name = fkey_same_space and (space_name or 'unknown') or box.space[fkey.space].name return {[fkey_name] = fkey} end @@ -747,7 +747,9 @@ local function normalize_format(space_id, space_name, end return result end -box.internal.space.normalize_format = normalize_format -- for space.upgrade + +-- for space.upgrade and box.tuple.format.new +box.internal.space.normalize_format = normalize_format local function denormalize_foreign_key_one(fkey) assert(type(fkey.field) == 'string' or type(fkey.field) == 'number') @@ -781,6 +783,8 @@ local function denormalize_format(format) return result end +box.internal.space.denormalize_format = denormalize_format + box.schema.space = {} box.schema.space.create = function(name, options) check_param(name, 'name', 'string') blob - dcdbb3ae408712b4bf843bfcb7d87c2ccf9bf419 blob + 316c9cd7701d50c7b286f0f7f48e4c0a8e974737 --- src/box/lua/tuple.c +++ src/box/lua/tuple.c @@ -29,6 +29,7 @@ * SUCH DAMAGE. */ #include "box/lua/tuple.h" +#include "box/lua/tuple_format.h" #include "box/xrow_update.h" #include "lua/utils.h" /* luaT_error() */ @@ -140,29 +141,6 @@ luaT_istuple(struct lua_State *L, int narg) * (because it is usual for the module API). */ -/** - * Encode a Lua values on a Lua stack as an MsgPack array. - * - * Raise a Lua error when encoding fails. - * - * Helper for (). - */ -static int -luaT_tuple_encode_values(struct lua_State *L, struct ibuf *buf) -{ - struct mpstream stream; - mpstream_init(&stream, buf, ibuf_reserve_cb, ibuf_alloc_cb, luamp_error, - L); - int argc = lua_gettop(L); - mpstream_encode_array(&stream, argc); - for (int k = 1; k <= argc; ++k) { - if (luamp_encode(L, luaL_msgpack_default, &stream, k) != 0) - return -1; - } - mpstream_flush(&stream); - return 0; -} - typedef void luaT_mpstream_init_f(struct mpstream *stream, struct lua_State *L, void *buffer); @@ -314,36 +292,16 @@ static int lbox_tuple_new(lua_State *L) { int argc = lua_gettop(L); - if (argc < 1) { - lua_newtable(L); /* create an empty tuple */ - ++argc; - } - /* - * Use backward-compatible parameters format: - * box.tuple.new(1, 2, 3). - */ - box_tuple_format_t *fmt = box_tuple_format_default(); - if (argc != 1 || (!lua_istable(L, 1) && !luaT_istuple(L, 1))) { - struct ibuf *buf = cord_ibuf_take(); - struct tuple *tuple = NULL; - - if (luaT_tuple_encode_values(L, buf) != 0) - goto cleanup; - tuple = box_tuple_new(fmt, buf->buf, buf->buf + ibuf_used(buf)); -cleanup: - cord_ibuf_drop(buf); - if (tuple == NULL) - return luaT_error(L); - luaT_pushtuple(L, tuple); - return 1; - } - - /* * Use the new parameters format: - * box.tuple.new({1, 2, 3}). + * box.tuple.new({tuple_field1, tuple_field2, tuple_field3}, [options]). */ - struct tuple *tuple = luaT_tuple_new(L, 1, fmt); + struct tuple_format *format; + if (argc == 2) + format = luaT_check_tuple_format(L, -1); + else + format = box_tuple_format_default(); + struct tuple *tuple = luaT_tuple_new(L, 1, format); if (tuple == NULL) return luaT_error(L); /* box_tuple_new() doesn't leak on exception, see public API doc */ @@ -736,10 +694,6 @@ static const struct luaL_Reg lbox_tuple_meta[] = { {"transform", lbox_tuple_transform}, {"tuple_to_map", lbox_tuple_to_map}, {"tuple_field_by_path", lbox_tuple_field_by_path}, - {NULL, NULL} -}; - -static const struct luaL_Reg lbox_tuplelib[] = { {"new", lbox_tuple_new}, {NULL, NULL} }; @@ -777,9 +731,6 @@ box_lua_tuple_init(struct lua_State *L) lua_pop(L, 1); /* box.internal */ luaL_register_type(L, tuple_iteratorlib_name, lbox_tuple_iterator_meta); - luaL_findtable(L, LUA_GLOBALSINDEX, tuplelib_name, 0); - luaL_setfuncs(L, lbox_tuplelib, 0); - lua_pop(L, 1); tuple_serializer_update_options(); trigger_create(&tuple_serializer.update_trigger, blob - 33aa444849b45d84fc154b8197d40b56fde9e23f blob + 200308aab19cf1fc7316e72ecc1cacbd67cd744a --- src/box/lua/tuple.lua +++ src/box/lua/tuple.lua @@ -4,6 +4,7 @@ local ffi = require('ffi') local msgpackffi = require('msgpackffi') local fun = require('fun') local buffer = require('buffer') +local compat = require('compat') local internal = box.internal local cord_ibuf_take = buffer.internal.cord_ibuf_take local cord_ibuf_put = buffer.internal.cord_ibuf_put @@ -65,6 +66,36 @@ local builtin = ffi.C local tuple_t = ffi.typeof('box_tuple_t') local const_tuple_ref_t = ffi.typeof('box_tuple_t&') +local NEW_OPTION_TYPES = { + format = function(format) + if type(format) ~= 'table' and not box.tuple.format.is(format) then + return false, "table, box.tuple.format" + end + return true + end +} + +local new_tuple = function(...) + if compat.box_tuple_new_vararg.current == 'old' then + return internal.tuple.new{...} + end + local tuple, options = ... + if type(tuple) ~= 'table' and not box.tuple.is(tuple) then + tuple = {tuple} + end + internal.check_param_table(options, NEW_OPTION_TYPES) + if options == nil then + return internal.tuple.new(tuple) + end + local format + if box.tuple.format.is(options.format) then + format = options.format + else + format = box.tuple.format.new(options.format) + end + return internal.tuple.new(tuple, format) +end + local is_tuple = function(tuple) return tuple ~= nil and type(tuple) == 'cdata' and ffi.istype(const_tuple_ref_t, tuple) end @@ -368,6 +399,10 @@ internal.tuple.encode = tuple_encode -- Public API, additional to implemented in C. +-- new() needs a wrapper in Lua, because format normalization needs to be done +-- in Lua. +box.tuple.new = new_tuple + -- is() is implemented in Lua, because then it is -- easy to be JITed. box.tuple.is = is_tuple blob - 089928ae5bd8f4fee46a2df246f165c706dbf325 blob + d0faf5515739d602306dbd6551e77ed82c64401f --- src/box/lua/tuple_format.c +++ src/box/lua/tuple_format.c @@ -24,6 +24,13 @@ luaT_check_tuple_format(struct lua_State *L, int narg) } static int +lbox_tuple_format_is(struct lua_State *L) +{ + lua_pushboolean(L, luaL_testudata(L, 1, tuple_format_typename) != NULL); + return 1; +} + +static int lbox_tuple_format_gc(struct lua_State *L) { struct tuple_format *format = luaT_check_tuple_format(L, 1); @@ -84,15 +91,73 @@ lbox_tuple_format_new(struct lua_State *L) return luaT_push_tuple_format(L, format); } +/** + * Returns the tuple format object type name. + */ +static int +lbox_tuple_format_tostring(struct lua_State *L) +{ + luaT_check_tuple_format(L, 1); + lua_pushstring(L, tuple_format_typename); + return 1; +} + +/* + * Returns the format clause with which this tuple format was created. + */ +static int +lbox_tuple_format_serialize(struct lua_State *L) +{ + struct tuple_format *format = luaT_check_tuple_format(L, 1); + if (format->data == NULL) { + lua_createtable(L, 0, 0); + return 1; + } + const char *data = format->data; + luamp_decode(L, luaL_msgpack_default, &data); + luaL_findtable(L, LUA_GLOBALSINDEX, "box.internal.space", 1); + lua_getfield(L, -1, "denormalize_format"); + lua_remove(L, -2); + lua_pushvalue(L, -2); + lua_call(L, 1, 1); + return 1; +} + +/* + * Simply returns `ipairs(format:totable())`. + */ +static int +lbox_tuple_format_ipairs(struct lua_State *L) +{ + lbox_tuple_format_serialize(L); + lua_getfield(L, LUA_GLOBALSINDEX, "ipairs"); + lua_insert(L, -2); + lua_call(L, 1, 3); + return 3; +} + void box_lua_tuple_format_init(struct lua_State *L) { const struct luaL_Reg lbox_tuple_format_meta[] = { {"__gc", lbox_tuple_format_gc}, + {"__serialize", lbox_tuple_format_serialize}, + {"__tostring", lbox_tuple_format_tostring}, + {"totable", lbox_tuple_format_serialize}, + {"ipairs", lbox_tuple_format_ipairs}, + {"pairs", lbox_tuple_format_ipairs}, {NULL, NULL} }; luaL_register_type(L, tuple_format_typename, lbox_tuple_format_meta); + const struct luaL_Reg lbox_tuple_formatlib[] = { + {"is", lbox_tuple_format_is}, + {NULL, NULL} + }; + luaL_findtable(L, LUA_GLOBALSINDEX, "box.tuple.format", 0); + luaL_setfuncs(L, lbox_tuple_formatlib, 0); + lua_pop(L, 1); + const struct luaL_Reg box_tuple_formatlib_internal[] = { {"new", lbox_tuple_format_new}, {NULL, NULL} blob - /dev/null blob + d4c64fe9dfdf9573883664f9222b4e097b80422c (mode 644) --- /dev/null +++ src/box/lua/tuple_format.lua @@ -0,0 +1,7 @@ +-- new() needs a wrapper in Lua, because format normalization needs to be done +-- in Lua. +box.tuple.format.new = function(format) + box.internal.check_param(format, 'format', 'table') + format = box.internal.space.normalize_format(nil, nil, format) + return box.internal.tuple_format.new(format) +end blob - 00ae2ac75c67d1d889ce70baf034cc4e88a2b069 blob + 6727b5a8dd3b042f6c27827780c6bce6515da2a7 --- src/lua/compat.lua +++ src/lua/compat.lua @@ -63,6 +63,14 @@ Whether a binary data field should be stored in a varb string when decoded in Lua. https://tarantool.io/compat/binary_data_decoding +]] + +local BOX_TUPLE_NEW_VARARG_BRIEF = [[ +Whether `box.tuple.new` should interpret the argument list as an array of tuple +fields (i.e., vararg) - this does not allow passing a tuple format as a second +argument. + +https://tarantool.io/compat/box_tuple_new_vararg ]] -- Returns an action callback that toggles a tweak. @@ -124,6 +132,12 @@ local options = { tweaks.yaml_decode_binary_as_string = not is_new tweaks.msgpack_decode_binary_as_string = not is_new end, + }, + box_tuple_new_vararg = { + default = 'new', + obsolete = nil, + brief = BOX_TUPLE_NEW_VARARG_BRIEF, + action = function() end, }, } blob - d21ae5108bfbdcbe3038456f6738cfb75b1eb7e7 blob + 73d9e79e84fe7b650ba0a8ab6be0862ccf38dd7c --- test/app-luatest/msgpack_test.lua +++ test/app-luatest/msgpack_test.lua @@ -219,7 +219,7 @@ g.test_object_encode_decode = function() t.assert_equals( msgpack.decode(msgpack.encode({mp, {foo = mp}})), {mp:decode(), {foo = mp:decode()}}) - t.assert_equals(msgpack.object(box.tuple.new(1, 2, 3)):decode(), {1, 2, 3}) + t.assert_equals(msgpack.object(box.tuple.new{1, 2, 3}):decode(), {1, 2, 3}) t.assert_equals( msgpack.object({ foo = box.tuple.new(123), blob - c54c30f57a7a6a500b2c7c5b77696efe4edaf3c2 blob + 98e4a7bcc78a2eda5bb5ba34d990d5a08e2783d6 --- test/box/call.result +++ test/box/call.result @@ -135,7 +135,7 @@ conn:eval("return return_emptytuple()") --- - [] ... -function return_tuple() return box.tuple.new(1, 2, 3) end +function return_tuple() return box.tuple.new{1, 2, 3} end --- ... conn:call("return_tuple") @@ -146,7 +146,7 @@ conn:eval("return return_tuple()") --- - [1, 2, 3] ... -function return_tuples() return box.tuple.new(1, 2, 3), box.tuple.new(3, 4, 5) end +function return_tuples() return box.tuple.new{1, 2, 3}, box.tuple.new{3, 4, 5} end --- ... conn:call("return_tuples") blob - 2b0c2b622162f67e7dda8a98a0bf06941e33b785 blob + 2703a944fc678c208b9e5616f4df2e1892635317 --- test/box/call.test.lua +++ test/box/call.test.lua @@ -47,11 +47,11 @@ function return_emptytuple() return box.tuple.new() en conn:call("return_emptytuple") conn:eval("return return_emptytuple()") -function return_tuple() return box.tuple.new(1, 2, 3) end +function return_tuple() return box.tuple.new{1, 2, 3} end conn:call("return_tuple") conn:eval("return return_tuple()") -function return_tuples() return box.tuple.new(1, 2, 3), box.tuple.new(3, 4, 5) end +function return_tuples() return box.tuple.new{1, 2, 3}, box.tuple.new{3, 4, 5} end conn:call("return_tuples") conn:eval("return return_tuples()") blob - 26c0a1efe5364ef2e6aed7d5d61c9b1e5f78974a blob + 71f23edb3e4161b9cae4181c8ed82e93146f165c --- test/box/sequence.result +++ test/box/sequence.result @@ -923,15 +923,15 @@ s3 = box.schema.space.create('test3') _ = s3:create_index('pk', {parts = {2, 'unsigned', 1, 'string'}, sequence = 'test'}) --- ... -s1:insert(box.tuple.new(nil)) -- 1 +s1:insert(box.tuple.new(box.NULL)) -- 1 --- - [1] ... -s2:insert(box.tuple.new('a', nil)) -- 2 +s2:insert(box.tuple.new{'a', box.NULL}) -- 2 --- - ['a', 2] ... -s3:insert(box.tuple.new('b', nil)) -- 3 +s3:insert(box.tuple.new{'b', box.NULL}) -- 3 --- - ['b', 3] ... blob - e9eb993cd7cfd3019221bb738751d06923465eff blob + 5a56b74e9fa507e749231977fa5a4c993b9e2938 --- test/box/sequence.test.lua +++ test/box/sequence.test.lua @@ -295,9 +295,9 @@ _ = s2:create_index('pk', {parts = {2, 'integer'}, seq s3 = box.schema.space.create('test3') _ = s3:create_index('pk', {parts = {2, 'unsigned', 1, 'string'}, sequence = 'test'}) -s1:insert(box.tuple.new(nil)) -- 1 -s2:insert(box.tuple.new('a', nil)) -- 2 -s3:insert(box.tuple.new('b', nil)) -- 3 +s1:insert(box.tuple.new(box.NULL)) -- 1 +s2:insert(box.tuple.new{'a', box.NULL}) -- 2 +s3:insert(box.tuple.new{'b', box.NULL}) -- 3 s1:truncate() s2:truncate() s3:truncate() blob - 0298b2c9f7e294faaff6170ca6a7c96425c6e126 blob + ce022ada3620e43411d91622423fa8389f713750 --- test/box/tuple.result +++ test/box/tuple.result @@ -205,34 +205,18 @@ box.tuple.new{1} --- - [1] ... -box.tuple.new(1, 2, 3, 4, 5) ---- -- [1, 2, 3, 4, 5] -... box.tuple.new{1, 2, 3, 4, 5} --- - [1, 2, 3, 4, 5] ... -box.tuple.new({'a', 'b'}, {'c', 'd'}, {'e', 'f'}) ---- -- [['a', 'b'], ['c', 'd'], ['e', 'f']] -... box.tuple.new{{'a', 'b'}, {'c', 'd'}, {'e', 'f'}} --- - [['a', 'b'], ['c', 'd'], ['e', 'f']] -... -box.tuple.new({1, 2}, 'x', 'y', 'z', {c = 3, d = 4}, {e = 5, f = 6}) ---- -- [[1, 2], 'x', 'y', 'z', {'c': 3, 'd': 4}, {'e': 5, 'f': 6}] ... box.tuple.new{{1, 2}, 'x', 'y', 'z', {c = 3, d = 4}, {e = 5, f = 6}} --- - [[1, 2], 'x', 'y', 'z', {'c': 3, 'd': 4}, {'e': 5, 'f': 6}] ... -box.tuple.new('x', 'y', 'z', {1, 2}, {c = 3, d = 4}, {e = 5, f = 6}) ---- -- ['x', 'y', 'z', [1, 2], {'c': 3, 'd': 4}, {'e': 5, 'f': 6}] -... box.tuple.new{'x', 'y', 'z', {1, 2}, {c = 3, d = 4}, {e = 5, f = 6}} --- - ['x', 'y', 'z', [1, 2], {'c': 3, 'd': 4}, {'e': 5, 'f': 6}] @@ -1003,12 +987,12 @@ test_run:cmd("setopt delimiter ';'") - true ... null = nil -t = box.tuple.new({1, -2, 1.2, -1.2}, 'x', 'y', 'z', null, true, false, +t = box.tuple.new{{1, -2, 1.2, -1.2}, 'x', 'y', 'z', null, true, false, {bin = "\x08\x5c\xc2\x80\x12\x2f", big_num = tonumber64('18446744073709551615'), map = {key = "value"}, double=1.0000000001, - utf8="Кудыкины горы"}); + utf8="Кудыкины горы"}}; --- ... tostring(t); @@ -1505,7 +1489,7 @@ a = uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39 b = uuid.fromstr("83eb4959-3de6-49fb-8890-6fb4423dd186") --- ... -t = box.tuple.new(a, 2, b, "string") +t = box.tuple.new{a, 2, b, "string"} --- ... state, val = t:next() blob - 4201e98606c6ddfa2f1c2c02cab3dbc481e5fe26 blob + 4841d4bbae3637a2077fbf96a0e7e4f42bf7961f --- test/box/tuple.test.lua +++ test/box/tuple.test.lua @@ -58,16 +58,12 @@ box.tuple.new{} box.tuple.new(1) box.tuple.new{1} -box.tuple.new(1, 2, 3, 4, 5) box.tuple.new{1, 2, 3, 4, 5} -box.tuple.new({'a', 'b'}, {'c', 'd'}, {'e', 'f'}) box.tuple.new{{'a', 'b'}, {'c', 'd'}, {'e', 'f'}} -box.tuple.new({1, 2}, 'x', 'y', 'z', {c = 3, d = 4}, {e = 5, f = 6}) box.tuple.new{{1, 2}, 'x', 'y', 'z', {c = 3, d = 4}, {e = 5, f = 6}} -box.tuple.new('x', 'y', 'z', {1, 2}, {c = 3, d = 4}, {e = 5, f = 6}) box.tuple.new{'x', 'y', 'z', {1, 2}, {c = 3, d = 4}, {e = 5, f = 6}} t=box.tuple.new{'a','b','c'} @@ -332,12 +328,12 @@ collectgarbage('collect') -- collect huge string -- testing tostring test_run:cmd("setopt delimiter ';'") null = nil -t = box.tuple.new({1, -2, 1.2, -1.2}, 'x', 'y', 'z', null, true, false, +t = box.tuple.new{{1, -2, 1.2, -1.2}, 'x', 'y', 'z', null, true, false, {bin = "\x08\x5c\xc2\x80\x12\x2f", big_num = tonumber64('18446744073709551615'), map = {key = "value"}, double=1.0000000001, - utf8="Кудыкины горы"}); + utf8="Кудыкины горы"}}; tostring(t); t; test_run:cmd("setopt delimiter ''"); @@ -518,7 +514,7 @@ uuid = require("uuid") -- output comparison. a = uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39") b = uuid.fromstr("83eb4959-3de6-49fb-8890-6fb4423dd186") -t = box.tuple.new(a, 2, b, "string") +t = box.tuple.new{a, 2, b, "string"} state, val = t:next() state val == a blob - /dev/null blob + 075e55f43f9a9b1237ff5b00dcabcfe9e28a5beb (mode 644) --- /dev/null +++ test/box-luatest/gh_4693_formats_for_standalone_tuples_test.lua @@ -0,0 +1,229 @@ +local server = require('luatest.server') +local t = require('luatest') + +local g = t.group() + +g.before_all(function(cg) + cg.server = server:new() + cg.server:start() +end) + +g.after_all(function(cg) + cg.server:drop() +end) + +-- Checks that `box.tuple.format.new` works as expected. +g.test_box_tuple_format_new = function() + t.assert_equals(type(box.tuple.format.new{}), 'userdata') + t.assert(box.tuple.format.is(box.tuple.format.new{})) + t.assert_not(box.tuple.format.is(777)) + + local err_msg = "Illegal parameters, format should be a table" + t.assert_error_msg_content_equals(err_msg, function () + box.tuple.format.new(777) + end) + err_msg = "Wrong space format field 1: unknown field type" + t.assert_error_msg_content_equals(err_msg, function () + box.tuple.format.new{{'field', 'unknown'}} + end) + err_msg = "unsupported Lua type 'function'" + t.assert_error_msg_content_equals(err_msg, function () + box.tuple.format.new{{'field', 'number', + nullable_action = function() end}} + end) + err_msg = "Space field 'field' is duplicate" + t.assert_error_msg_content_equals(err_msg, function () + box.tuple.format.new{{'field'}, {'field'}} + end) +end + +g.before_test('test_box_tuple_format_gc', function (cg) + cg.server:exec(function() + box.error.injection.set('ERRINJ_TUPLE_FORMAT_COUNT', 2) + end) +end) + +-- Checks that tuple formats are garbage collected and recycled. +g.test_box_tuple_format_gc = function(cg) + t.tarantool.skip_if_not_debug() + + cg.server:exec(function() + box.tuple.format.new{{name = 'field0'}} + collectgarbage() + box.tuple.format.new{{name = 'field1'}} + end) +end + +g.after_test('test_box_tuple_format_gc', function (cg) + cg.server:exec(function() + box.error.injection.set('ERRINJ_TUPLE_FORMAT_COUNT', -1) + end) +end) + + +-- Checks that box.tuple.format serialization works as expected. +g.test_box_tuple_format_serialization = function(cg) + cg.server:exec(function() + local err_msg = "Illegal parameters, format should be a table" + t.assert_error_msg_content_equals(err_msg, function() + box.tuple.format.new() + end) + local f = box.tuple.format.new{} + t.assert_equals(tostring(f), "box.tuple.format") + local mt = getmetatable(f) + t.assert_equals(mt.__serialize, mt.totable) + t.assert_equals(mt.ipairs, mt.pairs) + + err_msg = "box.tuple.format expected, got no value" + t.assert_error_msg_contains(err_msg, function() + f.__tostring() + end) + t.assert_error_msg_contains(err_msg, function() + f.totable() + end) + t.assert_error_msg_contains(err_msg, function() + f.pairs() + end) + + local test_tuple_format_contents = function(f, expected) + local actual = {} + for i, field in f:pairs() do + table.insert(actual, i, field) + end + t.assert_equals(actual, expected) + t.assert_equals(f:totable(), expected) + end + + f = box.tuple.format.new{} + test_tuple_format_contents(f, {}) + + local f = box.tuple.format.new{} + test_tuple_format_contents(f, {}) + + f = box.tuple.format.new{{name = 'field1', type = 'string'}, + {name = 'field2', type = 'number'}} + test_tuple_format_contents(f, {{name = 'field1', type = 'string'}, + {name = 'field2', type = 'number'}}) + + local contents = {{name = 'field', type = 'string'}} + f = box.tuple.format.new { { 'field', 'string' } } + test_tuple_format_contents(f, contents) + f = box.tuple.format.new{{'field', type = 'string'}} + test_tuple_format_contents(f, contents) + f = box.tuple.format.new{{name = 'field', 'string'}} + test_tuple_format_contents(f, contents) + f = box.tuple.format.new{{name = 'field', type = 'string'}} + test_tuple_format_contents(f, contents) + contents = {{name = 'field1', type = 'string', is_nullable = true, + nullable_action = 'none', collation = 'unicode_uk_s2', + default = 'UPPER("string")', + constraint = {ck = 'box.schema.user.info'}, + foreign_key = {fk = {space = '_space', field = 'name'}}}, + {name = 'field2', type = 'any', nullable_action = 'ignore', + foreign_key = {fk = {space = '_space', field = 1}}}} + f = box.tuple.format.new(contents) + contents[1].collation = + box.space._collation.index.name:get{contents[1].collation}.id + contents[1].constraint = + {ck = box.space._func.index.name:get{contents[1].constraint.ck}.id} + local sid = + box.space._space.index.name:get{contents[1].foreign_key.fk.space}.id + contents[1].foreign_key.fk.space = sid + contents[2].foreign_key.fk.space = sid + test_tuple_format_contents(f, contents) + local fk = {field = 'name'} + contents = {{name = 'field1', type = 'any', foreign_key = fk}} + f = box.tuple.format.new(contents) + contents[1].foreign_key = {unknown = fk} + test_tuple_format_contents(f, contents) + end) +end + +-- Checks that `box.tuple.new` with format option works as expected. +g.test_box_tuple_new_with_format = function(cg) + cg.server:exec(function() + local f = {{name = 'field', type = 'number'}} + local options = {format = f} + local tuple = box.tuple.new({1}, options) + -- Checks that options table is not changed by box.tuple.new. + t.assert_equals(options, {format = f}) + t.assert_equals(tuple:tomap{names_only = true}, {field = 1}) + tuple = box.tuple.new(1, options) + t.assert_equals(tuple:tomap{names_only = true}, {field = 1}) + tuple = box.tuple.new(box.tuple.new(1, options), options) + t.assert_equals(tuple:tomap{names_only = true}, {field = 1}) + tuple = box.tuple.new({1, 'str'}, {format = f}) + t.assert_equals(tuple:tomap{names_only = true}, {field = 1}) + local err_msg = "Tuple field 1 (field) type does not match one " .. + "required by operation: expected number, got string" + t.assert_error_msg_content_equals(err_msg, function() + box.tuple.new({'str'}, {format = f}) + end) + err_msg = "Tuple field 1 (field) required by space format is missing" + t.assert_error_msg_content_equals(err_msg, function() + box.tuple.new({}, {format = f}) + end) + f = {{name = 'field', type = 'number', is_nullable = true}} + tuple = box.tuple.new({}, {format = f}) + t.assert_equals(tuple:tomap{names_only = true}, {}) + f = box.tuple.format.new{{'field', 'number'}} + tuple = box.tuple.new({1}, {format = f}) + t.assert_equals(tuple:tomap{names_only = true}, {field = 1}) + tuple = box.tuple.new({1, 'str'}, {format = f}) + t.assert_equals(tuple:tomap{names_only = true}, {field = 1}) + err_msg = "Illegal parameters, options should be a table" + t.assert_error_msg_content_equals(err_msg, function() + box.tuple.new({'str'}, 'fmt') + end) + err_msg = "Illegal parameters, format should be a table" + t.assert_error_msg_content_equals(err_msg, function() + box.tuple.new({'str'}, {}) + end) + + -- Checks that check constraint is disabled. + box.schema.func.create('ck', {is_deterministic = true, + body = "function () return false end"}) + box.tuple.new({0}, {format = {{'field', constraint = 'ck'}}}) + box.func.ck:drop() + + -- Checks that foreign key constraint is disabled. + box.schema.space.create('s') + box.tuple.new({0}, {format = + {{'field', foreign_key = {space = 's', field = 1}}}}) + box.space.s:drop() + box.tuple.format.new{{'field', foreign_key = {field = 1}}} + box.tuple.format.new{{'field', foreign_key = {fk = {field = 1}}}} + err_msg = 'Illegal parameters, format[1]: foreign key: space ' .. + 'nonexistent was not found' + t.assert_error_msg_content_equals(err_msg, function() + box.tuple.format.new{{'field', foreign_key = {space = 'nonexistent', + field = 1}}} + end) + end) +end + +-- Checks that `box.tuple.new` with scalar argument works correctly. +g.test_tuple_new_with_scalar_argument = function() + t.assert_equals(box.tuple.new(1):totable(), {1}) + t.assert_equals(box.tuple.new(box.tuple.new(1)):totable(), {1}) +end + +-- Checks that `box.tuple.new` backward compatibility works correctly. +g.test_box_tuple_new_with_compat = function(cg) + cg.server:exec(function() + local compat = require('compat') + + local err_msg = 'Illegal parameters, options should be a table' + t.assert_error_msg_content_equals(err_msg, function() + box.tuple.new(1, 2) + end) + compat.box_tuple_new_vararg = 'old' + t.assert_equals(box.tuple.new(1, 2, 3):totable(), {1, 2, 3}) + local fmt_table = {{'field', 'number'}} + local fmt = box.tuple.format.new(fmt_table) + local tuple = box.tuple.new({1}, {format = fmt}) + t.assert_equals(tuple:tomap{names_only = true}, {}) + tuple = box.tuple.new(box.tuple.new{'str'}, {format = fmt_table}) + t.assert_equals(tuple:tomap{names_only = true}, {}) + end) +end blob - 7fbf91a6622eb7f31b742558ce2358dfb3645a8a blob + cfff69002be08a421f92ff0a46f67967f6325c0d --- test/box-py/call.result +++ test/box-py/call.result @@ -420,7 +420,7 @@ call f() --- - {"c": {"106": [1, 1428578535], "2": [1, 1428578535]}, "pc": {"106": [1, 1428578535, 9243], "2": [1, 1428578535, 9243]}, "s": [1, 1428578535], "u": 1428578535, "v": []} - true -t = box.tuple.new('tuple', {1, 2, 3}, { k1 = 'v', k2 = 'v2'}) +t = box.tuple.new{'tuple', {1, 2, 3}, { k1 = 'v', k2 = 'v2'}} --- ... eval (return t)() blob - b758be817d84c6960744fd960b00a060db2ce37b blob + 33e9f10354168d8074ea182ca75a5d3e3c6a560f --- test/box-py/call.test.py +++ test/box-py/call.test.py @@ -182,7 +182,7 @@ test("{k1 = 'v1', k2 = 'v2'}") test("{s = {1, 1428578535}, u = 1428578535, v = {}, c = {['2'] = {1, 1428578535}, ['106'] = { 1, 1428578535} }, pc = {['2'] = {1, 1428578535, 9243}, ['106'] = {1, 1428578535, 9243}}}") test("true, {s = {1, 1428578535}, u = 1428578535, v = {}, c = {['2'] = {1, 1428578535}, ['106'] = { 1, 1428578535} }, pc = {['2'] = {1, 1428578535, 9243}, ['106'] = {1, 1428578535, 9243}}}") test("{s = {1, 1428578535}, u = 1428578535, v = {}, c = {['2'] = {1, 1428578535}, ['106'] = { 1, 1428578535} }, pc = {['2'] = {1, 1428578535, 9243}, ['106'] = {1, 1428578535, 9243}}}, true") -admin("t = box.tuple.new('tuple', {1, 2, 3}, { k1 = 'v', k2 = 'v2'})") +admin("t = box.tuple.new{'tuple', {1, 2, 3}, { k1 = 'v', k2 = 'v2'}}") test("t") test("t, t, t") test("{t}") blob - 0624c326cb7873ae767923fe120d1ea5bc0421df blob + 88e5630b17c19b8bae8a3f8d3e7d01ddffdcfd60 --- test/unit/lua_func_adapter.c +++ test/unit/lua_func_adapter.c @@ -83,8 +83,9 @@ test_tuple(void) int idx = generate_function( "function(a, b, tuple) " - "return box.tuple.new{a, b}, tuple, box.tuple.new{b, a}, " - "box.tuple.new{a + b, a - b} end"); + "return box.internal.tuple.new{a, b}, tuple, " + "box.internal.tuple.new{b, a}, " + "box.internal.tuple.new{a + b, a - b} end"); struct func_adapter *func = func_adapter_lua_create(tarantool_L, idx); struct func_adapter_ctx ctx; func_adapter_begin(func, &ctx);