Tarantool development patches archive
 help / color / mirror / Atom feed
* [PATCH v3 1/1] lua: add key_def lua module
@ 2019-04-22 12:28 Kirill Shcherbatov
  2019-04-24 18:13 ` [tarantool-patches] " Konstantin Osipov
  2019-04-30 10:30 ` Vladimir Davydov
  0 siblings, 2 replies; 7+ messages in thread
From: Kirill Shcherbatov @ 2019-04-22 12:28 UTC (permalink / raw)
  To: tarantool-patches, vdavydov.dev; +Cc: alexander.turenko, Kirill Shcherbatov

Needed for #3276.
Fixes #3398.
Fixes #4025.

@TarantoolBot document
Title: lua: key_def module

It is convenient to have tuple compare function into lua-land for the
following case:
 - exporting key from tuple to iterate over secondary non-unique index
   and delete tuples from space
 - support comparing a tuple with a key / a tuple, support merging
   key_defs from Lua
 - factor out key parts parsing code from the tuples merger

A key_def instance has the following methods:
 - :extract_key(tuple)           -> key (as tuple)
    Receives tuple or Lua table. Returns tuple representing extracted
    key.
 - :compare(tuple_a, tuple_b)    -> number
    Receives tuples or Lua tables.
    Returns:
    - a value > 0 when tuple_a > tuple_b,
    - a value == 0 when tuple_a == tuple_b,
    - a value < 0 otherwise.
 - :compare_with_key(tuple, key) -> number
    - a value > 0 when key(tuple) > key,
    - a value == 0 when key(tuple) == key,
    - a value < 0 otherwise.
 - :merge(another_key_def)       -> new key_def instance
    Constructs a new key definition with a set union of key parts
    from first and second key defs
 - :totable()                    -> table
    Dump key_def object as a Lua table (needed to support __serialize
    method)

The root key_def library exports all instance methods directly.

The format of `parts` parameter in the `key_def.new(parts)` call is
compatible with the following structures:
* box.space[...].index[...].parts;
* net_box_conn.space[...].index[...].parts.

Example for extract_key():

