From 1d35d8667bee2b0afd5233c0572bcdf09e9e6d6a Mon Sep 17 00:00:00 2001
From: Aleksandr Lyapunov <alyapunov@tarantool.org>
Date: Tue, 28 Dec 2021 15:30:16 +0300
Subject: [PATCH] box: use field names in tuple constraint function

The previous commit adds tuple constraint lua functions that check
format of entire tuple. The problem was that in those functions
tuple could be accessed only by field indexes.

Add an ability to use field names too.

NO_DOC=see later commits
NO_CHANGELOG=see later commits
---
 src/box/lua/misc.cc                           | 82 +++++++++++++++++++
 src/box/lua/schema.lua                        |  3 +-
 src/box/port.c                                | 16 ++++
 src/box/port.h                                | 15 ++++
 src/box/tuple_constraint.h                    |  1 +
 src/box/tuple_constraint_func.c               |  6 +-
 src/box/tuple_format.c                        |  2 +-
 src/lib/core/port.h                           |  2 +-
 .../gh_6436_tuple_constraint_test.lua         |  6 +-
 9 files changed, 128 insertions(+), 5 deletions(-)

diff --git a/src/box/lua/misc.cc b/src/box/lua/misc.cc
index 98dabf2dd6..2cc1c74caf 100644
--- a/src/box/lua/misc.cc
+++ b/src/box/lua/misc.cc
@@ -61,6 +61,83 @@ lbox_encode_tuple_on_gc(lua_State *L, int idx, size_t *p_len)
 	return (char *) region_join_xc(gc, *p_len);
 }
 
+/**
+ * __index metamethod for the formatted array table that lookups field by name.
+ * Metatable of the table is expected to have `field_map` that provides
+ * name->index dictionary.
+ */
+static int
+lua_formatted_array_index(lua_State *L)
+{
+	/* L stack: table, field_name. */
+
+	assert(lua_gettop(L) == 2);
+
+	if (lua_getmetatable(L, 1) == 0) {
+		lua_settop(L, 0);
+		return 0;
+	}
+
+	/* L stack: table, field_name, metatable. */
+
+	lua_getfield(L, 3, "field_map");
+	if (lua_type(L, 4) != LUA_TTABLE) {
+		lua_settop(L, 0);
+		return 0;
+	}
+	lua_remove(L, 3);
+
+	/* L stack: table, field_name, field_map. */
+
+	lua_pushvalue(L, 2);
+	lua_remove(L, 2);
+
+	/* L stack: table, field_map, field_name. */
+
+	lua_gettable(L, 2);
+	if (lua_type(L, 3) != LUA_TNUMBER) {
+		lua_settop(L, 0);
+		return 0;
+	}
+	lua_remove(L, 2);
+
+	/* L stack: table, field_index. */
+
+	lua_gettable(L, 1);
+	lua_remove(L, 1);
+
+	return 1;
+}
+
+/**
+ * Set metatable for lua table on the top of lua stack @a L that would provide
+ * access by names in it according to given @a format.
+ * Lua table (that is on the top of L) is expected to be array-like.
+ */
+static void
+lua_wrap_formatted_array(struct lua_State *L, struct tuple_format *format)
+{
+	assert(format != NULL);
+	assert(lua_type(L, -1) == LUA_TTABLE);
+	if (format->dict->name_count == 0)
+		/* No names - no reason to wrap. */
+		return;
+
+	lua_newtable(L); /* metatable */
+	lua_newtable(L); /* metatable.field_map */
+
+	for (size_t i = 0; i < format->dict->name_count; i++) {
+		lua_pushnumber(L, i + 1);
+		lua_setfield(L, -2, format->dict->names[i]);
+	}
+
+	lua_setfield(L, -2, "field_map");
+
+	lua_pushcfunction(L, lua_formatted_array_index);
+	lua_setfield(L, -2, "__index");
+	lua_setmetatable(L, -2);
+}
+
 extern "C" void
 port_c_dump_lua(struct port *base, struct lua_State *L, bool is_flat)
 {
@@ -75,6 +152,11 @@ port_c_dump_lua(struct port *base, struct lua_State *L, bool is_flat)
 		} else {
 			mp = pe->mp;
 			luamp_decode(L, luaL_msgpack_default, &mp);
+
+			if (pe->mp_format != NULL) {
+				assert(mp_typeof(*pe->mp) == MP_ARRAY);
+				lua_wrap_formatted_array(L, pe->mp_format);
+			}
 		}
 		if (!is_flat)
 			lua_rawseti(L, -2, ++i);
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index b38ed88a85..865406dc95 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -85,7 +85,7 @@ ffi.cdef[[
 
     struct port {
         const struct port_vtab *vtab;
-        char pad[60];
+        char pad[68];
     };
 
     struct port_c_entry {
@@ -95,6 +95,7 @@ ffi.cdef[[
             char *mp;
         };
         uint32_t mp_size;
+        struct tuple_format *mp_format;
     };
 
     struct port_c {
diff --git a/src/box/port.c b/src/box/port.c
index 20dea5d658..dd020777f3 100644
--- a/src/box/port.c
+++ b/src/box/port.c
@@ -60,6 +60,8 @@ port_c_destroy_entry(struct port_c_entry *pe)
 		mempool_free(&port_entry_pool, pe->mp);
 	else
 		free(pe->mp);
+	if (pe->mp_format != NULL)
+		tuple_format_unref(pe->mp_format);
 }
 
 static void
@@ -102,6 +104,7 @@ port_c_new_entry(struct port_c *port)
 		port->last = e;
 	}
 	e->next = NULL;
+	e->mp_format = NULL;
 	++port->size;
 	return e;
 }
@@ -175,6 +178,19 @@ port_c_add_mp(struct port *base, const char *mp, const char *mp_end)
 	return 0;
 }
 
+int
+port_c_add_formatted_mp(struct port *base, const char *mp, const char *mp_end,
+			struct tuple_format *format)
+{
+	int rc = port_c_add_mp(base, mp, mp_end);
+	if (rc != 0)
+		return rc;
+	struct port_c *port = (struct port_c *)base;
+	port->last->mp_format = format;
+	tuple_format_ref(format);
+	return 0;
+}
+
 int
 port_c_add_str(struct port *base, const char *str, uint32_t len)
 {
diff --git a/src/box/port.h b/src/box/port.h
index c02ac5bd0b..08c5bf6695 100644
--- a/src/box/port.h
+++ b/src/box/port.h
@@ -119,6 +119,11 @@ struct port_c_entry {
 		char *mp;
 	};
 	uint32_t mp_size;
+	/**
+	 * Optional format of MsgPack data (that must be MP_ARR in that case).
+	 * Is NULL if format is not specified.
+	 */
+	struct tuple_format *mp_format;
 };
 
 /**
@@ -150,6 +155,16 @@ port_c_add_tuple(struct port *port, struct tuple *tuple);
 int
 port_c_add_mp(struct port *port, const char *mp, const char *mp_end);
 
+struct tuple_format;
+
+/**
+ * Append raw msgpack array to the port with given format.
+ * Msgpack is copied, the format is referenced for port's lifetime.
+ */
+int
+port_c_add_formatted_mp(struct port *port, const char *mp, const char *mp_end,
+			struct tuple_format *format);
+
 /** Append a string to the port. The string is copied as msgpack string. */
 int
 port_c_add_str(struct port *port, const char *str, uint32_t len);
diff --git a/src/box/tuple_constraint.h b/src/box/tuple_constraint.h
index 66c45e0dcf..8f98c73cec 100644
--- a/src/box/tuple_constraint.h
+++ b/src/box/tuple_constraint.h
@@ -16,6 +16,7 @@ struct tuple_constraint;
 struct tuple_field;
 struct tuple_format;
 struct space;
+struct tuple_format;
 
 /**
  * Type of constraint check function.
diff --git a/src/box/tuple_constraint_func.c b/src/box/tuple_constraint_func.c
index f3ec7aba84..8b74c62a6b 100644
--- a/src/box/tuple_constraint_func.c
+++ b/src/box/tuple_constraint_func.c
@@ -79,7 +79,11 @@ tuple_constraint_call_func(const struct tuple_constraint *constr,
 {
 	struct port out_port, in_port;
 	port_c_create(&in_port);
-	port_c_add_mp(&in_port, mp_data, mp_data_end);
+	if (field != NULL)
+		port_c_add_mp(&in_port, mp_data, mp_data_end);
+	else
+		port_c_add_formatted_mp(&in_port, mp_data, mp_data_end,
+					constr->space->format);
 	port_c_add_str(&in_port, constr->def.name, constr->def.name_len);
 
 	int rc = func_call(constr->func_cache_holder.func, &in_port, &out_port);
diff --git a/src/box/tuple_format.c b/src/box/tuple_format.c
index 80ad5518ab..e5b254f672 100644
--- a/src/box/tuple_format.c
+++ b/src/box/tuple_format.c
@@ -971,7 +971,7 @@ tuple_field_check_constraint(const struct tuple_field *field,
  * Check whole tuple constraints. Note that field constraints are not checked.
  */
 static int
-tuple_check_constraint(const struct tuple_format *format, const char *mp_data)
+tuple_check_constraint(struct tuple_format *format, const char *mp_data)
 {
 	if (format->constraint_count == 0)
 		return 0;
diff --git a/src/lib/core/port.h b/src/lib/core/port.h
index 2f88427098..d0002c1147 100644
--- a/src/lib/core/port.h
+++ b/src/lib/core/port.h
@@ -124,7 +124,7 @@ struct port {
 	 * Implementation dependent content. Needed to declare
 	 * an abstract port instance on stack.
 	 */
-	char pad[60];
+	char pad[68];
 };
 
 /** Is not inlined just to be exported. */
diff --git a/test/engine-luatest/gh_6436_tuple_constraint_test.lua b/test/engine-luatest/gh_6436_tuple_constraint_test.lua
index 9c16a2ba90..58c31cc084 100644
--- a/test/engine-luatest/gh_6436_tuple_constraint_test.lua
+++ b/test/engine-luatest/gh_6436_tuple_constraint_test.lua
@@ -39,14 +39,18 @@ g.test_tuple_constraint_basics = function(cg)
     cg.server:exec(function(engine)
         local constr_tuple_body1 = "function(tuple, name) " ..
             "if name ~= 'tuple_constr1' then error('wrong name!') end " ..
-            "return tuple[1] + tuple[2] < 100 end"
+            "if tuple[1] ~= tuple.id1 then error('wrong format!') end " ..
+            "if tuple[2] ~= tuple.id2 then error('wrong format!') end " ..
+            "return tuple[1] + tuple.id2 < 100 end"
 
         local function func_opts(body)
             return {language = 'LUA', is_deterministic = true, body = body}
         end
         box.schema.func.create('tuple_constr1', func_opts(constr_tuple_body1))
 
+        local fmt = {'id1', 'id2', 'id3'}
         local s = box.schema.create_space('test', {engine=engine,
+                                                   format=fmt,
                                                    constraint='tuple_constr1'})
         s:create_index('pk')
         box.schema.user.grant('guest', 'read,write', 'space', 'test')
-- 
GitLab