Commit Diff


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 <lbox_tuple_new>().
- */
-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);