```lua
-- Remove values got from a secondary non-unique index.
local key_def_lib = require('key_def')
local s = box.schema.space.create('test')
local pk = s:create_index('pk')
local sk = s:create_index('test', {unique = false, parts = {
    {2, 'number', path = 'a'}, {2, 'number', path = 'b'}}})
s:insert{1, {a = 1, b = 1}}
s:insert{2, {a = 1, b = 2}}
local key_def = key_def_lib.new(pk.parts)
for _, tuple in sk:pairs({1})) do
    local key = key_def:extract_key(tuple)
    pk:delete(key)
end
```
---
http://github.com/tarantool/tarantool/tree/kshch/gh-4025-lua-key-kef-methods
https://github.com/tarantool/tarantool/issues/4025

 src/CMakeLists.txt            |   1 +
 src/box/CMakeLists.txt        |   2 +
 src/box/errcode.h             |   1 +
 src/box/lua/init.c            |   5 +
 src/box/lua/key_def.c         | 425 ++++++++++++++++++++++++++++++++
 src/box/lua/key_def.h         |  63 +++++
 src/box/lua/key_def.lua       |  19 ++
 src/box/lua/space.cc          |  34 +--
 src/box/tuple.h               |  28 +++
 test/box-tap/key_def.test.lua | 448 ++++++++++++++++++++++++++++++++++
 test/box/misc.result          |   1 +
 11 files changed, 995 insertions(+), 32 deletions(-)
 create mode 100644 src/box/lua/key_def.c
 create mode 100644 src/box/lua/key_def.h
 create mode 100644 src/box/lua/key_def.lua
 create mode 100755 test/box-tap/key_def.test.lua

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 7c2395517..a6a18142b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -136,6 +136,7 @@ set(api_headers
     ${CMAKE_SOURCE_DIR}/src/lua/string.h
     ${CMAKE_SOURCE_DIR}/src/box/txn.h
     ${CMAKE_SOURCE_DIR}/src/box/key_def.h
+    ${CMAKE_SOURCE_DIR}/src/box/lua/key_def.h
     ${CMAKE_SOURCE_DIR}/src/box/field_def.h
     ${CMAKE_SOURCE_DIR}/src/box/tuple.h
     ${CMAKE_SOURCE_DIR}/src/box/tuple_format.h
diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 31600745a..7fbbc7803 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -12,6 +12,7 @@ lua_source(lua_sources lua/net_box.lua)
 lua_source(lua_sources lua/upgrade.lua)
 lua_source(lua_sources lua/console.lua)
 lua_source(lua_sources lua/xlog.lua)
+lua_source(lua_sources lua/key_def.lua)
 set(bin_sources)
 bin_source(bin_sources bootstrap.snap bootstrap.h)
 
@@ -140,6 +141,7 @@ add_library(box STATIC
     lua/net_box.c
     lua/xlog.c
     lua/execute.c
+    lua/key_def.c
     ${bin_sources})
 
 target_link_libraries(box box_error tuple stat xrow xlog vclock crc32 scramble
diff --git a/src/box/errcode.h b/src/box/errcode.h
index 3f8cb8e0e..72485d3c8 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -246,6 +246,7 @@ struct errcode_record {
 	/*191 */_(ER_SQL_PARSER_LIMIT,		"%s %d exceeds the limit (%d)") \
 	/*192 */_(ER_INDEX_DEF_UNSUPPORTED,	"%s are prohibited in an index definition") \
 	/*193 */_(ER_CK_DEF_UNSUPPORTED,	"%s are prohibited in a CHECK constraint definition") \
+	/*194 */_(ER_TUPLE_KEY_PART_MISSED,	"Supplied tuple field for part %u does not exists") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/lua/init.c b/src/box/lua/init.c
index 3ff4f3227..6b8be5096 100644
--- a/src/box/lua/init.c
+++ b/src/box/lua/init.c
@@ -59,9 +59,11 @@
 #include "box/lua/console.h"
 #include "box/lua/tuple.h"
 #include "box/lua/execute.h"
+#include "box/lua/key_def.h"
 
 extern char session_lua[],
 	tuple_lua[],
+	key_def_lua[],
 	schema_lua[],
 	load_cfg_lua[],
 	xlog_lua[],
@@ -80,6 +82,7 @@ static const char *lua_sources[] = {
 	"box/console", console_lua,
 	"box/load_cfg", load_cfg_lua,
 	"box/xlog", xlog_lua,
+	"box/key_def", key_def_lua,
 	NULL
 };
 
@@ -312,6 +315,8 @@ box_lua_init(struct lua_State *L)
 	lua_pop(L, 1);
 	tarantool_lua_console_init(L);
 	lua_pop(L, 1);
+	luaopen_key_def(L);
+	lua_pop(L, 1);
 
 	/* Load Lua extension */
 	for (const char **s = lua_sources; *s; s += 2) {
diff --git a/src/box/lua/key_def.c b/src/box/lua/key_def.c
new file mode 100644
index 000000000..d5fe237c0
--- /dev/null
+++ b/src/box/lua/key_def.c
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "box/coll_id_cache.h"
+#include "box/lua/key_def.h"
+#include "box/tuple.h"
+#include "diag.h"
+#include "fiber.h"
+#include "lua/utils.h"
+#include "misc.h"
+#include "tuple.h"
+
+static uint32_t key_def_type_id = 0;
+
+void
+luaT_push_key_def(struct lua_State *L, const struct key_def *key_def)
+{
+	lua_createtable(L, key_def->part_count, 0);
+	for (uint32_t i = 0; i < key_def->part_count; ++i) {
+		const struct key_part *part = &key_def->parts[i];
+		lua_newtable(L);
+
+		lua_pushstring(L, field_type_strs[part->type]);
+		lua_setfield(L, -2, "type");
+
+		lua_pushnumber(L, part->fieldno + TUPLE_INDEX_BASE);
+		lua_setfield(L, -2, "fieldno");
+
+		if (part->path != NULL) {
+			lua_pushlstring(L, part->path, part->path_len);
+			lua_setfield(L, -2, "path");
+		}
+
+		lua_pushboolean(L, key_part_is_nullable(part));
+		lua_setfield(L, -2, "is_nullable");
+
+		if (part->coll_id != COLL_NONE) {
+			struct coll_id *coll_id = coll_by_id(part->coll_id);
+			assert(coll_id != NULL);
+			lua_pushstring(L, coll_id->name);
+			lua_setfield(L, -2, "collation");
+		}
+		lua_rawseti(L, -2, i + 1);
+	}
+}
+
+/**
+ * Set key_part_def from a table on top of a Lua stack.
+ * The region argument is used to allocate a JSON path when
+ * required.
+ * When successful return 0, otherwise return -1 and set a diag.
+ */
+static int
+luaT_key_def_set_part(struct lua_State *L, struct key_part_def *part,
+		      struct region *region)
+{
+	*part = key_part_def_default;
+
+	/* Set part->fieldno. */
+	lua_pushstring(L, "fieldno");
+	lua_gettable(L, -2);
+	if (lua_isnil(L, -1)) {
+		diag_set(IllegalParams, "fieldno must not be nil");
+		return -1;
+	}
+	/*
+	 * Transform one-based Lua fieldno to zero-based
+	 * fieldno to use in key_def_new().
+	 */
+	part->fieldno = lua_tointeger(L, -1) - TUPLE_INDEX_BASE;
+	lua_pop(L, 1);
+
+	/* Set part->type. */
+	lua_pushstring(L, "type");
+	lua_gettable(L, -2);
+	if (lua_isnil(L, -1)) {
+		diag_set(IllegalParams, "type must not be nil");
+		return -1;
+	}
+	size_t type_len;
+	const char *type_name = lua_tolstring(L, -1, &type_len);
+	lua_pop(L, 1);
+	part->type = field_type_by_name(type_name, type_len);
+	switch (part->type) {
+	case FIELD_TYPE_ANY:
+	case FIELD_TYPE_ARRAY:
+	case FIELD_TYPE_MAP:
+		/* Tuple comparators don't support these types. */
+		diag_set(IllegalParams, "Unsupported field type: %s",
+			 type_name);
+		return -1;
+	case field_type_MAX:
+		diag_set(IllegalParams, "Unknown field type: %s", type_name);
+		return -1;
+	default:
+		/* Pass though. */
+		break;
+	}
+
+	/* Set part->is_nullable and part->nullable_action. */
+	lua_pushstring(L, "is_nullable");
+	lua_gettable(L, -2);
+	if (!lua_isnil(L, -1) && lua_toboolean(L, -1) != 0) {
+		part->is_nullable = true;
+		part->nullable_action = ON_CONFLICT_ACTION_NONE;
+	}
+	lua_pop(L, 1);
+
+	/*
+	 * Set part->coll_id using collation_id.
+	 *
+	 * The value will be checked in key_def_new().
+	 */
+	lua_pushstring(L, "collation_id");
+	lua_gettable(L, -2);
+	if (!lua_isnil(L, -1))
+		part->coll_id = lua_tointeger(L, -1);
+	lua_pop(L, 1);
+
+	/* Set part->coll_id using collation. */
+	lua_pushstring(L, "collation");
+	lua_gettable(L, -2);
+	if (!lua_isnil(L, -1)) {
+		/* Check for conflicting options. */
+		if (part->coll_id != COLL_NONE) {
+			diag_set(IllegalParams, "Conflicting options: "
+				 "collation_id and collation");
+			return -1;
+		}
+
+		size_t coll_name_len;
+		const char *coll_name = lua_tolstring(L, -1, &coll_name_len);
+		struct coll_id *coll_id = coll_by_name(coll_name,
+						       coll_name_len);
+		if (coll_id == NULL) {
+			diag_set(IllegalParams, "Unknown collation: \"%s\"",
+				 coll_name);
+			return -1;
+		}
+		part->coll_id = coll_id->id;
+	}
+	lua_pop(L, 1);
+
+	/* Set part->path (JSON path). */
+	lua_pushstring(L, "path");
+	lua_gettable(L, -2);
+	if (!lua_isnil(L, -1)) {
+		size_t path_len;
+		const char *path = lua_tolstring(L, -1, &path_len);
+		if (json_path_validate(path, path_len, TUPLE_INDEX_BASE) != 0) {
+			diag_set(IllegalParams, "invalid path");
+			return -1;
+		}
+		char *tmp = region_alloc(region, path_len + 1);
+		if (tmp == NULL) {
+			diag_set(OutOfMemory, path_len + 1, "region", "path");
+			return -1;
+		}
+		/*
+		 * lua_tolstring() guarantees that a string have
+		 * trailing '\0'.
+		 */
+		memcpy(tmp, path, path_len + 1);
+		part->path = tmp;
+	} else {
+		part->path = NULL;
+	}
+	lua_pop(L, 1);
+	return 0;
+}
+
+static struct tuple *
+luaT_key_def_check_tuple(struct lua_State *L, struct key_def *key_def, int idx)
+{
+	struct tuple *tuple = luaT_istuple(L, idx);
+	if (tuple == NULL)
+		tuple = luaT_tuple_new(L, idx, box_tuple_format_default());
+	if (tuple == NULL || tuple_validate_parts(key_def, tuple) != 0)
+		return NULL;
+	tuple_ref(tuple);
+	return tuple;
+}
+
+struct key_def *
+luaT_check_key_def(struct lua_State *L, int idx)
+{
+	if (lua_type(L, idx) != LUA_TCDATA)
+		return NULL;
+
+	uint32_t cdata_type;
+	struct key_def **key_def_ptr = luaL_checkcdata(L, idx, &cdata_type);
+	if (key_def_ptr == NULL || cdata_type != key_def_type_id)
+		return NULL;
+	return *key_def_ptr;
+}
+
+/**
+ * Free a key_def from a Lua code.
+ */
+static int
+lbox_key_def_gc(struct lua_State *L)
+{
+	struct key_def *key_def = luaT_check_key_def(L, 1);
+	if (key_def == NULL)
+		return 0;
+	box_key_def_delete(key_def);
+	return 0;
+}
+
+static int
+lbox_key_def_extract_key(struct lua_State *L)
+{
+	struct key_def *key_def;
+	if (lua_gettop(L) != 2 || (key_def = luaT_check_key_def(L, 1)) == NULL)
+		return luaL_error(L, "Usage: key_def:extract_key(tuple)");
+
+	struct tuple *tuple;
+	if ((tuple = luaT_key_def_check_tuple(L, key_def, 2)) == NULL)
+		return luaT_error(L);
+
+	uint32_t key_size;
+	char *key = tuple_extract_key(tuple, key_def, &key_size);
+	tuple_unref(tuple);
+	if (key == NULL)
+		return luaT_error(L);
+
+	struct tuple *ret =
+		box_tuple_new(box_tuple_format_default(), key, key + key_size);
+	if (ret == NULL)
+		return luaT_error(L);
+	luaT_pushtuple(L, ret);
+	return 1;
+}
+
+static int
+lbox_key_def_compare(struct lua_State *L)
+{
+	struct key_def *key_def;
+	if (lua_gettop(L) != 3 ||
+	    (key_def = luaT_check_key_def(L, 1)) == NULL) {
+		return luaL_error(L, "Usage: key_def:"
+				     "compare(tuple_a, tuple_b)");
+	}
+
+	struct tuple *tuple_a, *tuple_b;
+	if ((tuple_a = luaT_key_def_check_tuple(L, key_def, 2)) == NULL)
+		return luaT_error(L);
+	if ((tuple_b = luaT_key_def_check_tuple(L, key_def, 3)) == NULL) {
+		tuple_unref(tuple_a);
+		return luaT_error(L);
+	}
+
+	int rc = tuple_compare(tuple_a, tuple_b, key_def);
+	tuple_unref(tuple_a);
+	tuple_unref(tuple_b);
+	lua_pushinteger(L, rc);
+	return 1;
+}
+
+static int
+lbox_key_def_compare_with_key(struct lua_State *L)
+{
+	struct key_def *key_def;
+	if (lua_gettop(L) != 3 ||
+	    (key_def = luaT_check_key_def(L, 1)) == NULL) {
+		return luaL_error(L, "Usage: key_def:"
+				     "compare_with_key(tuple, key)");
+	}
+
+	struct tuple *tuple = luaT_key_def_check_tuple(L, key_def, 2);
+	if (tuple == NULL)
+		return luaT_error(L);
+
+	size_t key_len;
+	const char *key = lbox_encode_tuple_on_gc(L, 3, &key_len);
+	uint32_t part_count = mp_decode_array(&key);
+	if (key_validate_parts(key_def, key, part_count, true) != 0) {
+		tuple_unref(tuple);
+		return luaT_error(L);
+	}
+
+	int rc = tuple_compare_with_key(tuple, key, part_count, key_def);
+	tuple_unref(tuple);
+	lua_pushinteger(L, rc);
+	return 1;
+}
+
+static int
+lbox_key_def_merge(struct lua_State *L)
+{
+	struct key_def *key_def_a, *key_def_b;
+	if (lua_gettop(L) != 2 ||
+	    (key_def_a = luaT_check_key_def(L, 1)) == NULL ||
+	    (key_def_b = luaT_check_key_def(L, 2)) == NULL)
+		return luaL_error(L, "Usage: key_def:merge(second_key_def)");
+
+	struct key_def *new_key_def = key_def_merge(key_def_a, key_def_b);
+	if (new_key_def == NULL)
+		return luaT_error(L);
+
+	*(struct key_def **) luaL_pushcdata(L, key_def_type_id) = new_key_def;
+	lua_pushcfunction(L, lbox_key_def_gc);
+	luaL_setcdatagc(L, -2);
+	return 1;
+}
+
+static int
+lbox_key_def_to_table(struct lua_State *L)
+{
+	struct key_def *key_def;
+	if (lua_gettop(L) != 1 || (key_def = luaT_check_key_def(L, 1)) == NULL)
+		return luaL_error(L, "Usage: key_def:totable()");
+
+	luaT_push_key_def(L, key_def);
+	return 1;
+}
+
+/**
+ * Create a new key_def from a Lua table.
+ *
+ * Expected a table of key parts on the Lua stack. The format is
+ * the same as box.space.<...>.index.<...>.parts or corresponding
+ * net.box's one.
+ *
+ * Push the new key_def as cdata to a Lua stack.
+ */
+static int
+lbox_key_def_new(struct lua_State *L)
+{
+	if (lua_gettop(L) != 1 || lua_istable(L, 1) != 1)
+		return luaL_error(L, "Bad params, use: key_def.new({"
+				  "{fieldno = fieldno, type = type"
+				  "[, is_nullable = <boolean>]"
+				  "[, path = <string>]"
+				  "[, collation_id = <number>]"
+				  "[, collation = <string>]}, ...}");
+
+	uint32_t part_count = lua_objlen(L, 1);
+	const ssize_t parts_size = sizeof(struct key_part_def) * part_count;
+
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	struct key_part_def *parts = region_alloc(region, parts_size);
+	if (parts == NULL) {
+		diag_set(OutOfMemory, parts_size, "region", "parts");
+		return luaT_error(L);
+	}
+
+	for (uint32_t i = 0; i < part_count; ++i) {
+		lua_pushinteger(L, i + 1);
+		lua_gettable(L, 1);
+		if (luaT_key_def_set_part(L, &parts[i], region) != 0) {
+			region_truncate(region, region_svp);
+			return luaT_error(L);
+		}
+	}
+
+	struct key_def *key_def = key_def_new(parts, part_count);
+	region_truncate(region, region_svp);
+	if (key_def == NULL)
+		return luaT_error(L);
+
+	/*
+	 * Compare and extract key_def methods must work even with
+	 * tuples with omitted (optional) fields. As there is no
+	 * space format which would guarantee certain minimal
+	 * field_count, pass min_field_count = 0 to ensure that
+	 * functions will work correctly in such case.
+	 */
+	key_def_update_optionality(key_def, 0);
+
+	*(struct key_def **) luaL_pushcdata(L, key_def_type_id) = key_def;
+	lua_pushcfunction(L, lbox_key_def_gc);
+	luaL_setcdatagc(L, -2);
+
+	return 1;
+}
+
+LUA_API int
+luaopen_key_def(struct lua_State *L)
+{
+	luaL_cdef(L, "struct key_def;");
+	key_def_type_id = luaL_ctypeid(L, "struct key_def&");
+
+	/* Export C functions to Lua. */
+	static const struct luaL_Reg meta[] = {
+		{"new", lbox_key_def_new},
+		{"extract_key", lbox_key_def_extract_key},
+		{"compare", lbox_key_def_compare},
+		{"compare_with_key", lbox_key_def_compare_with_key},
+		{"merge", lbox_key_def_merge},
+		{"totable", lbox_key_def_to_table},
+		{NULL, NULL}
+	};
+	luaL_register_module(L, "key_def", meta);
+	return 1;
+}
diff --git a/src/box/lua/key_def.h b/src/box/lua/key_def.h
new file mode 100644
index 000000000..2cecfaae5
--- /dev/null
+++ b/src/box/lua/key_def.h
@@ -0,0 +1,63 @@
+#ifndef TARANTOOL_BOX_LUA_KEY_DEF_H_INCLUDED
+#define TARANTOOL_BOX_LUA_KEY_DEF_H_INCLUDED
+/*
+ * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY AUTHORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct lua_State;
+struct key_def;
+
+/**
+ * Push a new table representing a key_def to a Lua stack.
+ */
+void
+luaT_push_key_def(struct lua_State *L, const struct key_def *key_def);
+
+/**
+ * Extract a key_def object from a Lua stack.
+ */
+struct key_def *
+luaT_check_key_def(struct lua_State *L, int idx);
+
+/**
+ * Register the module.
+ */
+int
+luaopen_key_def(struct lua_State *L);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
+
+#endif /* TARANTOOL_BOX_LUA_KEY_DEF_H_INCLUDED */
diff --git a/src/box/lua/key_def.lua b/src/box/lua/key_def.lua
new file mode 100644
index 000000000..586005709
--- /dev/null
+++ b/src/box/lua/key_def.lua
@@ -0,0 +1,19 @@
+local ffi = require('ffi')
+local key_def = require('key_def')
+local key_def_t = ffi.typeof('struct key_def')
+
+local methods = {
+    ['extract_key'] = key_def.extract_key,
+    ['compare'] = key_def.compare,
+    ['compare_with_key'] = key_def.compare_with_key,
+    ['merge'] = key_def.merge,
+    ['totable'] = key_def.totable,
+    ['__serialize'] = key_def.totable,
+}
+
+ffi.metatype(key_def_t, {
+    __index = function(self, key)
+        return methods[key]
+    end,
+    __tostring = function(key_def) return "<struct key_def &>" end,
+})
diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc
index 93269b72b..100da0a79 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -30,6 +30,7 @@
  */
 #include "box/lua/space.h"
 #include "box/lua/tuple.h"
+#include "box/lua/key_def.h"
 #include "box/sql/sqlLimit.h"
 #include "lua/utils.h"
 #include "lua/trigger.h"
@@ -286,38 +287,7 @@ lbox_fillspace(struct lua_State *L, struct space *space, int i)
 		lua_setfield(L, -2, "name");
 
 		lua_pushstring(L, "parts");
-		lua_newtable(L);
-
-		for (uint32_t j = 0; j < index_def->key_def->part_count; j++) {
-			lua_pushnumber(L, j + 1);
-			lua_newtable(L);
-			const struct key_part *part =
-				&index_def->key_def->parts[j];
-
-			lua_pushstring(L, field_type_strs[part->type]);
-			lua_setfield(L, -2, "type");
-
-			lua_pushnumber(L, part->fieldno + TUPLE_INDEX_BASE);
-			lua_setfield(L, -2, "fieldno");
-
-			if (part->path != NULL) {
-				lua_pushlstring(L, part->path, part->path_len);
-				lua_setfield(L, -2, "path");
-			}
-
-			lua_pushboolean(L, key_part_is_nullable(part));
-			lua_setfield(L, -2, "is_nullable");
-
-			if (part->coll_id != COLL_NONE) {
-				struct coll_id *coll_id =
-					coll_by_id(part->coll_id);
-				assert(coll_id != NULL);
-				lua_pushstring(L, coll_id->name);
-				lua_setfield(L, -2, "collation");
-			}
-
-			lua_settable(L, -3); /* index[k].parts[j] */
-		}
+		luaT_push_key_def(L, index_def->key_def);
 
 		lua_settable(L, -3); /* space.index[k].parts */
 
diff --git a/src/box/tuple.h b/src/box/tuple.h
index eed8e1a34..a3ac45574 100644
--- a/src/box/tuple.h
+++ b/src/box/tuple.h
@@ -670,6 +670,34 @@ tuple_field_by_part(struct tuple *tuple, struct key_part *part)
 				       tuple_field_map(tuple), part);
 }
 
+/**
+ * Check that tuple fields match with given key definition
+ * key_def.
+ * @param key_def Key definition.
+ * @param tuple Tuple to validate.
+ *
+ * @retval 0  The tuple is valid.
+ * @retval -1 The tuple is invalid.
+ */
+static inline int
+tuple_validate_parts(struct key_def *key_def, struct tuple *tuple)
+{
+	for (uint32_t idx = 0; idx < key_def->part_count; idx++) {
+		struct key_part *part = &key_def->parts[idx];
+		const char *field = tuple_field_by_part(tuple, part);
+		if (field == NULL) {
+			if (key_part_is_nullable(part))
+				continue;
+			diag_set(ClientError, ER_TUPLE_KEY_PART_MISSED, idx);
+			return -1;
+		}
+		if (key_part_validate(part->type, field, idx,
+				      key_part_is_nullable(part)) != 0)
+			return -1;
+	}
+	return 0;
+}
+
 /**
  * @brief Tuple Interator
  */
diff --git a/test/box-tap/key_def.test.lua b/test/box-tap/key_def.test.lua
new file mode 100755
index 000000000..5fc9cc67b
--- /dev/null
+++ b/test/box-tap/key_def.test.lua
@@ -0,0 +1,448 @@
+#!/usr/bin/env tarantool
+
+local tap = require('tap')
+local ffi = require('ffi')
+local json = require('json')
+local fun = require('fun')
+local key_def_lib = require('key_def')
+
+local usage_error = 'Bad params, use: key_def.new({' ..
+                    '{fieldno = fieldno, type = type' ..
+                    '[, is_nullable = <boolean>]' ..
+                    '[, path = <string>]' ..
+                    '[, collation_id = <number>]' ..
+                    '[, collation = <string>]}, ...}'
+
+local function coll_not_found(fieldno, collation)
+    if type(collation) == 'number' then
+        return ('Wrong index options (field %d): ' ..
+               'collation was not found by ID'):format(fieldno)
+    end
+
+    return ('Unknown collation: "%s"'):format(collation)
+end
+
+local function set_key_part_defaults(parts)
+    local res = {}
+    for i, part in ipairs(parts) do
+        res[i] = table.copy(part)
+        if res[i].is_nullable == nil then
+            res[i].is_nullable = false
+        end
+    end
+    return res
+end
+
+local key_def_new_cases = {
+    -- Cases to call before box.cfg{}.
+    {
+        'Pass a field on an unknown type',
+        parts = {{
+            fieldno = 2,
+            type = 'unknown',
+        }},
+        exp_err = 'Unknown field type: unknown',
+    },
+    {
+        'Try to use collation_id before box.cfg{}',
+        parts = {{
+            fieldno = 1,
+            type = 'string',
+            collation_id = 2,
+        }},
+        exp_err = coll_not_found(1, 2),
+    },
+    {
+        'Try to use collation before box.cfg{}',
+        parts = {{
+            fieldno = 1,
+            type = 'string',
+            collation = 'unicode_ci',
+        }},
+        exp_err = coll_not_found(1, 'unicode_ci'),
+    },
+    function()
+        -- For collations.
+        box.cfg{}
+    end,
+    -- Cases to call after box.cfg{}.
+    {
+        'Try to use both collation_id and collation',
+        parts = {{
+            fieldno = 1,
+            type = 'string',
+            collation_id = 2,
+            collation = 'unicode_ci',
+        }},
+        exp_err = 'Conflicting options: collation_id and collation',
+    },
+    {
+        'Unknown collation_id',
+        parts = {{
+            fieldno = 1,
+            type = 'string',
+            collation_id = 999,
+        }},
+        exp_err = coll_not_found(1, 999),
+    },
+    {
+        'Unknown collation name',
+        parts = {{
+            fieldno = 1,
+            type = 'string',
+            collation = 'unknown',
+        }},
+        exp_err = 'Unknown collation: "unknown"',
+    },
+    {
+        'Bad parts parameter type',
+        parts = 1,
+        exp_err = usage_error,
+    },
+    {
+        'No parameters',
+        params = {},
+        exp_err = usage_error,
+    },
+    {
+        'Two parameters',
+        params = {{}, {}},
+        exp_err = usage_error,
+    },
+    {
+        'Invalid JSON path',
+        parts = {{
+            fieldno = 1,
+            type = 'string',
+            path = '[3[',
+        }},
+        exp_err = 'invalid path',
+    },
+    {
+        'Success case; zero parts',
+        parts = {},
+        exp_err = nil,
+    },
+    {
+        'Success case; one part',
+        parts = {{
+            fieldno = 1,
+            type = 'string',
+        }},
+        exp_err = nil,
+    },
+    {
+        'Success case; one part with a JSON path',
+        parts = {{
+            fieldno = 1,
+            type = 'string',
+            path = '[3]',
+        }},
+        exp_err = nil,
+    },
+}
+
+local test = tap.test('key_def')
+
+test:plan(#key_def_new_cases - 1 + 7)
+for _, case in ipairs(key_def_new_cases) do
+    if type(case) == 'function' then
+        case()
+    else
+        local ok, res
+        if case.params then
+            ok, res = pcall(key_def_lib.new, unpack(case.params))
+        else
+            ok, res = pcall(key_def_lib.new, case.parts)
+        end
+        if case.exp_err == nil then
+            ok = ok and type(res) == 'cdata' and
+                ffi.istype('struct key_def', res)
+            test:ok(ok, case[1])
+        else
+            local err = tostring(res) -- cdata -> string
+            test:is_deeply({ok, err}, {false, case.exp_err}, case[1])
+        end
+    end
+end
+
+-- Prepare source data for test cases.
+
+-- Case: extract_key().
+test:test('extract_key()', function(test)
+    test:plan(13)
+
+    local key_def_a = key_def_lib.new({
+        {type = 'unsigned', fieldno = 1},
+    })
+    local key_def_b = key_def_lib.new({
+        {type = 'number', fieldno = 2},
+        {type = 'number', fieldno = 3},
+    })
+    local key_def_c = key_def_lib.new({
+        {type = 'scalar', fieldno = 2},
+        {type = 'scalar', fieldno = 1},
+        {type = 'string', fieldno = 4, is_nullable = true},
+    })
+    local tuple_a = box.tuple.new({1, 1, 22})
+
+    test:is_deeply(key_def_a:extract_key(tuple_a):totable(), {1}, 'case 1')
+    test:is_deeply(key_def_b:extract_key(tuple_a):totable(), {1, 22}, 'case 2')
+
+    -- JSON path.
+    local res = key_def_lib.new({
+        {type = 'string', fieldno = 1, path = 'a.b'},
+    }):extract_key(box.tuple.new({{a = {b = 'foo'}}})):totable()
+    test:is_deeply(res, {'foo'}, 'JSON path (tuple argument)')
+
+    local res = key_def_lib.new({
+        {type = 'string', fieldno = 1, path = 'a.b'},
+    }):extract_key({{a = {b = 'foo'}}}):totable()
+    test:is_deeply(res, {'foo'}, 'JSON path (table argument)')
+
+    -- A key def has a **nullable** part with a field that is over
+    -- a tuple size.
+    --
+    -- The key def options are:
+    --
+    -- * is_nullable = true;
+    -- * has_optional_parts = true.
+    test:is_deeply(key_def_c:extract_key(tuple_a):totable(), {1, 1, box.NULL},
+        'short tuple with a nullable part')
+
+    -- A key def has a **non-nullable** part with a field that is
+    -- over a tuple size.
+    --
+    -- The key def options are:
+    --
+    -- * is_nullable = false;
+    -- * has_optional_parts = false.
+    local exp_err = 'Supplied tuple field for part 1 does not exists'
+    local key_def = key_def_lib.new({
+        {type = 'string', fieldno = 1},
+        {type = 'string', fieldno = 2},
+    })
+    local ok, err = pcall(key_def.extract_key, key_def,
+        box.tuple.new({'foo'}))
+    test:is_deeply({ok, tostring(err)}, {false, exp_err},
+        'short tuple with a non-nullable part (case 1)')
+
+    -- Same as before, but a max fieldno is over tuple:len() + 1.
+    local exp_err = 'Supplied tuple field for part 1 does not exists'
+    local key_def = key_def_lib.new({
+        {type = 'string', fieldno = 1},
+        {type = 'string', fieldno = 2},
+        {type = 'string', fieldno = 3},
+    })
+    local ok, err = pcall(key_def.extract_key, key_def,
+        box.tuple.new({'foo'}))
+    test:is_deeply({ok, tostring(err)}, {false, exp_err},
+        'short tuple with a non-nullable part (case 2)')
+
+    -- Same as before, but with another key def options:
+    --
+    -- * is_nullable = true;
+    -- * has_optional_parts = false.
+    local exp_err = 'Supplied tuple field for part 1 does not exists'
+    local key_def = key_def_lib.new({
+        {type = 'string', fieldno = 1, is_nullable = true},
+        {type = 'string', fieldno = 2},
+    })
+    local ok, err = pcall(key_def.extract_key, key_def,
+        box.tuple.new({'foo'}))
+    test:is_deeply({ok, tostring(err)}, {false, exp_err},
+        'short tuple with a non-nullable part (case 3)')
+
+    -- A tuple has a field that does not match corresponding key
+    -- part type.
+    local exp_err = 'Supplied key type of part 2 does not match index ' ..
+                    'part type: expected string'
+    local key_def = key_def_lib.new({
+        {type = 'string', fieldno = 1},
+        {type = 'string', fieldno = 2},
+        {type = 'string', fieldno = 3},
+    })
+    local ok, err = pcall(key_def.extract_key, key_def, {'one', 'two', 3})
+    test:is_deeply({ok, tostring(err)}, {false, exp_err},
+        'wrong field type')
+
+    local key_def = key_def_lib.new({
+        {type = 'number', fieldno = 1, path='a'},
+        {type = 'number', fieldno = 1, path='b'},
+        {type = 'number', fieldno = 1, path='c', is_nullable=true},
+        {type = 'number', fieldno = 3, is_nullable=true},
+    })
+    local ok, err = pcall(key_def.extract_key, key_def,
+                          box.tuple.new({1, 1, 22}))
+    test:is_deeply({ok, tostring(err)},
+                   {false, 'Supplied tuple field for part 0 does not exists'},
+                   'invalid JSON structure')
+    test:is_deeply(key_def:extract_key({{a=1, b=2}, 1}):totable(),
+                   {1, 2, box.NULL, box.NULL},
+                   'tuple with optional parts - case 1')
+    test:is_deeply(key_def:extract_key({{a=1, b=2, c=3}, 1}):totable(),
+                   {1, 2, 3, box.NULL},
+                   'tuple with optional parts - case 2')
+    test:is_deeply(key_def:extract_key({{a=1, b=2}, 1, 3}):totable(),
+                   {1, 2, box.NULL, 3},
+                   'tuple with optional parts - case 3')
+end)
+
+-- Case: compare().
+test:test('compare()', function(test)
+    test:plan(8)
+
+    local key_def_a = key_def_lib.new({
+        {type = 'unsigned', fieldno = 1},
+    })
+    local key_def_b = key_def_lib.new({
+        {type = 'number', fieldno = 2},
+        {type = 'number', fieldno = 3},
+    })
+    local tuple_a = box.tuple.new({1, 1, 22})
+    local tuple_b = box.tuple.new({2, 1, 11})
+    local tuple_c = box.tuple.new({3, 1, 22})
+
+    test:is(key_def_a:compare(tuple_b, tuple_a), 1,
+            'case 1: great (tuple argument)')
+    test:is(key_def_a:compare(tuple_b, tuple_c), -1,
+            'case 2: less (tuple argument)')
+    test:is(key_def_b:compare(tuple_b, tuple_a), -1,
+            'case 3: less (tuple argument)')
+    test:is(key_def_b:compare(tuple_a, tuple_c), 0,
+            'case 4: equal (tuple argument)')
+
+    test:is(key_def_a:compare(tuple_b:totable(), tuple_a:totable()), 1,
+            'case 1: great (table argument)')
+    test:is(key_def_a:compare(tuple_b:totable(), tuple_c:totable()), -1,
+            'case 2: less (table argument)')
+    test:is(key_def_b:compare(tuple_b:totable(), tuple_a:totable()), -1,
+            'case 3: less (table argument)')
+    test:is(key_def_b:compare(tuple_a:totable(), tuple_c:totable()), 0,
+            'case 4: equal (table argument)')
+end)
+
+-- Case: compare_with_key().
+test:test('compare_with_key()', function(test)
+    test:plan(2)
+
+    local key_def_b = key_def_lib.new({
+        {type = 'number', fieldno = 2},
+        {type = 'number', fieldno = 3},
+    })
+    local tuple_a = box.tuple.new({1, 1, 22})
+
+    local key = {1, 22}
+    test:is(key_def_b:compare_with_key(tuple_a:totable(), key), 0, 'table')
+
+    local key = box.tuple.new({1, 22})
+    test:is(key_def_b:compare_with_key(tuple_a, key), 0, 'tuple')
+end)
+
+-- Case: totable().
+test:test('totable()', function(test)
+    test:plan(2)
+
+    local parts_a = {
+        {type = 'unsigned', fieldno = 1}
+    }
+    local parts_b = {
+        {type = 'number', fieldno = 2},
+        {type = 'number', fieldno = 3},
+    }
+    local key_def_a = key_def_lib.new(parts_a)
+    local key_def_b = key_def_lib.new(parts_b)
+
+    local exp = set_key_part_defaults(parts_a)
+    test:is_deeply(key_def_a:totable(), exp, 'case 1')
+
+    local exp = set_key_part_defaults(parts_b)
+    test:is_deeply(key_def_b:totable(), exp, 'case 2')
+end)
+
+-- Case: __serialize().
+test:test('__serialize()', function(test)
+    test:plan(2)
+
+    local parts_a = {
+        {type = 'unsigned', fieldno = 1}
+    }
+    local parts_b = {
+        {type = 'number', fieldno = 2},
+        {type = 'number', fieldno = 3},
+    }
+    local key_def_a = key_def_lib.new(parts_a)
+    local key_def_b = key_def_lib.new(parts_b)
+
+    local exp = set_key_part_defaults(parts_a)
+    test:is(json.encode(key_def_a), json.encode(exp), 'case 1')
+
+    local exp = set_key_part_defaults(parts_b)
+    test:is(json.encode(key_def_b), json.encode(exp), 'case 2')
+end)
+
+-- Case: tostring().
+test:test('tostring()', function(test)
+    test:plan(2)
+
+    local parts_a = {
+        {type = 'unsigned', fieldno = 1}
+    }
+    local parts_b = {
+        {type = 'number', fieldno = 2},
+        {type = 'number', fieldno = 3},
+    }
+    local key_def_a = key_def_lib.new(parts_a)
+    local key_def_b = key_def_lib.new(parts_b)
+
+    local exp = '<struct key_def &>'
+    test:is(tostring(key_def_a), exp, 'case 1')
+    test:is(tostring(key_def_b), exp, 'case 2')
+end)
+
+-- Case: merge().
+test:test('merge()', function(test)
+    test:plan(6)
+
+    local key_def_a = key_def_lib.new({
+        {type = 'unsigned', fieldno = 1},
+    })
+    local key_def_b = key_def_lib.new({
+        {type = 'number', fieldno = 2},
+        {type = 'number', fieldno = 3},
+    })
+    local key_def_c = key_def_lib.new({
+        {type = 'scalar', fieldno = 2},
+        {type = 'scalar', fieldno = 1},
+        {type = 'string', fieldno = 4, is_nullable = true},
+    })
+    local tuple_a = box.tuple.new({1, 1, 22})
+
+    local key_def_ab = key_def_a:merge(key_def_b)
+    local exp_parts = fun.iter(key_def_a:totable())
+        :chain(fun.iter(key_def_b:totable())):totable()
+    test:is_deeply(key_def_ab:totable(), exp_parts,
+        'case 1: verify with :totable()')
+    test:is_deeply(key_def_ab:extract_key(tuple_a):totable(), {1, 1, 22},
+        'case 1: verify with :extract_key()')
+
+    local key_def_ba = key_def_b:merge(key_def_a)
+    local exp_parts = fun.iter(key_def_b:totable())
+        :chain(fun.iter(key_def_a:totable())):totable()
+    test:is_deeply(key_def_ba:totable(), exp_parts,
+        'case 2: verify with :totable()')
+    test:is_deeply(key_def_ba:extract_key(tuple_a):totable(), {1, 22, 1},
+        'case 2: verify with :extract_key()')
+
+    -- Intersecting parts + NULL parts.
+    local key_def_cb = key_def_c:merge(key_def_b)
+    local exp_parts = key_def_c:totable()
+    exp_parts[#exp_parts + 1] = {type = 'number', fieldno = 3,
+        is_nullable = false}
+    test:is_deeply(key_def_cb:totable(), exp_parts,
+        'case 3: verify with :totable()')
+    test:is_deeply(key_def_cb:extract_key(tuple_a):totable(),
+        {1, 1, box.NULL, 22}, 'case 3: verify with :extract_key()')
+end)
+
+os.exit(test:check() and 0 or 1)
diff --git a/test/box/misc.result b/test/box/misc.result
index a1f7a0990..7d6d54241 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -522,6 +522,7 @@ t;
   191: box.error.SQL_PARSER_LIMIT
   192: box.error.INDEX_DEF_UNSUPPORTED
   193: box.error.CK_DEF_UNSUPPORTED
+  194: box.error.TUPLE_KEY_PART_MISSED
 ...
 test_run:cmd("setopt delimiter ''");
 ---
-- 
2.21.0

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2019-05-06 16:25 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-04-22 12:28 [PATCH v3 1/1] lua: add key_def lua module Kirill Shcherbatov
2019-04-24 18:13 ` [tarantool-patches] " Konstantin Osipov
2019-04-25  8:42   ` [tarantool-patches] " Alexander Turenko
2019-04-30 10:30 ` Vladimir Davydov
2019-05-06 15:27   ` [tarantool-patches] " Kirill Shcherbatov
2019-05-06 15:35     ` Alexander Turenko
2019-05-06 16:25     ` Vladimir Davydov

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox