[PATCH v3 4/4] box: introduce functional indexes in memtx

Kirill Shcherbatov kshcherbatov at tarantool.org
Wed Jul 17 04:20:45 MSK 2019


Closes #1260

@TarantoolBot document
Title: introduce functional indexes in memtx
Now you can define a functional index using registered persistent
function as key extractor.

There is some restrictions for function and key definition for
functional index:
 - referenced extractor function must be persistent, deterministic
   and return scalar type or array.
 - key parts describe the referenced routine's returned key
   (when the routine returned value type is array in meaning of
   multikey index key): all parts must be sequential and the
   first part's fieldno must be 1.
 - key parts can't use JSON paths.
 - you are not allowed to access tuple fields by names in functional
   index extractor

Functional index can't be primary.
You are not allowed to change functional index extractor function
while there are some functional indexes depends of it.

Example:
s = box.schema.space.create('withdata')
s:format({{name = 'name', type = 'string'},
          {name = 'address', type = 'string'}})
pk = s:create_index('name', {parts = {1, 'string'}})
lua_code = [[function(tuple)
                local address = string.split(tuple[2])
                local ret = {}
                for _, v in pairs(address) do
			table.insert(ret, {utf8.upper(v)})
		end
                return unpack(ret)
             end]]
box.schema.func.create('addr_extractor', {body = lua_code,
                       is_deterministic = true, is_sandboxed = true})
idx = s:create_index('addr', {unique = false, functional_fid =
                     box.func.addr_extractor.id, is_multikey = true,
                     parts = {{1, 'string'}}})
s:insert({"James", "SIS Building Lambeth London UK"})
s:insert({"Sherlock", "221B Baker St Marylebone London NW1 6XE UK"})
idx:select('UK')
---
- - ['James', 'SIS Building Lambeth London UK']
  - ['Sherlock', '221B Baker St Marylebone London NW1 6XE UK']
...
---
 src/box/CMakeLists.txt             |   4 +-
 src/box/alter.cc                   | 134 +++++-
 src/box/alter.h                    |   1 +
 src/box/bootstrap.snap             | Bin 5863 -> 5914 bytes
 src/box/box.cc                     |   4 +-
 src/box/call.c                     |   8 +-
 src/box/errcode.h                  |   1 +
 src/box/functional_extractor.c     | 118 +++++
 src/box/functional_extractor.h     | 101 ++++
 src/box/functional_key.c           | 228 ++++++++++
 src/box/functional_key.h           |  94 ++++
 src/box/index.cc                   |  28 ++
 src/box/index.h                    |  38 +-
 src/box/index_def.c                |   9 +
 src/box/index_def.h                |   9 +-
 src/box/key_def.c                  |  33 +-
 src/box/key_def.h                  |  26 +-
 src/box/lua/key_def.c              |   2 +-
 src/box/lua/misc.cc                |   2 +-
 src/box/lua/schema.lua             |  20 +
 src/box/lua/space.cc               |  15 +
 src/box/lua/upgrade.lua            |  18 +
 src/box/memtx_engine.c             |   4 +
 src/box/memtx_space.c              |  18 +
 src/box/memtx_tree.c               |  41 +-
 src/box/port.h                     |   4 +-
 src/box/schema.cc                  |  13 +-
 src/box/schema_def.h               |   9 +
 src/box/sql.c                      |   3 +-
 src/box/sql/build.c                |   2 +-
 src/box/sql/select.c               |   3 +-
 src/box/sql/where.c                |   2 +-
 src/box/tuple.c                    |  25 +-
 src/box/tuple.h                    |  64 ++-
 src/box/tuple_compare.cc           | 206 ++++++---
 src/box/tuple_extract_key.cc       |   9 +-
 src/box/tuple_format.c             |  58 ++-
 src/box/tuple_format.h             |  17 +
 src/box/tuple_hash.cc              |   1 +
 src/box/vinyl.c                    |  11 +-
 test/app-tap/tarantoolctl.test.lua |   4 +-
 test/box-py/bootstrap.result       |   5 +
 test/box/access.result             |   3 +
 test/box/access.test.lua           |   1 +
 test/box/access_misc.result        | 132 +++---
 test/box/access_sysview.result     |   6 +-
 test/box/alter.result              |   7 +-
 test/box/bitset.result             |  24 +
 test/box/bitset.test.lua           |   9 +
 test/box/hash.result               |  24 +
 test/box/hash.test.lua             |   9 +
 test/box/misc.result               |   1 +
 test/box/rtree_misc.result         |  24 +
 test/box/rtree_misc.test.lua       |   9 +
 test/engine/engine.cfg             |   5 +-
 test/engine/functional.result      | 708 +++++++++++++++++++++++++++++
 test/engine/functional.test.lua    | 240 ++++++++++
 test/unit/luaT_tuple_new.c         |   2 +-
 test/unit/merger.test.c            |   8 +-
 test/unit/tuple_bigref.c           |   2 +-
 test/unit/vy_iterators_helper.c    |   2 +-
 test/vinyl/misc.result             |  23 +
 test/vinyl/misc.test.lua           |   9 +
 test/wal_off/alter.result          |   2 +-
 64 files changed, 2468 insertions(+), 174 deletions(-)
 create mode 100644 src/box/functional_extractor.c
 create mode 100644 src/box/functional_extractor.h
 create mode 100644 src/box/functional_key.c
 create mode 100644 src/box/functional_key.h
 create mode 100644 test/engine/functional.result
 create mode 100644 test/engine/functional.test.lua

diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 481842a39..ddc8440a6 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -51,8 +51,9 @@ add_library(tuple STATIC
     coll_id_cache.c
     field_def.c
     opt_def.c
+    functional_extractor.c
 )
-target_link_libraries(tuple json box_error core ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES} misc bit)
+target_link_libraries(tuple json box_error core coll ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES} misc bit)
 
 add_library(xlog STATIC xlog.c)
 target_link_libraries(xlog core box_error crc32 ${ZSTD_LIBRARIES})
@@ -125,6 +126,7 @@ add_library(box STATIC
     wal.c
     call.c
     merger.c
+    functional_key.c
     ${lua_sources}
     lua/init.c
     lua/call.c
diff --git a/src/box/alter.cc b/src/box/alter.cc
index 1dbfe6b26..fdf38598e 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -30,6 +30,7 @@
  */
 #include "alter.h"
 #include "assoc.h"
+#include "box.h"
 #include "ck_constraint.h"
 #include "column_mask.h"
 #include "schema.h"
@@ -38,6 +39,7 @@
 #include "index.h"
 #include "fk_constraint.h"
 #include "func.h"
+#include "functional_extractor.h"
 #include "coll_id_cache.h"
 #include "coll_id_def.h"
 #include "txn.h"
@@ -235,6 +237,21 @@ index_opts_decode(struct index_opts *opts, const char *map,
 			  "bloom_fpr must be greater than 0 and "
 			  "less than or equal to 1");
 	}
+	/**
+	 * Can't verify functional index extractor routine
+	 * reference on load because the function object
+	 * had not been registered in Tarantool yet.
+	 */
+	if (strcmp(box_status(), "loading") != 0 && opts->functional_fid > 0) {
+		struct func *func = func_cache_find(opts->functional_fid);
+		if (func->def->language != FUNC_LANGUAGE_LUA ||
+		    func->def->body == NULL || !func->def->is_deterministic ||
+		    !func->def->is_sandboxed) {
+			tnt_raise(ClientError, ER_WRONG_INDEX_OPTIONS, 0,
+				"referenced function doesn't satisfy "
+				"functional index constraints");
+		}
+	}
 }
 
 /**
@@ -291,7 +308,8 @@ index_def_new_from_tuple(struct tuple *tuple, struct space *space)
 				 space->def->fields,
 				 space->def->field_count, &fiber()->gc) != 0)
 		diag_raise();
-	key_def = key_def_new(part_def, part_count);
+	key_def = key_def_new(part_def, part_count, opts.functional_fid > 0,
+			      opts.is_multikey);
 	if (key_def == NULL)
 		diag_raise();
 	struct index_def *index_def =
@@ -1376,6 +1394,60 @@ RebuildIndex::~RebuildIndex()
 		index_def_delete(new_index_def);
 }
 
+/**
+ * RebuildFunctionalIndex - prepare functional index definition,
+ * drop the old index data and rebuild index from by reading the
+ * primary key.
+ */
+class RebuildFunctionalIndex: public RebuildIndex
+{
+	struct index_def *
+	functional_index_def_new(struct index_def *index_def,
+				 struct func *func)
+	{
+		struct index_def *new_index_def = index_def_dup_xc(index_def);
+		old_extractor = index_def->key_def->functional_extractor;
+		if (func != NULL) {
+			new_extractor = functional_extractor_new(func);
+			if (new_extractor == NULL)
+				diag_raise();
+		} else {
+			new_extractor = NULL;
+		}
+		new_index_def->key_def->functional_extractor = new_extractor;
+		new_index_def->cmp_def->functional_extractor = new_extractor;
+		return new_index_def;
+	}
+	/** New functional extractor. */
+	struct functional_extractor *new_extractor;
+	/** Old functional extractor. */
+	struct functional_extractor *old_extractor;
+public:
+	RebuildFunctionalIndex(struct alter_space *alter,
+			       struct index_def *old_index_def_arg,
+			       struct func *func) :
+		RebuildIndex(alter,
+			     functional_index_def_new(old_index_def_arg, func),
+			     old_index_def_arg) {}
+	virtual void commit(struct alter_space *alter, int64_t signature);
+	virtual ~RebuildFunctionalIndex();
+};
+
+void
+RebuildFunctionalIndex::commit(struct alter_space *alter, int64_t signature)
+{
+	new_extractor = NULL;
+	if (old_extractor != NULL)
+		functional_extractor_delete(old_extractor);
+	RebuildIndex::commit(alter, signature);
+}
+
+RebuildFunctionalIndex::~RebuildFunctionalIndex()
+{
+	if (new_extractor != NULL)
+		functional_extractor_delete(new_extractor);
+}
+
 /** TruncateIndex - truncate an index. */
 class TruncateIndex: public AlterSpaceOp
 {
@@ -2847,6 +2919,12 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 				  (unsigned) old_func->def->uid,
 				  "function has grants");
 		}
+		if (old_func != NULL &&
+		    space_has_data(BOX_FUNC_INDEX_ID, 1, old_func->def->fid)) {
+			tnt_raise(ClientError, ER_DROP_FUNCTION,
+				  (unsigned) old_func->def->uid,
+				  "function has references");
+		}
 		struct trigger *on_commit =
 			txn_alter_trigger_new(on_drop_func_commit, old_func);
 		struct trigger *on_rollback =
@@ -4695,6 +4773,56 @@ on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event)
 	trigger_run_xc(&on_alter_space, space);
 }
 
+/** A trigger invoked on replace in the _func_index space. */
+static void
+on_replace_dd_func_index(struct trigger *trigger, void *event)
+{
+	(void) trigger;
+	struct txn *txn = (struct txn *) event;
+	struct txn_stmt *stmt = txn_current_stmt(txn);
+	struct tuple *old_tuple = stmt->old_tuple;
+	struct tuple *new_tuple = stmt->new_tuple;
+
+	struct alter_space *alter = NULL;
+	struct func *func = NULL;
+	struct index *index;
+	struct space *space;
+	if (old_tuple == NULL && new_tuple != NULL) {
+		uint32_t space_id = tuple_field_u32_xc(new_tuple,
+					BOX_FUNC_INDEX_FIELD_SPACE_ID);
+		uint32_t index_id = tuple_field_u32_xc(new_tuple,
+					BOX_FUNC_INDEX_FIELD_INDEX_ID);
+		uint32_t fid = tuple_field_u32_xc(new_tuple,
+					BOX_FUNC_INDEX_FUNCTION_ID);
+		space = space_cache_find_xc(space_id);
+		index = index_find_xc(space, index_id);
+		func = func_cache_find(fid);
+	} else if (old_tuple != NULL && new_tuple == NULL) {
+		uint32_t space_id = tuple_field_u32_xc(old_tuple,
+					BOX_FUNC_INDEX_FIELD_SPACE_ID);
+		uint32_t index_id = tuple_field_u32_xc(old_tuple,
+					BOX_FUNC_INDEX_FIELD_INDEX_ID);
+		space = space_cache_find_xc(space_id);
+		index = index_find_xc(space, index_id);
+		func = NULL;
+	} else {
+		assert(old_tuple != NULL && new_tuple != NULL);
+		tnt_raise(ClientError, ER_UNSUPPORTED, "func_index", "alter");
+	}
+
+	alter = alter_space_new(space);
+	auto scoped_guard = make_scoped_guard([=] {alter_space_delete(alter);});
+	alter_space_move_indexes(alter, 0, index->def->iid);
+	(void) new RebuildFunctionalIndex(alter, index->def, func);
+	alter_space_move_indexes(alter, index->def->iid + 1,
+				 space->index_id_max + 1);
+	(void) new MoveCkConstraints(alter);
+	(void) new UpdateSchemaVersion(alter);
+	alter_space_do(txn, alter);
+
+	scoped_guard.is_active = false;
+}
+
 struct trigger alter_space_on_replace_space = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_space, NULL, NULL
 };
@@ -4755,4 +4883,8 @@ struct trigger on_replace_ck_constraint = {
 	RLIST_LINK_INITIALIZER, on_replace_dd_ck_constraint, NULL, NULL
 };
 
+struct trigger on_replace_func_index = {
+	RLIST_LINK_INITIALIZER, on_replace_dd_func_index, NULL, NULL
+};
+
 /* vim: set foldmethod=marker */
diff --git a/src/box/alter.h b/src/box/alter.h
index c339ccea6..1bc837359 100644
--- a/src/box/alter.h
+++ b/src/box/alter.h
@@ -47,5 +47,6 @@ extern struct trigger on_replace_space_sequence;
 extern struct trigger on_replace_trigger;
 extern struct trigger on_replace_fk_constraint;
 extern struct trigger on_replace_ck_constraint;
+extern struct trigger on_replace_func_index;
 
 #endif /* INCLUDES_TARANTOOL_BOX_ALTER_H */
diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index ae698731e480e0d8cc250659222032291922e549..a52256b65c113a2055f0a8f65f9b90398d3816d5 100644
diff --git a/src/box/box.cc b/src/box/box.cc
index 80249919e..9e6f2da62 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -74,6 +74,7 @@
 #include "call.h"
 #include "func.h"
 #include "sequence.h"
+#include "functional_key.h"
 
 static char status[64] = "unknown";
 
@@ -2068,7 +2069,8 @@ box_init(void)
 	if (module_init() != 0)
 		diag_raise();
 
-	if (tuple_init(lua_hash) != 0)
+	if (tuple_init(lua_hash, functional_key_get,
+		       functional_key_get_raw) != 0)
 		diag_raise();
 
 	sequence_init();
diff --git a/src/box/call.c b/src/box/call.c
index ac2bf3004..d2a89ced3 100644
--- a/src/box/call.c
+++ b/src/box/call.c
@@ -46,13 +46,15 @@
 static const struct port_vtab port_msgpack_vtab;
 
 void
-port_msgpack_create(struct port *base, const char *data, uint32_t data_sz)
+port_msgpack_create(struct port *base, const char *data, uint32_t data_sz,
+		    bool is_single_tuple)
 {
 	struct port_msgpack *port_msgpack = (struct port_msgpack *) base;
 	memset(port_msgpack, 0, sizeof(*port_msgpack));
 	port_msgpack->vtab = &port_msgpack_vtab;
 	port_msgpack->data = data;
 	port_msgpack->data_sz = data_sz;
+	port_msgpack->is_single_tuple = is_single_tuple;
 }
 
 static const char *
@@ -113,7 +115,7 @@ box_process_call(struct call_request *request, struct port *port)
 	int rc;
 	struct port args;
 	port_msgpack_create(&args, request->args,
-			    request->args_end - request->args);
+			    request->args_end - request->args, false);
 	struct func *func = func_by_name(name, name_len);
 	if (func != NULL) {
 		rc = func_call(func, &args, port);
@@ -149,7 +151,7 @@ box_process_eval(struct call_request *request, struct port *port)
 		return -1;
 	struct port args;
 	port_msgpack_create(&args, request->args,
-			    request->args_end - request->args);
+			    request->args_end - request->args, false);
 	const char *expr = request->expr;
 	uint32_t expr_len = mp_decode_strl(&expr);
 	struct txn *txn;
diff --git a/src/box/errcode.h b/src/box/errcode.h
index 361ad3a45..87001f80d 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -250,6 +250,7 @@ struct errcode_record {
 	/*195 */_(ER_CREATE_CK_CONSTRAINT,	"Failed to create check constraint '%s': %s") \
 	/*196 */_(ER_CK_CONSTRAINT_FAILED,	"Check constraint failed '%s': %s") \
 	/*197 */_(ER_SQL_COLUMN_COUNT,		"Unequal number of entries in row expression: left side has %u, but right side - %u") \
+	/*198 */_(ER_FUNCTIONAL_EXTRACTOR,	"Functional index extractor '%s' error: %s") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/functional_extractor.c b/src/box/functional_extractor.c
new file mode 100644
index 000000000..ff69aa063
--- /dev/null
+++ b/src/box/functional_extractor.c
@@ -0,0 +1,118 @@
+/*
+ * 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 "functional_extractor.h"
+#include "func.h"
+#include "func_def.h"
+#include "tuple.h"
+
+/** Map: (struct tuple *) => (struct tuple_extra  *). */
+#define MH_SOURCE 1
+#define mh_name _tuple_extra
+#define mh_key_t struct tuple *
+#define mh_node_t struct tuple_extra *
+#define mh_arg_t uint32_t
+#define mh_hash(a, arg) ((uintptr_t)(*(a))->tuple)
+#define mh_hash_key(a, arg) ((uintptr_t) a)
+#define mh_cmp(a, b, arg) ((*(a))->tuple != (*(b))->tuple || \
+			   (*(a))->chunk_id != (*(b))->chunk_id)
+#define mh_cmp_key(a, b, arg) ((a) != (*(b))->tuple || (arg) != (*(b))->chunk_id)
+#include "salad/mhash.h"
+
+int
+functional_extractor_hash_register(struct functional_extractor *extractor,
+				   struct tuple_extra *tuple_extra)
+{
+	mh_int_t id = mh_tuple_extra_put(extractor->hash,
+			(const struct tuple_extra **)&tuple_extra, NULL, 0);
+	if (id == mh_end(extractor->hash))
+		return -1;
+	return 0;
+}
+
+void
+functional_extractor_hash_unregister(struct functional_extractor *extractor,
+				     struct tuple_extra *tuple_extra)
+{
+	mh_int_t id = mh_tuple_extra_find(extractor->hash, tuple_extra->tuple,
+					  tuple_extra->chunk_id);
+	assert(id != mh_end(extractor->hash));
+	mh_tuple_extra_del(extractor->hash, id, tuple_extra->chunk_id);
+}
+
+struct tuple_extra *
+functional_extractor_hash_lookup(struct functional_extractor *extractor,
+				 struct tuple *tuple)
+{
+	mh_int_t id = mh_tuple_extra_find(extractor->hash, tuple,
+					  extractor->func->def->fid);
+	if (id == mh_end(extractor->hash))
+		return NULL;
+	struct tuple_extra **tuple_extra_ptr =
+		mh_tuple_extra_node(extractor->hash, id);
+	assert(tuple_extra_ptr != NULL && *tuple_extra_ptr != NULL);
+	return *tuple_extra_ptr;
+}
+
+struct functional_extractor *
+functional_extractor_new(struct func *func)
+{
+	struct functional_extractor *extractor = malloc(sizeof(*extractor));
+	if (extractor == NULL) {
+		diag_set(OutOfMemory, sizeof(*extractor), "malloc",
+			 "extractor");
+		return NULL;
+	}
+	extractor->func = func;
+	extractor->hash = mh_tuple_extra_new();
+	if (extractor->hash == NULL) {
+		diag_set(OutOfMemory, sizeof(*extractor->hash),
+			 "malloc", "hash");
+		free(extractor);
+		return NULL;
+	}
+	return extractor;
+}
+
+void
+functional_extractor_delete(struct functional_extractor *extractor)
+{
+	while (mh_size(extractor->hash) > 0) {
+		mh_int_t i = mh_first(extractor->hash);
+		struct tuple_extra **tuple_extra =
+			mh_tuple_extra_node(extractor->hash, i);
+		assert(tuple_extra != NULL && *tuple_extra != NULL);
+		mh_tuple_extra_del(extractor->hash, i, 0);
+		tuple_extra_delete(*tuple_extra);
+	}
+	mh_tuple_extra_delete(extractor->hash);
+	TRASH(extractor);
+	free(extractor);
+}
diff --git a/src/box/functional_extractor.h b/src/box/functional_extractor.h
new file mode 100644
index 000000000..1496ef864
--- /dev/null
+++ b/src/box/functional_extractor.h
@@ -0,0 +1,101 @@
+#ifndef TARANTOOL_BOX_FUNCTIONAL_EXTRACTOR_H_INCLUDED
+#define TARANTOOL_BOX_FUNCTIONAL_EXTRACTOR_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 <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.
+ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "inttypes.h"
+#include <stdio.h>
+
+struct func;
+struct tuple;
+struct tuple_extra;
+struct mh_tuple_extra_t;
+
+/**
+ * Functional extractor is a proxy object to tune functional index
+ * extractor calls.
+ */
+struct functional_extractor {
+	/** A pointer to functional index extractor routine. */
+	struct func *func;
+	/**
+	 * Hash of extracted and registered functional index
+	 * keys: <tuple *, functional_fid> -> tuple_extra *
+	 */
+	struct mh_tuple_extra_t *hash;
+};
+
+/**
+ * Register a new functional index key represented as
+ * engine-dependent tuple_extra memory chunk.
+ */
+int
+functional_extractor_hash_register(struct functional_extractor *extractor,
+				   struct tuple_extra *tuple_extra);
+
+/**
+ * Unregister a given functional index key represented as
+ * engine-dependent tuple_extra.
+ */
+void
+functional_extractor_hash_unregister(struct functional_extractor *extractor,
+				     struct tuple_extra *tuple_extra);
+
+/**
+ * Lookup for a registered functional index key represented as a
+ * engine-dependent tuple_extra.
+ */
+struct tuple_extra *
+functional_extractor_hash_lookup(struct functional_extractor *extractor,
+				 struct tuple *tuple);
+
+/**
+ * Create a new functional index extractor object for
+ * given function.
+ */
+struct functional_extractor *
+functional_extractor_new(struct func *func);
+
+/**
+ * Release a given functional index extractor object and free
+ * associated memory.
+ */
+void
+functional_extractor_delete(struct functional_extractor *extractor);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TARANTOOL_BOX_FUNCTIONAL_EXTRACTOR_H_INCLUDED */
diff --git a/src/box/functional_key.c b/src/box/functional_key.c
new file mode 100644
index 000000000..23ea5dd5f
--- /dev/null
+++ b/src/box/functional_key.c
@@ -0,0 +1,228 @@
+/*
+ * 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 "functional_key.h"
+#include "functional_extractor.h"
+#include "fiber.h"
+#include "func.h"
+#include "port.h"
+#include "stdbool.h"
+#include "tuple.h"
+
+/** Calculate the size of the functional key map. */
+static inline uint32_t
+functional_key_map_sz(uint32_t key_count)
+{
+	return sizeof(uint32_t) * key_count;
+}
+
+/**
+ * Execute a given functional index extractor function and
+ * return an extracted key_data and key_data_sz.
+ */
+static const char *
+functional_key_extract(struct func *func, struct port *in_port,
+		       uint32_t *key_data_sz)
+{
+	struct port out_port;
+	int rc = func_call(func, in_port, &out_port);
+	if (rc != 0)
+		goto error;
+	const char *key_data = port_get_msgpack(&out_port, key_data_sz);
+	port_destroy(&out_port);
+	if (key_data == NULL)
+		goto error;
+
+	return key_data;
+error:
+	diag_set(ClientError, ER_FUNCTIONAL_EXTRACTOR, func->def->name,
+		 diag_last_error(diag_get())->errmsg);
+	return NULL;
+}
+
+/**
+ * Process a given data and initialize a key_map allocation.
+ * Perform key validation if validate == true.
+ */
+static int
+functional_key_map_create(struct func *func, struct key_def *key_def,
+			  const char *data, uint32_t key_count,
+			  uint32_t *key_map)
+{
+	const char *key = data;
+	for (uint32_t key_idx = 0; key_idx < key_count; key_idx++) {
+		if (key_map != NULL)
+			key_map[key_idx] = key - data;
+		if (mp_typeof(*key) != MP_ARRAY) {
+			diag_set(ClientError, ER_FUNCTIONAL_EXTRACTOR,
+				 func->def->name,
+				 "returned key type is invalid");
+			return -1;
+		}
+		const char *key_end;
+		uint32_t part_count = mp_decode_array(&key);
+		if (key_def->functional_part_count != part_count) {
+			const char *error_msg =
+				tt_sprintf(tnt_errcode_desc(ER_EXACT_MATCH),
+					   key_def->functional_part_count,
+					   part_count);
+			diag_set(ClientError, ER_FUNCTIONAL_EXTRACTOR,
+				 func->def->name, error_msg);
+			return -1;
+		}
+		if (key_validate_parts(key_def, key, part_count, true,
+				       &key_end) != 0) {
+			diag_set(ClientError, ER_FUNCTIONAL_EXTRACTOR,
+				 func->def->name,
+				 diag_last_error(diag_get())->errmsg);
+			return -1;
+		}
+		key = key_end;
+	}
+	if (key_map != NULL)
+		key_map[0] = key_count;
+	return 0;
+}
+
+const char *
+functional_key_get_raw(const char *tuple, const char *tuple_end,
+		       struct key_def *key_def, uint32_t *key_count)
+{
+	struct functional_extractor *extractor = key_def->functional_extractor;
+	assert(extractor != NULL);
+	struct func *func = extractor->func;
+	/*
+	 * Evaluate functional index extractor function with
+	 * given tuple data.
+	 */
+	struct port in_port;
+	port_msgpack_create(&in_port, tuple, tuple_end - tuple, true);
+	uint32_t key_data_sz;
+	const char *key_data =
+		functional_key_extract(func, &in_port, &key_data_sz);
+	if (key_data == NULL)
+		return NULL;
+
+	*key_count = mp_decode_array(&key_data);
+	if (!key_def->is_multikey && *key_count > 1) {
+		diag_set(ClientError, ER_FUNCTIONAL_EXTRACTOR,
+			 func->def->name, "to many keys were returned");
+		return NULL;
+	}
+	if (functional_key_map_create(func, key_def, key_data, *key_count,
+				      NULL) != 0)
+		return NULL;
+	return key_data;
+}
+
+struct tuple_extra *
+functional_key_new(struct tuple *tuple, struct key_def *key_def)
+{
+	struct functional_extractor *extractor = key_def->functional_extractor;
+	assert(extractor != NULL);
+	struct func *func = extractor->func;
+
+	struct port in_port;
+	port_tuple_create(&in_port);
+	port_tuple_add(&in_port, tuple);
+	uint32_t key_data_sz;
+	const char *key_data =
+		functional_key_extract(func, &in_port, &key_data_sz);
+	port_destroy(&in_port);
+	if (key_data == NULL)
+		return NULL;
+
+	/*
+	 * Test whether extractor function returned only one
+	 * value in case on non-multikey index.
+	 */
+	uint32_t key_count = mp_decode_array(&key_data);
+	if (!key_def->is_multikey && key_count > 1) {
+		diag_set(ClientError, ER_FUNCTIONAL_EXTRACTOR,
+			 func->def->name, "to many keys were returned");
+		return NULL;
+	}
+
+	uint32_t key_map_sz = functional_key_map_sz(key_count);
+	key_data_sz -= mp_sizeof_array(key_count);
+	struct tuple_extra *tuple_extra =
+		tuple_extra_new(tuple, func->def->fid, key_data_sz + key_map_sz);
+	if (tuple_extra == NULL)
+		return NULL;
+
+	memcpy(tuple_extra->data + key_map_sz, key_data, key_data_sz);
+	uint32_t *key_map = (uint32_t *) tuple_extra->data;
+	key_data = tuple_extra->data + key_map_sz;
+
+	/** Prepare functional multikey key map and validate. */
+	if (functional_key_map_create(func, key_def, key_data, key_count,
+				      key_map) != 0)
+		goto error;
+	if (functional_extractor_hash_register(extractor, tuple_extra) != 0)
+		goto error;
+
+	return tuple_extra;
+error:
+	tuple_extra_delete(tuple_extra);
+	return NULL;
+}
+
+void
+functional_key_delete(struct tuple *tuple, struct key_def *key_def)
+{
+	struct functional_extractor *extractor = key_def->functional_extractor;
+	assert(extractor != NULL);
+
+	struct tuple_extra *tuple_extra =
+			functional_extractor_hash_lookup(extractor, tuple);
+	if (tuple_extra == NULL)
+		return;
+	functional_extractor_hash_unregister(extractor, tuple_extra);
+	tuple_extra_delete(tuple_extra);
+}
+
+const char *
+functional_key_get(struct tuple *tuple, struct key_def *key_def,
+		   uint32_t *key_count, uint32_t **key_map)
+{
+	struct functional_extractor *extractor = key_def->functional_extractor;
+	assert(extractor != NULL);
+
+	struct tuple_extra *tuple_extra =
+		functional_extractor_hash_lookup(extractor, tuple);
+	if (unlikely(tuple_extra == NULL)) {
+		tuple_extra = functional_key_new(tuple, key_def);
+		if (tuple_extra == NULL)
+			return NULL;
+	}
+	*key_map = (uint32_t *) tuple_extra->data;
+	*key_count = (*key_map)[0];
+	return tuple_extra->data + *key_count * sizeof(uint32_t);
+}
diff --git a/src/box/functional_key.h b/src/box/functional_key.h
new file mode 100644
index 000000000..5d8a78812
--- /dev/null
+++ b/src/box/functional_key.h
@@ -0,0 +1,94 @@
+#ifndef TARANTOOL_BOX_FUNCTIONAL_KEY_H_INCLUDED
+#define TARANTOOL_BOX_FUNCTIONAL_KEY_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 <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.
+ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "inttypes.h"
+#include "stdbool.h"
+
+struct tuple;
+struct key_def;
+struct tuple_extra;
+
+/**
+ * Evaluate functional index extractor with give tuple, validate
+ * result, alloc and register engine-dependent tuple_extra
+ * extention representing functional key;
+ *
+ *       4b         4b           4b
+ * +-----------+-----------+-----------+------+-----+------+
+ * | key_count |    ...    |keyN_offset| key1 | ... | keyN |
+ * +-----------+-----------+-----------+------+-----+------+
+ *                         |________________________^
+ * <----------------------------------->
+ *  (sizeof(uint32_t) * key_count) bytes
+ *
+ * Returns not NULL tuple_extra pointer in case of success and
+ * NULL otherwise. The diag message is set.
+ */
+struct tuple_extra *
+functional_key_new(struct tuple *tuple, struct key_def *key_def);
+
+/**
+ * Unregister and destroy a functional key associated with given
+ * tuple and key definition.
+ */
+void
+functional_key_delete(struct tuple *tuple, struct key_def *key_def);
+
+/**
+ * Get functional index key by given tuple and functional
+ * key defintion. Function is expected a functional key to be
+ * registered in functional extractor cache; if not so, does it.
+ *
+ * Returns not NULL functional index key pointer or NULL.
+ */
+const char *
+functional_key_get(struct tuple *tuple, struct key_def *key_def,
+		   uint32_t *key_count, uint32_t **key_map);
+
+/**
+ * Get functional index key by given tuple data and functional
+ * key defintion.
+ * Returns not NULL functional index key pointer or NULL.
+ */
+const char *
+functional_key_get_raw(const char *tuple, const char *tuple_end,
+		       struct key_def *key_def, uint32_t *key_count);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TARANTOOL_BOX_FUNCTIONAL_KEY_H_INCLUDED */
diff --git a/src/box/index.cc b/src/box/index.cc
index 843d0e73d..00a1b502e 100644
--- a/src/box/index.cc
+++ b/src/box/index.cc
@@ -679,6 +679,16 @@ generic_index_replace(struct index *index, struct tuple *old_tuple,
 	return -1;
 }
 
+struct iterator *
+generic_index_create_iterator(struct index *base, enum iterator_type type,
+			      const char *key, uint32_t part_count)
+{
+	(void) type; (void) key; (void) part_count;
+	diag_set(UnsupportedIndexFeature, base->def, "read view");
+	return NULL;
+}
+
+
 struct snapshot_iterator *
 generic_index_create_snapshot_iterator(struct index *index)
 {
@@ -729,4 +739,22 @@ generic_index_end_build(struct index *)
 {
 }
 
+int
+disabled_index_build_next(struct index *index, struct tuple *tuple)
+{
+	(void) index; (void) tuple;
+	return 0;
+}
+
+int
+disabled_index_replace(struct index *index, struct tuple *old_tuple,
+		       struct tuple *new_tuple, enum dup_replace_mode mode,
+		       struct tuple **result)
+{
+	(void) old_tuple; (void) new_tuple; (void) mode;
+	(void) index;
+	*result = NULL;
+	return 0;
+}
+
 /* }}} */
diff --git a/src/box/index.h b/src/box/index.h
index 97d600c96..75cf1eda6 100644
--- a/src/box/index.h
+++ b/src/box/index.h
@@ -33,6 +33,9 @@
 #include <stdbool.h>
 #include "trivia/util.h"
 #include "iterator_type.h"
+#include "tuple.h"
+#include "functional_extractor.h"
+#include "functional_key.h"
 #include "index_def.h"
 
 #if defined(__cplusplus)
@@ -595,12 +598,35 @@ index_get(struct index *index, const char *key,
 	return index->vtab->get(index, key, part_count, result);
 }
 
+static inline int
+index_functional_replace(struct index *index, struct tuple *old_tuple,
+			 struct tuple *new_tuple, enum dup_replace_mode mode,
+			 struct tuple **result)
+{
+	struct key_def *key_def = index->def->key_def;
+	if (new_tuple != NULL && functional_key_new(new_tuple, key_def) == NULL)
+		return -1;
+	int rc = index->vtab->replace(index, old_tuple, new_tuple, mode,
+				      result);
+	if (rc == 0 && old_tuple != NULL)
+		functional_key_delete(old_tuple, key_def);
+	else if (rc != 0 && new_tuple != NULL)
+		functional_key_delete(new_tuple, key_def);
+	return rc;
+}
+
 static inline int
 index_replace(struct index *index, struct tuple *old_tuple,
 	      struct tuple *new_tuple, enum dup_replace_mode mode,
 	      struct tuple **result)
 {
-	return index->vtab->replace(index, old_tuple, new_tuple, mode, result);
+	if (index->def->key_def->functional_extractor != NULL) {
+		return index_functional_replace(index, old_tuple, new_tuple,
+						mode, result);
+	} else {
+		return index->vtab->replace(index, old_tuple, new_tuple, mode,
+					    result);
+	}
 }
 
 static inline struct iterator *
@@ -685,9 +711,19 @@ void generic_index_compact(struct index *);
 void generic_index_reset_stat(struct index *);
 void generic_index_begin_build(struct index *);
 int generic_index_reserve(struct index *, uint32_t);
+struct iterator *
+generic_index_create_iterator(struct index *base, enum iterator_type type,
+			      const char *key, uint32_t part_count);
 int generic_index_build_next(struct index *, struct tuple *);
 void generic_index_end_build(struct index *);
 
+int
+disabled_index_build_next(struct index *index, struct tuple *tuple);
+int
+disabled_index_replace(struct index *index, struct tuple *old_tuple,
+		       struct tuple *new_tuple, enum dup_replace_mode mode,
+		       struct tuple **result);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 
diff --git a/src/box/index_def.c b/src/box/index_def.c
index eb309a30c..82479d2cb 100644
--- a/src/box/index_def.c
+++ b/src/box/index_def.c
@@ -50,6 +50,8 @@ const struct index_opts index_opts_default = {
 	/* .bloom_fpr           = */ 0.05,
 	/* .lsn                 = */ 0,
 	/* .stat                = */ NULL,
+	/* .functional_fid      = */ 0,
+	/* .is_multikey         = */ false,
 };
 
 const struct opt_def index_opts_reg[] = {
@@ -63,6 +65,8 @@ const struct opt_def index_opts_reg[] = {
 	OPT_DEF("run_size_ratio", OPT_FLOAT, struct index_opts, run_size_ratio),
 	OPT_DEF("bloom_fpr", OPT_FLOAT, struct index_opts, bloom_fpr),
 	OPT_DEF("lsn", OPT_INT64, struct index_opts, lsn),
+	OPT_DEF("functional_fid", OPT_UINT32, struct index_opts, functional_fid),
+	OPT_DEF("is_multikey", OPT_BOOL, struct index_opts, is_multikey),
 	OPT_DEF_LEGACY("sql"),
 	OPT_END,
 };
@@ -296,6 +300,11 @@ index_def_is_valid(struct index_def *index_def, const char *space_name)
 			 space_name, "primary key cannot be multikey");
 		return false;
 	}
+	if (index_def->iid == 0 && key_def_is_functional(index_def->key_def)) {
+		diag_set(ClientError, ER_MODIFY_INDEX, index_def->name,
+			space_name, "primary key cannot be functional");
+		return false;
+	}
 	for (uint32_t i = 0; i < index_def->key_def->part_count; i++) {
 		assert(index_def->key_def->parts[i].type < field_type_MAX);
 		if (index_def->key_def->parts[i].fieldno > BOX_INDEX_FIELD_MAX) {
diff --git a/src/box/index_def.h b/src/box/index_def.h
index 6dac28377..ab7429c50 100644
--- a/src/box/index_def.h
+++ b/src/box/index_def.h
@@ -163,6 +163,10 @@ struct index_opts {
 	 * filled after running ANALYZE command.
 	 */
 	struct index_stat *stat;
+	/** Identifier of the functional index extractor. */
+	uint32_t functional_fid;
+	/** Whether functional index extractor is multikey. */
+	bool is_multikey;
 };
 
 extern const struct index_opts index_opts_default;
@@ -207,6 +211,10 @@ index_opts_cmp(const struct index_opts *o1, const struct index_opts *o2)
 		return o1->run_size_ratio < o2->run_size_ratio ? -1 : 1;
 	if (o1->bloom_fpr != o2->bloom_fpr)
 		return o1->bloom_fpr < o2->bloom_fpr ? -1 : 1;
+	if (o1->functional_fid != o2->functional_fid)
+		return o1->functional_fid - o2->functional_fid;
+	if (o1->is_multikey != o2->is_multikey)
+		return o1->is_multikey - o2->is_multikey;
 	return 0;
 }
 
@@ -297,7 +305,6 @@ index_def_update_optionality(struct index_def *def, uint32_t min_field_count)
 	key_def_update_optionality(def->key_def, min_field_count);
 	key_def_update_optionality(def->cmp_def, min_field_count);
 }
-
 /**
  * Add an index definition to a list, preserving the
  * first position of the primary key.
diff --git a/src/box/key_def.c b/src/box/key_def.c
index ee758eefa..bfbca6b30 100644
--- a/src/box/key_def.c
+++ b/src/box/key_def.c
@@ -241,7 +241,8 @@ key_def_set_part(struct key_def *def, uint32_t part_no, uint32_t fieldno,
 }
 
 struct key_def *
-key_def_new(const struct key_part_def *parts, uint32_t part_count)
+key_def_new(const struct key_part_def *parts, uint32_t part_count,
+	    bool is_functional, bool is_multikey)
 {
 	size_t sz = 0;
 	for (uint32_t i = 0; i < part_count; i++)
@@ -255,7 +256,6 @@ key_def_new(const struct key_part_def *parts, uint32_t part_count)
 
 	def->part_count = part_count;
 	def->unique_part_count = part_count;
-
 	/* A pointer to the JSON paths data in the new key_def. */
 	char *path_pool = (char *)def + key_def_sizeof(part_count, 0);
 	for (uint32_t i = 0; i < part_count; i++) {
@@ -266,8 +266,7 @@ key_def_new(const struct key_part_def *parts, uint32_t part_count)
 			if (coll_id == NULL) {
 				diag_set(ClientError, ER_WRONG_INDEX_OPTIONS,
 					 i + 1, "collation was not found by ID");
-				key_def_delete(def);
-				return NULL;
+				goto error;
 			}
 			coll = coll_id->coll;
 		}
@@ -276,14 +275,25 @@ key_def_new(const struct key_part_def *parts, uint32_t part_count)
 				     part->nullable_action, coll, part->coll_id,
 				     part->sort_order, part->path, path_len,
 				     &path_pool, TUPLE_OFFSET_SLOT_NIL,
-				     0) != 0) {
-			key_def_delete(def);
-			return NULL;
+				     0) != 0)
+			goto error;
+	}
+	if (is_functional) {
+		def->functional_part_count = part_count;
+		def->is_multikey = is_multikey;
+		if (!key_def_is_sequential(def) || parts->fieldno != 0 ||
+		    def->has_json_paths) {
+			diag_set(ClientError, ER_WRONG_INDEX_OPTIONS, 0,
+				 "invalid functional key definition");
+			goto error;
 		}
 	}
 	assert(path_pool == (char *)def + sz);
 	key_def_set_func(def);
 	return def;
+error:
+	key_def_delete(def);
+	return NULL;
 }
 
 int
@@ -677,6 +687,7 @@ key_def_find_by_fieldno(const struct key_def *key_def, uint32_t fieldno)
 const struct key_part *
 key_def_find(const struct key_def *key_def, const struct key_part *to_find)
 {
+	assert(!key_def_is_functional(key_def));
 	const struct key_part *part = key_def->parts;
 	const struct key_part *end = part + key_def->part_count;
 	for (; part != end; part++) {
@@ -708,6 +719,9 @@ static bool
 key_def_can_merge(const struct key_def *key_def,
 		  const struct key_part *to_merge)
 {
+	if (key_def_is_functional(key_def))
+		return true;
+
 	const struct key_part *part = key_def_find(key_def, to_merge);
 	if (part == NULL)
 		return true;
@@ -722,6 +736,7 @@ key_def_can_merge(const struct key_def *key_def,
 struct key_def *
 key_def_merge(const struct key_def *first, const struct key_def *second)
 {
+	assert(!key_def_is_functional(second));
 	uint32_t new_part_count = first->part_count + second->part_count;
 	/*
 	 * Find and remove part duplicates, i.e. parts counted
@@ -754,6 +769,8 @@ key_def_merge(const struct key_def *first, const struct key_def *second)
 	new_def->has_optional_parts = first->has_optional_parts ||
 				      second->has_optional_parts;
 	new_def->is_multikey = first->is_multikey || second->is_multikey;
+	new_def->functional_part_count = first->functional_part_count;
+	new_def->functional_extractor = first->functional_extractor;
 
 	/* JSON paths data in the new key_def. */
 	char *path_pool = (char *)new_def + key_def_sizeof(new_part_count, 0);
@@ -826,7 +843,7 @@ key_def_find_pk_in_cmp_def(const struct key_def *cmp_def,
 	}
 
 	/* Finally, allocate the new key definition. */
-	extracted_def = key_def_new(parts, pk_def->part_count);
+	extracted_def = key_def_new(parts, pk_def->part_count, false, false);
 out:
 	region_truncate(region, region_svp);
 	return extracted_def;
diff --git a/src/box/key_def.h b/src/box/key_def.h
index df83d055c..d163d1380 100644
--- a/src/box/key_def.h
+++ b/src/box/key_def.h
@@ -192,6 +192,13 @@ struct key_def {
 	 * unique_part_count == part count of a merged key_def.
 	 */
 	uint32_t unique_part_count;
+	/**
+	 * Count of parts in functional index defintion.
+	 * All functional_part_count key_part(s) of an
+	 * initialized key def instance have func != NULL pointer.
+	 * != 0 iff it is functional index definition.
+	*/
+	uint32_t functional_part_count;
 	/** True, if at least one part can store NULL. */
 	bool is_nullable;
 	/** True if some key part has JSON path. */
@@ -205,6 +212,12 @@ struct key_def {
 	bool has_optional_parts;
 	/** Key fields mask. @sa column_mask.h for details. */
 	uint64_t column_mask;
+	/**
+	 * The pointer to a functional index extractor.
+	 * != NULL for initialized functional index key definition.
+	 * Key definition object doesn't manage this reference.
+	 */
+	struct functional_extractor *functional_extractor;
 	/**
 	 * In case of the multikey index, a pointer to the
 	 * JSON path string, the path to the root node of
@@ -330,7 +343,8 @@ key_def_sizeof(uint32_t part_count, uint32_t path_pool_size)
  * and initialize its parts.
  */
 struct key_def *
-key_def_new(const struct key_part_def *parts, uint32_t part_count);
+key_def_new(const struct key_part_def *parts, uint32_t part_count,
+	    bool is_functional, bool is_multikey);
 
 /**
  * Dump part definitions of the given key def.
@@ -341,6 +355,16 @@ int
 key_def_dump_parts(const struct key_def *def, struct key_part_def *parts,
 		   struct region *region);
 
+/**
+ * Return true if a given key definition defines functional index
+ * key.
+ */
+static inline bool
+key_def_is_functional(const struct key_def *key_def)
+{
+	return key_def->functional_part_count > 0;
+}
+
 /**
  * Update 'has_optional_parts' of @a key_def with correspondence
  * to @a min_field_count.
diff --git a/src/box/lua/key_def.c b/src/box/lua/key_def.c
index 041b5ec98..e572fec1b 100644
--- a/src/box/lua/key_def.c
+++ b/src/box/lua/key_def.c
@@ -445,7 +445,7 @@ lbox_key_def_new(struct lua_State *L)
 		lua_pop(L, 1);
 	}
 
-	struct key_def *key_def = key_def_new(parts, part_count);
+	struct key_def *key_def = key_def_new(parts, part_count, false, false);
 	region_truncate(region, region_svp);
 	if (key_def == NULL)
 		return luaT_error(L);
diff --git a/src/box/lua/misc.cc b/src/box/lua/misc.cc
index 7b8b9dcad..74cff84fd 100644
--- a/src/box/lua/misc.cc
+++ b/src/box/lua/misc.cc
@@ -86,7 +86,7 @@ port_msgpack_dump_lua(struct port *base, struct lua_State *L, bool is_flat)
 	struct port_msgpack *port = (struct port_msgpack *) base;
 
 	const char *args = port->data;
-	uint32_t arg_count = mp_decode_array(&args);
+	uint32_t arg_count = port->is_single_tuple ? 1 : mp_decode_array(&args);
 	for (uint32_t i = 0; i < arg_count; i++)
 		luamp_decode(L, luaL_msgpack_default, &args);
 }
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index aadcd3fa9..20c96b6e5 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -515,6 +515,7 @@ box.schema.space.drop = function(space_id, space_name, opts)
     local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
     local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID]
     local _ck_constraint = box.space[box.schema.CK_CONSTRAINT_ID]
+    local _func_index = box.space[box.schema.FUNC_INDEX_ID]
     local sequence_tuple = _space_sequence:delete{space_id}
     if sequence_tuple ~= nil and sequence_tuple.is_generated == true then
         -- Delete automatically generated sequence.
@@ -529,6 +530,9 @@ box.schema.space.drop = function(space_id, space_name, opts)
     for _, t in _ck_constraint.index.primary:pairs({space_id}) do
         _ck_constraint:delete({space_id, t.name})
     end
+    for _, t in _func_index.index.primary:pairs({space_id}) do
+        _func_index:delete({space_id, t.index_id, t.func_id})
+    end
     local keys = _vindex:select(space_id)
     for i = #keys, 1, -1 do
         local v = keys[i]
@@ -956,6 +960,8 @@ local index_options = {
     range_size = 'number',
     page_size = 'number',
     bloom_fpr = 'number',
+    functional_fid = 'number',
+    is_multikey = 'boolean',
 }
 
 --
@@ -1061,6 +1067,8 @@ box.schema.index.create = function(space_id, name, options)
             run_count_per_level = options.run_count_per_level,
             run_size_ratio = options.run_size_ratio,
             bloom_fpr = options.bloom_fpr,
+            functional_fid = options.functional_fid,
+            is_multikey = options.is_multikey,
     }
     local field_type_aliases = {
         num = 'unsigned'; -- Deprecated since 1.7.2
@@ -1086,6 +1094,10 @@ box.schema.index.create = function(space_id, name, options)
                                                         space.name, name)
     _index:insert{space_id, iid, name, options.type, index_opts, parts}
     space_sequence_alter_commit(sequence_proxy)
+    if (options.functional_fid ~= nil and options.functional_fid > 0) then
+        local _func_index = box.space[box.schema.FUNC_INDEX_ID]
+        _func_index:insert{space_id, iid, options.functional_fid}
+    end
     return space.index[name]
 end
 
@@ -1101,6 +1113,10 @@ box.schema.index.drop = function(space_id, index_id)
         end
     end
     local _index = box.space[box.schema.INDEX_ID]
+    local _func_index = box.space[box.schema.FUNC_INDEX_ID]
+    for _, v in box.space._func_index:pairs{space_id, index_id} do
+        _func_index:delete(v)
+    end
     _index:delete{space_id, index_id}
 end
 
@@ -1202,6 +1218,10 @@ box.schema.index.alter = function(space_id, index_id, options)
                                                         space.name, options.name)
     _index:replace{space_id, index_id, options.name, options.type,
                    index_opts, parts}
+    if (options.functional_fid ~= nil and options.functional_fid > 0) then
+        local _func_index = box.space[box.schema.FUNC_INDEX_ID]
+        _func_index:insert{space_id, iid, options.functional_fid}
+    end
     space_sequence_alter_commit(sequence_proxy)
 end
 
diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc
index 18039fd6a..c13ffe2a4 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -335,6 +335,19 @@ lbox_fillspace(struct lua_State *L, struct space *space, int i)
 			lua_setfield(L, -2, "dimension");
 		}
 
+		if (index_opts->functional_fid > 0) {
+			lua_pushstring(L, "functional");
+			lua_newtable(L);
+
+			lua_pushnumber(L, index_opts->functional_fid);
+			lua_setfield(L, -2, "fid");
+
+			lua_pushboolean(L, index_opts->is_multikey);
+			lua_setfield(L, -2, "is_multikey");
+
+			lua_settable(L, -3);
+		}
+
 		lua_pushstring(L, index_type_strs[index_def->type]);
 		lua_setfield(L, -2, "type");
 
@@ -629,6 +642,8 @@ box_lua_space_init(struct lua_State *L)
 	lua_setfield(L, -2, "VSEQUENCE_ID");
 	lua_pushnumber(L, BOX_SPACE_SEQUENCE_ID);
 	lua_setfield(L, -2, "SPACE_SEQUENCE_ID");
+	lua_pushnumber(L, BOX_FUNC_INDEX_ID);
+	lua_setfield(L, -2, "FUNC_INDEX_ID");
 	lua_pushnumber(L, BOX_SYSTEM_ID_MIN);
 	lua_setfield(L, -2, "SYSTEM_ID_MIN");
 	lua_pushnumber(L, BOX_SYSTEM_ID_MAX);
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index 02c1cb0ff..f570a1c08 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -885,11 +885,29 @@ local function upgrade_func_to_2_2_1()
                                       collation = 'unicode_ci'}}})
 end
 
+local function create_func_index()
+    log.info("Create _func_index space")
+    local _func_index = box.space[box.schema.FUNC_INDEX_ID]
+    local _space = box.space._space
+    local _index = box.space._index
+    local format = {{name='space_id', type='unsigned'},
+                    {name='index_id', type='unsigned'},
+                    {name='func_id',  type='unsigned'}}
+    _space:insert{_func_index.id, ADMIN, '_func_index', 'memtx', 0,
+                  setmap({}), format}
+    _index:insert{_func_index.id, 0, 'primary', 'tree', {unique = true},
+                  {{0, 'unsigned'}, {1, 'unsigned'}, {2, 'unsigned'}}}
+    _index:insert{_func_index.id, 1, 'fid', 'tree', {unique = false},
+                  {{2, 'unsigned'}}}
+
+end
+
 local function upgrade_to_2_2_1()
     upgrade_sequence_to_2_2_1()
     upgrade_ck_constraint_to_2_2_1()
     create_vcollation_space()
     upgrade_func_to_2_2_1()
+    create_func_index()
 end
 
 --------------------------------------------------------------------------------
diff --git a/src/box/memtx_engine.c b/src/box/memtx_engine.c
index 49efcc192..883c79ff7 100644
--- a/src/box/memtx_engine.c
+++ b/src/box/memtx_engine.c
@@ -1250,6 +1250,10 @@ memtx_index_def_change_requires_rebuild(struct index *index,
 		return true;
 	if (!old_def->opts.is_unique && new_def->opts.is_unique)
 		return true;
+	if (old_def->opts.is_multikey != new_def->opts.is_multikey)
+		return true;
+	if (old_def->opts.functional_fid != new_def->opts.functional_fid)
+		return true;
 
 	const struct key_def *old_cmp_def, *new_cmp_def;
 	if (index_depends_on_pk(index)) {
diff --git a/src/box/memtx_space.c b/src/box/memtx_space.c
index a8e7b8080..216ef3912 100644
--- a/src/box/memtx_space.c
+++ b/src/box/memtx_space.c
@@ -659,6 +659,12 @@ memtx_space_check_index_def(struct space *space, struct index_def *index_def)
 				 "HASH index cannot be multikey");
 			return -1;
 		}
+		if (key_def_is_functional(index_def->key_def)) {
+			diag_set(ClientError, ER_MODIFY_INDEX,
+				 index_def->name, space_name(space),
+				 "HASH index cannot be functional");
+			return -1;
+		}
 		break;
 	case TREE:
 		/* TREE index has no limitations. */
@@ -688,6 +694,12 @@ memtx_space_check_index_def(struct space *space, struct index_def *index_def)
 				 "RTREE index cannot be multikey");
 			return -1;
 		}
+		if (key_def_is_functional(index_def->key_def)) {
+			diag_set(ClientError, ER_MODIFY_INDEX,
+				 index_def->name, space_name(space),
+				 "RTREE index cannot be functional");
+			return -1;
+		}
 		/* no furter checks of parts needed */
 		return 0;
 	case BITSET:
@@ -716,6 +728,12 @@ memtx_space_check_index_def(struct space *space, struct index_def *index_def)
 				 "BITSET index cannot be multikey");
 			return -1;
 		}
+		if (key_def_is_functional(index_def->key_def)) {
+			diag_set(ClientError, ER_MODIFY_INDEX,
+				 index_def->name, space_name(space),
+				 "BITSET index cannot be functional");
+			return -1;
+		}
 		/* no furter checks of parts needed */
 		return 0;
 	default:
diff --git a/src/box/memtx_tree.c b/src/box/memtx_tree.c
index 3edf94044..5606bdda9 100644
--- a/src/box/memtx_tree.c
+++ b/src/box/memtx_tree.c
@@ -1017,6 +1017,36 @@ static const struct index_vtab memtx_tree_index_multikey_vtab = {
 	/* .end_build = */ memtx_tree_index_end_build,
 };
 
+static const struct index_vtab memtx_tree_index_disabled_vtab = {
+	/* .destroy = */ memtx_tree_index_destroy,
+	/* .commit_create = */ generic_index_commit_create,
+	/* .abort_create = */ generic_index_abort_create,
+	/* .commit_modify = */ generic_index_commit_modify,
+	/* .commit_drop = */ generic_index_commit_drop,
+	/* .update_def = */ generic_index_update_def,
+	/* .depends_on_pk = */ generic_index_depends_on_pk,
+	/* .def_change_requires_rebuild = */
+		generic_index_def_change_requires_rebuild,
+	/* .size = */ generic_index_size,
+	/* .bsize = */ generic_index_bsize,
+	/* .min = */ generic_index_min,
+	/* .max = */ generic_index_max,
+	/* .random = */ generic_index_random,
+	/* .count = */ generic_index_count,
+	/* .get = */ generic_index_get,
+	/* .replace = */ disabled_index_replace,
+	/* .create_iterator = */ generic_index_create_iterator,
+	/* .create_snapshot_iterator = */
+		generic_index_create_snapshot_iterator,
+	/* .stat = */ generic_index_stat,
+	/* .compact = */ generic_index_compact,
+	/* .reset_stat = */ generic_index_reset_stat,
+	/* .begin_build = */ generic_index_begin_build,
+	/* .reserve = */ generic_index_reserve,
+	/* .build_next = */ disabled_index_build_next,
+	/* .end_build = */ generic_index_end_build,
+};
+
 struct index *
 memtx_tree_index_new(struct memtx_engine *memtx, struct index_def *def)
 {
@@ -1027,9 +1057,14 @@ memtx_tree_index_new(struct memtx_engine *memtx, struct index_def *def)
 			 "malloc", "struct memtx_tree_index");
 		return NULL;
 	}
-	const struct index_vtab *vtab = def->key_def->is_multikey ?
-					&memtx_tree_index_multikey_vtab :
-					&memtx_tree_index_vtab;
+	const struct index_vtab *vtab;
+	if (key_def_is_functional(def->key_def) &&
+	    def->key_def->functional_extractor == NULL)
+		vtab = &memtx_tree_index_disabled_vtab;
+	else if (def->key_def->is_multikey)
+		vtab = &memtx_tree_index_multikey_vtab;
+	else
+		vtab = &memtx_tree_index_vtab;
 	if (index_create(&index->base, (struct engine *)memtx,
 			 vtab, def) != 0) {
 		free(index);
diff --git a/src/box/port.h b/src/box/port.h
index a7f5d81bd..2136f0ec2 100644
--- a/src/box/port.h
+++ b/src/box/port.h
@@ -86,6 +86,7 @@ struct port_msgpack {
 	const struct port_vtab *vtab;
 	const char *data;
 	uint32_t data_sz;
+	bool is_single_tuple;
 };
 
 static_assert(sizeof(struct port_msgpack) <= sizeof(struct port),
@@ -93,7 +94,8 @@ static_assert(sizeof(struct port_msgpack) <= sizeof(struct port),
 
 /** Initialize a port to dump raw data. */
 void
-port_msgpack_create(struct port *port, const char *data, uint32_t data_sz);
+port_msgpack_create(struct port *base, const char *data, uint32_t data_sz,
+		    bool is_single_tuple);
 
 /** Port for storing the result of a Lua CALL/EVAL. */
 struct port_lua {
diff --git a/src/box/schema.cc b/src/box/schema.cc
index 20666386d..d5ccdd1b6 100644
--- a/src/box/schema.cc
+++ b/src/box/schema.cc
@@ -266,7 +266,8 @@ sc_space_new(uint32_t id, const char *name,
 	     uint32_t key_part_count,
 	     struct trigger *replace_trigger)
 {
-	struct key_def *key_def = key_def_new(key_parts, key_part_count);
+	struct key_def *key_def = key_def_new(key_parts, key_part_count,
+					      false, false);
 	if (key_def == NULL)
 		diag_raise();
 	auto key_def_guard =
@@ -462,6 +463,16 @@ schema_init()
 	sc_space_new(BOX_CK_CONSTRAINT_ID, "_ck_constraint", key_parts, 2,
 		     &on_replace_ck_constraint);
 
+	/* _func_index - check constraints. */
+	key_parts[0].fieldno = 0; /* space id */
+	key_parts[0].type = FIELD_TYPE_UNSIGNED;
+	key_parts[1].fieldno = 1; /* index id */
+	key_parts[1].type = FIELD_TYPE_UNSIGNED;
+	key_parts[2].fieldno = 2; /* function id */
+	key_parts[2].type = FIELD_TYPE_UNSIGNED;
+	sc_space_new(BOX_FUNC_INDEX_ID, "_func_index", key_parts, 3,
+		     &on_replace_func_index);
+
 	/*
 	 * _vinyl_deferred_delete - blackhole that is needed
 	 * for writing deferred DELETE statements generated by
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index a97b6d531..85f652d52 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -112,6 +112,8 @@ enum {
 	BOX_FK_CONSTRAINT_ID = 356,
 	/** Space id of _ck_contraint. */
 	BOX_CK_CONSTRAINT_ID = 364,
+	/** Space id of _func_index. */
+	BOX_FUNC_INDEX_ID = 372,
 	/** End of the reserved range of system spaces. */
 	BOX_SYSTEM_ID_MAX = 511,
 	BOX_ID_NIL = 2147483647
@@ -267,6 +269,13 @@ enum {
 	BOX_CK_CONSTRAINT_FIELD_CODE = 4,
 };
 
+/** _func_index fields. */
+enum {
+	BOX_FUNC_INDEX_FIELD_SPACE_ID = 0,
+	BOX_FUNC_INDEX_FIELD_INDEX_ID = 1,
+	BOX_FUNC_INDEX_FUNCTION_ID = 2,
+};
+
 /*
  * Different objects which can be subject to access
  * control.
diff --git a/src/box/sql.c b/src/box/sql.c
index 4c9a4c15b..ba5bcbf10 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -347,7 +347,8 @@ sql_ephemeral_space_create(uint32_t field_count, struct sql_key_info *key_info)
 		}
 	}
 	struct key_def *ephemer_key_def = key_def_new(ephemer_key_parts,
-						      field_count);
+						      field_count, false,
+						      false);
 	if (ephemer_key_def == NULL)
 		return NULL;
 
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index 396de63fd..9fd9f1552 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -2307,7 +2307,7 @@ index_fill_def(struct Parse *parse, struct index *index,
 		part->coll_id = coll_id;
 		part->path = NULL;
 	}
-	key_def = key_def_new(key_parts, expr_list->nExpr);
+	key_def = key_def_new(key_parts, expr_list->nExpr, false, false);
 	if (key_def == NULL)
 		goto tnt_error;
 	/*
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index 7c8da251e..40d3394ea 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -1438,7 +1438,8 @@ sql_key_info_to_key_def(struct sql_key_info *key_info)
 {
 	if (key_info->key_def == NULL) {
 		key_info->key_def = key_def_new(key_info->parts,
-						key_info->part_count);
+						key_info->part_count,
+						false, false);
 	}
 	return key_info->key_def;
 }
diff --git a/src/box/sql/where.c b/src/box/sql/where.c
index 5458c6a75..b85243a88 100644
--- a/src/box/sql/where.c
+++ b/src/box/sql/where.c
@@ -2775,7 +2775,7 @@ whereLoopAddBtree(WhereLoopBuilder * pBuilder,	/* WHERE clause information */
 		part.coll_id = COLL_NONE;
 		part.path = NULL;
 
-		struct key_def *key_def = key_def_new(&part, 1);
+		struct key_def *key_def = key_def_new(&part, 1, false, false);
 		if (key_def == NULL) {
 tnt_error:
 			pWInfo->pParse->is_aborted = true;
diff --git a/src/box/tuple.c b/src/box/tuple.c
index ca0e5230a..80312da71 100644
--- a/src/box/tuple.c
+++ b/src/box/tuple.c
@@ -57,6 +57,9 @@ struct tuple *box_tuple_last;
 
 struct tuple_format *tuple_format_runtime;
 
+functional_key_extractor box_functional_key_get = NULL;
+functional_key_extractor_raw box_functional_key_get_raw = NULL;
+
 static void
 runtime_tuple_delete(struct tuple_format *format, struct tuple *tuple);
 
@@ -122,7 +125,8 @@ runtime_tuple_delete(struct tuple_format *format, struct tuple *tuple)
 int
 tuple_validate_raw(struct tuple_format *format, const char *tuple)
 {
-	if (tuple_format_field_count(format) == 0)
+	if (tuple_format_field_count(format) == 0 &&
+	    rlist_empty(&format->functional_handle))
 		return 0; /* Nothing to check */
 
 	struct region *region = &fiber()->gc;
@@ -130,7 +134,18 @@ tuple_validate_raw(struct tuple_format *format, const char *tuple)
 	struct field_map_builder builder;
 	int rc = tuple_field_map_create(format, tuple, true, &builder);
 	region_truncate(region, region_svp);
-	return rc;
+	if (rc != 0 || rlist_empty(&format->functional_handle))
+		return rc;
+	struct functional_handle *handle;
+	const char *tuple_end = tuple;
+	mp_next(&tuple_end);
+	rlist_foreach_entry(handle, &format->functional_handle, link) {
+		uint32_t key_count;
+		if (box_functional_key_get_raw(tuple, tuple_end, handle->key_def,
+					       &key_count) == NULL)
+			return -1;
+	}
+	return 0;
 }
 
 /**
@@ -288,7 +303,8 @@ tuple_unref_slow(struct tuple *tuple)
 /* }}} Bigref */
 
 int
-tuple_init(field_name_hash_f hash)
+tuple_init(field_name_hash_f hash, functional_key_extractor functional_key_get,
+	   functional_key_extractor_raw functional_key_get_raw)
 {
 	if (tuple_format_init() != 0)
 		return -1;
@@ -319,6 +335,9 @@ tuple_init(field_name_hash_f hash)
 	if (coll_id_cache_init() != 0)
 		return -1;
 
+	box_functional_key_get = functional_key_get;
+	box_functional_key_get_raw = functional_key_get_raw;
+
 	return 0;
 }
 
diff --git a/src/box/tuple.h b/src/box/tuple.h
index 70119292e..907d561b7 100644
--- a/src/box/tuple.h
+++ b/src/box/tuple.h
@@ -45,6 +45,18 @@ extern "C" {
 struct slab_arena;
 struct quota;
 struct key_part;
+struct tuple;
+
+typedef const char *
+(*functional_key_extractor)(struct tuple *tuple, struct key_def *key_def,
+			    uint32_t *key_count, uint32_t **key_map);
+
+typedef const char *
+(*functional_key_extractor_raw)(const char *tuple, const char *tuple_end,
+				struct key_def *key_def, uint32_t *key_count);
+
+extern functional_key_extractor box_functional_key_get;
+extern functional_key_extractor_raw box_functional_key_get_raw;
 
 /**
  * A format for standalone tuples allocated on runtime arena.
@@ -54,7 +66,8 @@ extern struct tuple_format *tuple_format_runtime;
 
 /** Initialize tuple library */
 int
-tuple_init(field_name_hash_f hash);
+tuple_init(field_name_hash_f hash, functional_key_extractor functional_key_get,
+	   functional_key_extractor_raw functional_key_get_raw);
 
 /** Cleanup tuple library */
 void
@@ -727,6 +740,40 @@ tuple_field_by_part(struct tuple *tuple, struct key_part *part,
 				       multikey_idx);
 }
 
+/**
+ * Get a field refereed by index @part of functional key
+ * definition in tuple.
+ * @param tuple Tuple to get the field from.
+ * @param part Index part to use.
+ * @param multikey_idx A multikey index hint.
+ * @retval Field data if the field exists or NULL.
+ */
+static inline const char *
+tuple_field_by_functional_def(struct tuple *tuple, struct key_part *part,
+			      struct key_def *key_def, int multikey_idx)
+{
+	assert(key_def_is_functional(key_def));
+	uint32_t key_count, *key_map;
+	const char *key = box_functional_key_get(tuple, key_def, &key_count,
+						 &key_map);
+	if (unlikely(key == NULL)) {
+		panic_syserror("Functional index runtime exception: %s",
+			       diag_last_error(diag_get())->errmsg);
+	}
+	if (multikey_idx != MULTIKEY_NONE && multikey_idx > 0) {
+		if (unlikely((uint32_t) multikey_idx > key_count))
+			return NULL;
+		key += key_map[multikey_idx];
+	}
+	assert(mp_typeof(*key) == MP_ARRAY);
+	uint32_t part_count = mp_decode_array(&key);
+	if (unlikely(part->fieldno >= part_count))
+		return NULL;
+	for (uint32_t k = 0; k < part->fieldno; k++)
+		mp_next(&key);
+	return key;
+}
+
 /**
  * Get count of multikey index keys in tuple by given multikey
  * index definition.
@@ -750,8 +797,21 @@ tuple_raw_multikey_count(struct tuple_format *format, const char *data,
 static inline uint32_t
 tuple_multikey_count(struct tuple *tuple, struct key_def *key_def)
 {
-	return tuple_raw_multikey_count(tuple_format(tuple), tuple_data(tuple),
+	if (key_def_is_functional(key_def)) {
+		assert(key_def->is_multikey);
+		uint32_t key_count, *key_map;
+		const char *key = box_functional_key_get(tuple, key_def,
+							 &key_count, &key_map);
+		if (unlikely(key == NULL)) {
+			panic_syserror("Functional index runtime exception: %s",
+				       diag_last_error(diag_get())->errmsg);
+		}
+		return key_count;
+	} else {
+		return tuple_raw_multikey_count(tuple_format(tuple),
+					tuple_data(tuple),
 					tuple_field_map(tuple), key_def);
+	}
 }
 
 /**
diff --git a/src/box/tuple_compare.cc b/src/box/tuple_compare.cc
index c03b584d0..8ec8a2fd4 100644
--- a/src/box/tuple_compare.cc
+++ b/src/box/tuple_compare.cc
@@ -450,13 +450,15 @@ tuple_compare_field_with_type(const char *field_a, enum mp_type a_type,
 }
 
 template<bool is_nullable, bool has_optional_parts, bool has_json_paths,
-	 bool is_multikey>
+	 bool is_multikey, bool is_functional>
 static inline int
 tuple_compare_slowpath(struct tuple *tuple_a, hint_t tuple_a_hint,
 		       struct tuple *tuple_b, hint_t tuple_b_hint,
 		       struct key_def *key_def)
 {
 	assert(has_json_paths == key_def->has_json_paths);
+	assert(is_functional == key_def_is_functional(key_def));
+	assert(!key_def_is_functional(key_def) || !has_optional_parts);
 	assert(!has_optional_parts || is_nullable);
 	assert(is_nullable == key_def->is_nullable);
 	assert(has_optional_parts == key_def->has_optional_parts);
@@ -470,7 +472,7 @@ tuple_compare_slowpath(struct tuple *tuple_a, hint_t tuple_a_hint,
 	const char *tuple_a_raw = tuple_data(tuple_a);
 	const char *tuple_b_raw = tuple_data(tuple_b);
 	if (key_def->part_count == 1 && part->fieldno == 0 &&
-	    (!has_json_paths || part->path == NULL)) {
+	    (!has_json_paths || part->path == NULL) && !is_functional) {
 		/*
 		 * First field can not be optional - empty tuples
 		 * can not exist.
@@ -498,7 +500,7 @@ tuple_compare_slowpath(struct tuple *tuple_a, hint_t tuple_a_hint,
 	struct tuple_format *format_b = tuple_format(tuple_b);
 	const uint32_t *field_map_a = tuple_field_map(tuple_a);
 	const uint32_t *field_map_b = tuple_field_map(tuple_b);
-	struct key_part *end;
+	struct key_part *end, *functional_end;
 	const char *field_a, *field_b;
 	enum mp_type a_type, b_type;
 	if (is_nullable)
@@ -506,21 +508,48 @@ tuple_compare_slowpath(struct tuple *tuple_a, hint_t tuple_a_hint,
 	else
 		end = part + key_def->part_count;
 
+	functional_end = part + key_def->functional_part_count;
+	for (; is_functional && part < functional_end; part++) {
+		field_a = tuple_field_by_functional_def(tuple_a, part, key_def,
+				is_multikey ? (int)tuple_a_hint : MULTIKEY_NONE);
+		field_b = tuple_field_by_functional_def(tuple_b, part, key_def,
+				is_multikey ? (int)tuple_b_hint : MULTIKEY_NONE);
+		if (! is_nullable) {
+			rc = tuple_compare_field(field_a, field_b, part->type,
+						 part->coll);
+			if (rc != 0)
+				return rc;
+			else
+				continue;
+		}
+		a_type = mp_typeof(*field_a);
+		b_type = mp_typeof(*field_b);
+		if (a_type == MP_NIL) {
+			if (b_type != MP_NIL)
+				return -1;
+			was_null_met = true;
+		} else if (b_type == MP_NIL) {
+			return 1;
+		} else {
+			rc = tuple_compare_field_with_type(field_a, a_type,
+							   field_b, b_type,
+							   part->type,
+							   part->coll);
+			if (rc != 0)
+				return rc;
+		}
+	}
+
 	for (; part < end; part++) {
-		if (is_multikey) {
+		if (has_json_paths) {
 			field_a = tuple_field_raw_by_part(format_a, tuple_a_raw,
-							  field_map_a, part,
-							  (int)tuple_a_hint);
+					field_map_a, part,
+					is_multikey ?
+					(int)tuple_a_hint : MULTIKEY_NONE);
 			field_b = tuple_field_raw_by_part(format_b, tuple_b_raw,
-							  field_map_b, part,
-							  (int)tuple_b_hint);
-		} else if (has_json_paths) {
-			field_a = tuple_field_raw_by_part(format_a, tuple_a_raw,
-							  field_map_a, part,
-							  MULTIKEY_NONE);
-			field_b = tuple_field_raw_by_part(format_b, tuple_b_raw,
-							  field_map_b, part,
-							  MULTIKEY_NONE);
+					field_map_b, part,
+					is_multikey ?
+					(int)tuple_b_hint : MULTIKEY_NONE);
 		} else {
 			field_a = tuple_field_raw(format_a, tuple_a_raw,
 						  field_map_a, part->fieldno);
@@ -573,20 +602,15 @@ tuple_compare_slowpath(struct tuple *tuple_a, hint_t tuple_a_hint,
 	 */
 	end = key_def->parts + key_def->part_count;
 	for (; part < end; ++part) {
-		if (is_multikey) {
-			field_a = tuple_field_raw_by_part(format_a, tuple_a_raw,
-							  field_map_a, part,
-							  (int)tuple_a_hint);
-			field_b = tuple_field_raw_by_part(format_b, tuple_b_raw,
-							  field_map_b, part,
-							  (int)tuple_b_hint);
-		} else if (has_json_paths) {
+		if (has_json_paths) {
 			field_a = tuple_field_raw_by_part(format_a, tuple_a_raw,
-							  field_map_a, part,
-							  MULTIKEY_NONE);
+					field_map_a, part,
+					is_multikey ?
+					(int)tuple_a_hint : MULTIKEY_NONE);
 			field_b = tuple_field_raw_by_part(format_b, tuple_b_raw,
-							  field_map_b, part,
-							  MULTIKEY_NONE);
+					field_map_b, part,
+					is_multikey ?
+					(int)tuple_b_hint : MULTIKEY_NONE);
 		} else {
 			field_a = tuple_field_raw(format_a, tuple_a_raw,
 						  field_map_a, part->fieldno);
@@ -607,13 +631,14 @@ tuple_compare_slowpath(struct tuple *tuple_a, hint_t tuple_a_hint,
 }
 
 template<bool is_nullable, bool has_optional_parts, bool has_json_paths,
-	 bool is_multikey>
+	 bool is_multikey, bool is_functional>
 static inline int
 tuple_compare_with_key_slowpath(struct tuple *tuple, hint_t tuple_hint,
 				const char *key, uint32_t part_count,
 				hint_t key_hint, struct key_def *key_def)
 {
 	assert(has_json_paths == key_def->has_json_paths);
+	assert(is_functional == key_def_is_functional(key_def));
 	assert(!has_optional_parts || is_nullable);
 	assert(is_nullable == key_def->is_nullable);
 	assert(has_optional_parts == key_def->has_optional_parts);
@@ -632,14 +657,15 @@ tuple_compare_with_key_slowpath(struct tuple *tuple, hint_t tuple_hint,
 	enum mp_type a_type, b_type;
 	if (likely(part_count == 1)) {
 		const char *field;
-		if (is_multikey) {
-			field = tuple_field_raw_by_part(format, tuple_raw,
-							field_map, part,
-							(int)tuple_hint);
+		if (is_functional) {
+			field = tuple_field_by_functional_def(tuple, part,
+					key_def, is_multikey ?
+					(int)tuple_hint : MULTIKEY_NONE);
 		} else if (has_json_paths) {
 			field = tuple_field_raw_by_part(format, tuple_raw,
-							field_map, part,
-							MULTIKEY_NONE);
+					field_map, part,
+					is_multikey ?
+					(int)tuple_hint : MULTIKEY_NONE);
 		} else {
 			field = tuple_field_raw(format, tuple_raw, field_map,
 						part->fieldno);
@@ -665,16 +691,42 @@ tuple_compare_with_key_slowpath(struct tuple *tuple, hint_t tuple_hint,
 	}
 
 	struct key_part *end = part + part_count;
+	struct key_part *functional_end =
+		part + MIN(part_count, key_def->functional_part_count);
+	for (; is_functional && part < functional_end; part++) {
+		const char *field = tuple_field_by_functional_def(tuple, part,
+						key_def, is_multikey ?
+						(int)tuple_hint : MULTIKEY_NONE);
+		if (! is_nullable) {
+			rc = tuple_compare_field(field, key, part->type,
+						 part->coll);
+			if (rc != 0)
+				return rc;
+			else
+				continue;
+		}
+		a_type = mp_typeof(*field);
+		b_type = mp_typeof(*key);
+		if (a_type == MP_NIL) {
+			if (b_type != MP_NIL)
+				return -1;
+		} else if (b_type == MP_NIL) {
+			return 1;
+		} else {
+			rc = tuple_compare_field_with_type(field, a_type, key,
+							   b_type, part->type,
+							   part->coll);
+			if (rc != 0)
+				return rc;
+		}
+	}
 	for (; part < end; ++part, mp_next(&key)) {
 		const char *field;
-		if (is_multikey) {
-			field = tuple_field_raw_by_part(format, tuple_raw,
-							field_map, part,
-							(int)tuple_hint);
-		} else if (has_json_paths) {
+		if (has_json_paths) {
 			field = tuple_field_raw_by_part(format, tuple_raw,
-							field_map, part,
-							MULTIKEY_NONE);
+					field_map, part,
+					is_multikey ?
+					(int)tuple_hint : MULTIKEY_NONE);
 		} else {
 			field = tuple_field_raw(format, tuple_raw, field_map,
 						part->fieldno);
@@ -1579,13 +1631,19 @@ key_hint(const char *key, uint32_t part_count, struct key_def *key_def)
 	return field_hint<type, is_nullable>(key, key_def->parts->coll);
 }
 
-template <enum field_type type, bool is_nullable>
+template <enum field_type type, bool is_nullable, bool is_functional>
 static hint_t
 tuple_hint(struct tuple *tuple, struct key_def *key_def)
 {
 	assert(!key_def->is_multikey);
-	const char *field = tuple_field_by_part(tuple, key_def->parts,
-						MULTIKEY_NONE);
+	const char *field;
+	if (is_functional) {
+		field = tuple_field_by_functional_def(tuple, key_def->parts,
+						      key_def,  MULTIKEY_NONE);
+	} else {
+		field = tuple_field_by_part(tuple, key_def->parts,
+					    MULTIKEY_NONE);
+	}
 	if (is_nullable && field == NULL)
 		return hint_nil();
 	return field_hint<type, is_nullable>(field, key_def->parts->coll);
@@ -1625,7 +1683,10 @@ static void
 key_def_set_hint_func(struct key_def *def)
 {
 	def->key_hint = key_hint<type, is_nullable>;
-	def->tuple_hint = tuple_hint<type, is_nullable>;
+	if (key_def_is_functional(def))
+		def->tuple_hint = tuple_hint<type, is_nullable, true>;
+	else
+		def->tuple_hint = tuple_hint<type, is_nullable, false>;
 }
 
 template<enum field_type type>
@@ -1720,13 +1781,14 @@ key_def_set_compare_func_fast(struct key_def *def)
 	if (cmp == NULL) {
 		cmp = is_sequential ?
 			tuple_compare_sequential<false, false> :
-			tuple_compare_slowpath<false, false, false, false>;
+			tuple_compare_slowpath<false, false, false,
+					       false, false>;
 	}
 	if (cmp_wk == NULL) {
 		cmp_wk = is_sequential ?
 			tuple_compare_with_key_sequential<false, false> :
 			tuple_compare_with_key_slowpath<false, false,
-							false, false>;
+							false, false, false>;
 	}
 
 	def->tuple_compare = cmp;
@@ -1745,9 +1807,11 @@ key_def_set_compare_func_plain(struct key_def *def)
 					<is_nullable, has_optional_parts>;
 	} else {
 		def->tuple_compare = tuple_compare_slowpath
-				<is_nullable, has_optional_parts, false, false>;
+				<is_nullable, has_optional_parts, false,
+				 false, false>;
 		def->tuple_compare_with_key = tuple_compare_with_key_slowpath
-				<is_nullable, has_optional_parts, false, false>;
+				<is_nullable, has_optional_parts, false,
+				 false, false>;
 	}
 }
 
@@ -1758,14 +1822,36 @@ key_def_set_compare_func_json(struct key_def *def)
 	assert(def->has_json_paths);
 	if (def->is_multikey) {
 		def->tuple_compare = tuple_compare_slowpath
-				<is_nullable, has_optional_parts, true, true>;
+				<is_nullable, has_optional_parts, true,
+				 true, false>;
+		def->tuple_compare_with_key = tuple_compare_with_key_slowpath
+				<is_nullable, has_optional_parts, true,
+				 true, false>;
+	} else {
+		def->tuple_compare = tuple_compare_slowpath
+				<is_nullable, has_optional_parts, true,
+				 false, false>;
+		def->tuple_compare_with_key = tuple_compare_with_key_slowpath
+				<is_nullable, has_optional_parts, true,
+				 false, false>;
+	}
+}
+
+template<bool is_nullable>
+static void
+key_def_set_compare_func_functional(struct key_def *def)
+{
+	assert(key_def_is_functional(def));
+	if (def->is_multikey) {
+		def->tuple_compare = tuple_compare_slowpath
+				<is_nullable, false, false, true, true>;
 		def->tuple_compare_with_key = tuple_compare_with_key_slowpath
-				<is_nullable, has_optional_parts, true, true>;
+				<is_nullable, false, false, true, true>;
 	} else {
 		def->tuple_compare = tuple_compare_slowpath
-				<is_nullable, has_optional_parts, true, false>;
+				<is_nullable, false, false, false, true>;
 		def->tuple_compare_with_key = tuple_compare_with_key_slowpath
-				<is_nullable, has_optional_parts, true, false>;
+				<is_nullable, false, false, false, true>;
 	}
 }
 
@@ -1773,9 +1859,10 @@ void
 key_def_set_compare_func(struct key_def *def)
 {
 	if (!key_def_has_collation(def) &&
-	    !def->is_nullable && !def->has_json_paths) {
+	    !def->is_nullable && !def->has_json_paths &&
+	    !key_def_is_functional(def)) {
 		key_def_set_compare_func_fast(def);
-	} else if (!def->has_json_paths) {
+	} else if (!def->has_json_paths && !key_def_is_functional(def)) {
 		if (def->is_nullable && def->has_optional_parts) {
 			key_def_set_compare_func_plain<true, true>(def);
 		} else if (def->is_nullable && !def->has_optional_parts) {
@@ -1784,7 +1871,16 @@ key_def_set_compare_func(struct key_def *def)
 			assert(!def->is_nullable && !def->has_optional_parts);
 			key_def_set_compare_func_plain<false, false>(def);
 		}
+	} else if (key_def_is_functional(def)) {
+		assert(!def->has_json_paths);
+		assert(!def->has_optional_parts);
+		if (def->is_nullable) {
+			key_def_set_compare_func_functional<true>(def);
+		} else {
+			key_def_set_compare_func_functional<false>(def);
+		}
 	} else {
+		assert(def->has_json_paths);
 		if (def->is_nullable && def->has_optional_parts) {
 			key_def_set_compare_func_json<true, true>(def);
 		} else if (def->is_nullable && !def->has_optional_parts) {
diff --git a/src/box/tuple_extract_key.cc b/src/box/tuple_extract_key.cc
index 471c7df80..21e04fd8c 100644
--- a/src/box/tuple_extract_key.cc
+++ b/src/box/tuple_extract_key.cc
@@ -120,6 +120,7 @@ tuple_extract_key_slowpath(struct tuple *tuple, struct key_def *key_def,
 	       key_def_contains_sequential_parts(key_def));
 	assert(is_multikey == key_def->is_multikey);
 	assert(!key_def->is_multikey || multikey_idx != MULTIKEY_NONE);
+	assert(!key_def_is_functional(key_def));
 	assert(mp_sizeof_nil() == 1);
 	const char *data = tuple_data(tuple);
 	uint32_t part_count = key_def->part_count;
@@ -251,6 +252,7 @@ tuple_extract_key_slowpath_raw(const char *data, const char *data_end,
 	assert(!has_optional_parts || key_def->is_nullable);
 	assert(has_optional_parts == key_def->has_optional_parts);
 	assert(!key_def->is_multikey || multikey_idx != MULTIKEY_NONE);
+	assert(!key_def_is_functional(key_def));
 	assert(mp_sizeof_nil() == 1);
 	/* allocate buffer with maximal possible size */
 	char *key = (char *) region_alloc(&fiber()->gc, data_end - data);
@@ -367,6 +369,7 @@ key_def_set_extract_func_plain(struct key_def *def)
 {
 	assert(!def->has_json_paths);
 	assert(!def->is_multikey);
+	assert(!key_def_is_functional(def));
 	if (key_def_is_sequential(def)) {
 		assert(contains_sequential_parts || def->part_count == 1);
 		def->tuple_extract_key = tuple_extract_key_sequential
@@ -387,6 +390,7 @@ static void
 key_def_set_extract_func_json(struct key_def *def)
 {
 	assert(def->has_json_paths);
+	assert(!key_def_is_functional(def));
 	if (def->is_multikey) {
 		def->tuple_extract_key = tuple_extract_key_slowpath
 					<contains_sequential_parts,
@@ -406,7 +410,10 @@ key_def_set_extract_func(struct key_def *key_def)
 	bool contains_sequential_parts =
 		key_def_contains_sequential_parts(key_def);
 	bool has_optional_parts = key_def->has_optional_parts;
-	if (!key_def->has_json_paths) {
+	if (key_def_is_functional(key_def)) {
+		key_def->tuple_extract_key = NULL;
+		key_def->tuple_extract_key_raw = NULL;
+	} else if (!key_def->has_json_paths) {
 		if (!contains_sequential_parts && !has_optional_parts) {
 			key_def_set_extract_func_plain<false, false>(key_def);
 		} else if (!contains_sequential_parts && has_optional_parts) {
diff --git a/src/box/tuple_format.c b/src/box/tuple_format.c
index 02fadf1cf..1ebafd83e 100644
--- a/src/box/tuple_format.c
+++ b/src/box/tuple_format.c
@@ -35,6 +35,7 @@
 #include "coll_id_cache.h"
 #include "tt_static.h"
 
+
 #include "third_party/PMurHash.h"
 
 /** Global table of tuple formats */
@@ -462,6 +463,12 @@ tuple_format_create(struct tuple_format *format, struct key_def * const *keys,
 	/* extract field type info */
 	for (uint16_t key_no = 0; key_no < key_count; ++key_no) {
 		const struct key_def *key_def = keys[key_no];
+		/*
+		 * Functional key definitions are not the part
+		 * of the space format.
+		 */
+		if (key_def_is_functional(key_def))
+			continue;
 		bool is_sequential = key_def_is_sequential(key_def);
 		const struct key_part *part = key_def->parts;
 		const struct key_part *parts_end = part + key_def->part_count;
@@ -605,16 +612,52 @@ tuple_format_destroy_fields(struct tuple_format *format)
 	json_tree_destroy(&format->fields);
 }
 
+static struct functional_handle *
+functional_handle_new(const struct key_def *key_def)
+{
+	struct functional_handle *handle = malloc(sizeof(*handle));
+	if (handle == NULL) {
+		diag_set(OutOfMemory, sizeof(*handle), "malloc", "handle");
+		return NULL;
+	}
+	handle->key_def = key_def_dup(key_def);
+	if (handle->key_def == NULL) {
+		free(handle);
+		return NULL;
+	}
+	rlist_create(&handle->link);
+	return handle;
+}
+
+static void
+functional_handle_delete(struct functional_handle *handle)
+{
+	key_def_delete(handle->key_def);
+	free(handle);
+}
+
 static struct tuple_format *
 tuple_format_alloc(struct key_def * const *keys, uint16_t key_count,
 		   uint32_t space_field_count, struct tuple_dictionary *dict)
 {
+	struct tuple_format *format = NULL;
+	struct rlist functional_handle;
+	rlist_create(&functional_handle);
 	/* Size of area to store JSON paths data. */
 	uint32_t path_pool_size = 0;
 	uint32_t index_field_count = 0;
 	/* find max max field no */
 	for (uint16_t key_no = 0; key_no < key_count; ++key_no) {
 		const struct key_def *key_def = keys[key_no];
+		if (key_def_is_functional(key_def) &&
+		    key_def->functional_extractor != NULL) {
+			struct functional_handle *handle =
+				functional_handle_new(key_def);
+			if (handle == NULL)
+				goto error;
+			rlist_add(&functional_handle, &handle->link);
+			continue;
+		}
 		const struct key_part *part = key_def->parts;
 		const struct key_part *pend = part + key_def->part_count;
 		for (; part < pend; part++) {
@@ -626,7 +669,7 @@ tuple_format_alloc(struct key_def * const *keys, uint16_t key_count,
 	uint32_t field_count = MAX(space_field_count, index_field_count);
 
 	uint32_t allocation_size = sizeof(struct tuple_format) + path_pool_size;
-	struct tuple_format *format = malloc(allocation_size);
+	format = malloc(allocation_size);
 	if (format == NULL) {
 		diag_set(OutOfMemory, allocation_size, "malloc",
 			 "tuple format");
@@ -635,8 +678,7 @@ tuple_format_alloc(struct key_def * const *keys, uint16_t key_count,
 	if (json_tree_create(&format->fields) != 0) {
 		diag_set(OutOfMemory, 0, "json_lexer_create",
 			 "tuple field tree");
-		free(format);
-		return NULL;
+		goto error;
 	}
 	for (uint32_t fieldno = 0; fieldno < field_count; fieldno++) {
 		struct tuple_field *field = tuple_field_new();
@@ -662,6 +704,8 @@ tuple_format_alloc(struct key_def * const *keys, uint16_t key_count,
 		format->dict = dict;
 		tuple_dictionary_ref(dict);
 	}
+	rlist_create(&format->functional_handle);
+	rlist_swap(&format->functional_handle, &functional_handle);
 	format->total_field_count = field_count;
 	format->required_fields = NULL;
 	format->fields_depth = 1;
@@ -672,7 +716,10 @@ tuple_format_alloc(struct key_def * const *keys, uint16_t key_count,
 	format->min_field_count = 0;
 	format->epoch = 0;
 	return format;
-error:
+error:;
+	struct functional_handle *handle, *tmp;
+	rlist_foreach_entry_safe(handle, &functional_handle, link, tmp)
+		functional_handle_delete(handle);
 	tuple_format_destroy_fields(format);
 	free(format);
 	return NULL;
@@ -682,6 +729,9 @@ error:
 static inline void
 tuple_format_destroy(struct tuple_format *format)
 {
+	struct functional_handle *handle, *tmp;
+	rlist_foreach_entry_safe(handle, &format->functional_handle, link, tmp)
+		functional_handle_delete(handle);
 	free(format->required_fields);
 	tuple_format_destroy_fields(format);
 	tuple_dictionary_unref(format->dict);
diff --git a/src/box/tuple_format.h b/src/box/tuple_format.h
index ac4f8a702..99ae5f5d5 100644
--- a/src/box/tuple_format.h
+++ b/src/box/tuple_format.h
@@ -68,6 +68,21 @@ struct tuple_extra;
 struct tuple_format;
 struct coll;
 
+/**
+ * A functional handle is a tuple_format extention used when
+ * functional index key is defined. The functional handle
+ * represents a single functional index reference and allows
+ * to perform a validation of the functional index extracted key.
+ */
+struct functional_handle {
+	struct rlist link;
+	/**
+	 * The key definition that describe keys produced by
+	 * functional index extractor.
+	*/
+	struct key_def *key_def;
+};
+
 /** Engine-specific tuple format methods. */
 struct tuple_format_vtab {
 	/**
@@ -233,6 +248,8 @@ struct tuple_format {
 	 * A maximum depth of format::fields subtree.
 	 */
 	uint32_t fields_depth;
+	/** A functional index handle list. */
+	struct rlist functional_handle;
 	/**
 	 * Fields comprising the format, organized in a tree.
 	 * First level nodes correspond to tuple fields.
diff --git a/src/box/tuple_hash.cc b/src/box/tuple_hash.cc
index 780e3d053..dad87871a 100644
--- a/src/box/tuple_hash.cc
+++ b/src/box/tuple_hash.cc
@@ -365,6 +365,7 @@ tuple_hash_slowpath(struct tuple *tuple, struct key_def *key_def)
 	assert(has_json_paths == key_def->has_json_paths);
 	assert(has_optional_parts == key_def->has_optional_parts);
 	assert(!key_def->is_multikey);
+	assert(!key_def_is_functional(key_def));
 	uint32_t h = HASH_SEED;
 	uint32_t carry = 0;
 	uint32_t total_size = 0;
diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index cf7af26b7..9d4912230 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -701,6 +701,11 @@ vinyl_space_check_index_def(struct space *space, struct index_def *index_def)
 			return -1;
 		}
 	}
+	if (key_def_is_functional(index_def->key_def)) {
+		diag_set(ClientError, ER_UNSUPPORTED, "Vinyl",
+			  "functional indexes");
+		return -1;
+	}
 	return 0;
 }
 
@@ -986,6 +991,10 @@ vinyl_index_def_change_requires_rebuild(struct index *index,
 
 	if (!old_def->opts.is_unique && new_def->opts.is_unique)
 		return true;
+	if (old_def->opts.is_multikey != new_def->opts.is_multikey)
+		return true;
+	if (old_def->opts.functional_fid != new_def->opts.functional_fid)
+		return true;
 
 	assert(index_depends_on_pk(index));
 	const struct key_def *old_cmp_def = old_def->cmp_def;
@@ -3165,7 +3174,7 @@ vy_send_lsm(struct vy_join_ctx *ctx, struct vy_lsm_recovery_info *lsm_info)
 
 	/* Create key definition and tuple format. */
 	ctx->key_def = key_def_new(lsm_info->key_parts,
-				   lsm_info->key_part_count);
+				   lsm_info->key_part_count, false, false);
 	if (ctx->key_def == NULL)
 		goto out;
 	ctx->format = vy_stmt_format_new(&ctx->env->stmt_env, &ctx->key_def, 1,
diff --git a/test/app-tap/tarantoolctl.test.lua b/test/app-tap/tarantoolctl.test.lua
index 957b883f4..df2ee377f 100755
--- a/test/app-tap/tarantoolctl.test.lua
+++ b/test/app-tap/tarantoolctl.test.lua
@@ -405,8 +405,8 @@ do
             check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1", "\n", 3)
             check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 1 --replica 2", "\n", 3)
             check_ctlcat_xlog(test_i, dir, "--from=3 --to=6 --format=json --show-system --replica 2", "\n", 0)
-            check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 23)
-            check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 50)
+            check_ctlcat_snap(test_i, dir, "--space=280", "---\n", 24)
+            check_ctlcat_snap(test_i, dir, "--space=288", "---\n", 52)
         end)
     end)
 
diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result
index f2d1f46fb..a5d645df8 100644
--- a/test/box-py/bootstrap.result
+++ b/test/box-py/bootstrap.result
@@ -93,6 +93,8 @@ box.space._space:select{}
   - [364, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'space_id', 'type': 'unsigned'},
       {'name': 'name', 'type': 'string'}, {'name': 'is_deferred', 'type': 'boolean'},
       {'name': 'language', 'type': 'str'}, {'name': 'code', 'type': 'str'}]]
+  - [372, 1, '_func_index', 'memtx', 0, {}, [{'name': 'space_id', 'type': 'unsigned'},
+      {'name': 'index_id', 'type': 'unsigned'}, {'name': 'func_id', 'type': 'unsigned'}]]
 ...
 box.space._index:select{}
 ---
@@ -148,6 +150,9 @@ box.space._index:select{}
   - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]]
   - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]]
   - [364, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned'], [1, 'string']]]
+  - [372, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned'], [1, 'unsigned'],
+      [2, 'unsigned']]]
+  - [372, 1, 'fid', 'tree', {'unique': false}, [[2, 'unsigned']]]
 ...
 box.space._user:select{}
 ---
diff --git a/test/box/access.result b/test/box/access.result
index 5ee92a443..ba72b5f74 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -1496,6 +1496,9 @@ box.schema.user.grant('tester', 'read', 'space', '_fk_constraint')
 box.schema.user.grant('tester', 'read', 'space', '_ck_constraint')
 ---
 ...
+box.schema.user.grant('tester', 'read', 'space', '_func_index')
+---
+...
 box.session.su("tester")
 ---
 ...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 79340b0f5..219cdb04a 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -557,6 +557,7 @@ box.schema.user.grant('tester', 'read', 'space', '_space_sequence')
 box.schema.user.grant('tester', 'read', 'space', '_trigger')
 box.schema.user.grant('tester', 'read', 'space', '_fk_constraint')
 box.schema.user.grant('tester', 'read', 'space', '_ck_constraint')
+box.schema.user.grant('tester', 'read', 'space', '_func_index')
 box.session.su("tester")
 -- successful create
 s1 = box.schema.space.create("test_space")
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index c69cf0283..31b935914 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -833,140 +833,142 @@ box.space._space:select()
   - [364, 1, '_ck_constraint', 'memtx', 0, {}, [{'name': 'space_id', 'type': 'unsigned'},
       {'name': 'name', 'type': 'string'}, {'name': 'is_deferred', 'type': 'boolean'},
       {'name': 'language', 'type': 'str'}, {'name': 'code', 'type': 'str'}]]
+  - [372, 1, '_func_index', 'memtx', 0, {}, [{'name': 'space_id', 'type': 'unsigned'},
+      {'name': 'index_id', 'type': 'unsigned'}, {'name': 'func_id', 'type': 'unsigned'}]]
 ...
 box.space._func:select()
 ---
 - - [1, 1, 'box.schema.user.info', 1, 'LUA', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, ['LUA'], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, ['LUA'], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [2, 1, 'TRIM', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [3, 1, 'TYPEOF', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [4, 1, 'PRINTF', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [5, 1, 'UNICODE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [6, 1, 'CHAR', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [7, 1, 'HEX', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [8, 1, 'VERSION', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [9, 1, 'QUOTE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [10, 1, 'REPLACE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [11, 1, 'SUBSTR', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [12, 1, 'GROUP_CONCAT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [13, 1, 'JULIANDAY', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [14, 1, 'DATE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [15, 1, 'TIME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [16, 1, 'DATETIME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [17, 1, 'STRFTIME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [18, 1, 'CURRENT_TIME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [19, 1, 'CURRENT_TIMESTAMP', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
-    'none', false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    'none', false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [20, 1, 'CURRENT_DATE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [21, 1, 'LENGTH', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [22, 1, 'POSITION', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [23, 1, 'ROUND', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [24, 1, 'UPPER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [25, 1, 'LOWER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [26, 1, 'IFNULL', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [27, 1, 'RANDOM', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [28, 1, 'CEIL', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [29, 1, 'CEILING', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [30, 1, 'CHARACTER_LENGTH', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
-    'none', false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    'none', false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [31, 1, 'CHAR_LENGTH', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [32, 1, 'FLOOR', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [33, 1, 'MOD', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [34, 1, 'OCTET_LENGTH', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [35, 1, 'ROW_COUNT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [36, 1, 'COUNT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [37, 1, 'LIKE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [38, 1, 'ABS', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [39, 1, 'EXP', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [40, 1, 'LN', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [41, 1, 'POWER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [42, 1, 'SQRT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [43, 1, 'SUM', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [44, 1, 'TOTAL', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [45, 1, 'AVG', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [46, 1, 'RANDOMBLOB', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [47, 1, 'NULLIF', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [48, 1, 'ZEROBLOB', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [49, 1, 'MIN', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [50, 1, 'MAX', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [51, 1, 'COALESCE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [52, 1, 'EVERY', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [53, 1, 'EXISTS', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [54, 1, 'EXTRACT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [55, 1, 'SOME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
-    false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [56, 1, 'GREATER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [57, 1, 'LESSER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [58, 1, 'SOUNDEX', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [59, 1, 'LIKELIHOOD', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [60, 1, 'LIKELY', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [61, 1, 'UNLIKELY', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
-    false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [62, 1, '_sql_stat_get', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
-    'none', false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    'none', false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [63, 1, '_sql_stat_push', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
-    'none', false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    'none', false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [64, 1, '_sql_stat_init', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
-    'none', false, false, true, [], {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    'none', false, false, true, [], {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
   - [65, 1, 'LUA', 1, 'LUA', 'function(code) return assert(loadstring(code))() end',
     'function', ['string'], 'any', 'none', 'none', false, false, true, ['LUA', 'SQL'],
-    {}, '', '2019-07-11 13:57:48', '2019-07-11 13:57:48']
+    {}, '', '2019-07-15 17:54:58', '2019-07-15 17:54:58']
 ...
 session = nil
 ---
diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result
index d65aa37ae..a82127ebb 100644
--- a/test/box/access_sysview.result
+++ b/test/box/access_sysview.result
@@ -246,11 +246,11 @@ box.session.su('guest')
 ...
 #box.space._vspace:select{}
 ---
-- 24
+- 25
 ...
 #box.space._vindex:select{}
 ---
-- 51
+- 53
 ...
 #box.space._vuser:select{}
 ---
@@ -282,7 +282,7 @@ box.session.su('guest')
 ...
 #box.space._vindex:select{}
 ---
-- 51
+- 53
 ...
 #box.space._vuser:select{}
 ---
diff --git a/test/box/alter.result b/test/box/alter.result
index a6db011ff..91a239bbc 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -92,7 +92,7 @@ space = box.space[t[1]]
 ...
 space.id
 ---
-- 365
+- 373
 ...
 space.field_count
 ---
@@ -137,7 +137,7 @@ space_deleted
 ...
 space:replace{0}
 ---
-- error: Space '365' does not exist
+- error: Space '373' does not exist
 ...
 _index:insert{_space.id, 0, 'primary', 'tree', {unique=true}, {{0, 'unsigned'}}}
 ---
@@ -218,6 +218,9 @@ _index:select{}
   - [356, 0, 'primary', 'tree', {'unique': true}, [[0, 'string'], [1, 'unsigned']]]
   - [356, 1, 'child_id', 'tree', {'unique': false}, [[1, 'unsigned']]]
   - [364, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned'], [1, 'string']]]
+  - [372, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned'], [1, 'unsigned'],
+      [2, 'unsigned']]]
+  - [372, 1, 'fid', 'tree', {'unique': false}, [[2, 'unsigned']]]
 ...
 -- modify indexes of a system space
 _index:delete{_index.id, 0}
diff --git a/test/box/bitset.result b/test/box/bitset.result
index 78f74ec37..dbaa7e74a 100644
--- a/test/box/bitset.result
+++ b/test/box/bitset.result
@@ -1996,3 +1996,27 @@ _ = s:create_index('bitset', {type = 'bitset', parts = {{'[2][*]', 'unsigned'}}}
 s:drop()
 ---
 ...
+-- Bitset index cannot be functional.
+s = box.schema.space.create('withdata')
+---
+...
+lua_code = [[function(tuple) return tuple[1] + tuple[2] end]]
+---
+...
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+---
+...
+_ = s:create_index('pk')
+---
+...
+_ = s:create_index('idx', {type = 'bitset', functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+---
+- error: 'Can''t create or modify index ''idx'' in space ''withdata'': BITSET index
+    cannot be functional'
+...
+s:drop()
+---
+...
+box.schema.func.drop('s')
+---
+...
diff --git a/test/box/bitset.test.lua b/test/box/bitset.test.lua
index eb013a1c0..025799aef 100644
--- a/test/box/bitset.test.lua
+++ b/test/box/bitset.test.lua
@@ -153,3 +153,12 @@ s = box.schema.space.create('test')
 _ = s:create_index('primary')
 _ = s:create_index('bitset', {type = 'bitset', parts = {{'[2][*]', 'unsigned'}}})
 s:drop()
+
+-- Bitset index cannot be functional.
+s = box.schema.space.create('withdata')
+lua_code = [[function(tuple) return tuple[1] + tuple[2] end]]
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+_ = s:create_index('pk')
+_ = s:create_index('idx', {type = 'bitset', functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+s:drop()
+box.schema.func.drop('s')
diff --git a/test/box/hash.result b/test/box/hash.result
index 9f08c49b8..b2a303a52 100644
--- a/test/box/hash.result
+++ b/test/box/hash.result
@@ -847,3 +847,27 @@ _ = s:create_index('hash', {type = 'hash', parts = {{'[2][*]', 'unsigned'}}})
 s:drop()
 ---
 ...
+-- Hash index cannot be functional.
+s = box.schema.space.create('withdata')
+---
+...
+lua_code = [[function(tuple) return tuple[1] + tuple[2] end]]
+---
+...
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+---
+...
+_ = s:create_index('pk')
+---
+...
+_ = s:create_index('idx', {type = 'hash', functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+---
+- error: 'Can''t create or modify index ''idx'' in space ''withdata'': HASH index
+    cannot be functional'
+...
+s:drop()
+---
+...
+box.schema.func.drop('s')
+---
+...
diff --git a/test/box/hash.test.lua b/test/box/hash.test.lua
index 9801873c4..b35d58623 100644
--- a/test/box/hash.test.lua
+++ b/test/box/hash.test.lua
@@ -353,3 +353,12 @@ s = box.schema.space.create('test')
 _ = s:create_index('primary')
 _ = s:create_index('hash', {type = 'hash', parts = {{'[2][*]', 'unsigned'}}})
 s:drop()
+
+-- Hash index cannot be functional.
+s = box.schema.space.create('withdata')
+lua_code = [[function(tuple) return tuple[1] + tuple[2] end]]
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+_ = s:create_index('pk')
+_ = s:create_index('idx', {type = 'hash', functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+s:drop()
+box.schema.func.drop('s')
diff --git a/test/box/misc.result b/test/box/misc.result
index 791730935..7449fdc84 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -526,6 +526,7 @@ t;
   195: box.error.CREATE_CK_CONSTRAINT
   196: box.error.CK_CONSTRAINT_FAILED
   197: box.error.SQL_COLUMN_COUNT
+  198: box.error.FUNCTIONAL_EXTRACTOR
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/box/rtree_misc.result b/test/box/rtree_misc.result
index 6e48bacc7..5d0d54a6e 100644
--- a/test/box/rtree_misc.result
+++ b/test/box/rtree_misc.result
@@ -682,3 +682,27 @@ _ = s:create_index('rtree', {type = 'rtree', parts = {{'[2][*]', 'array'}}})
 s:drop()
 ---
 ...
+-- Rtree index cannot be functional.
+s = box.schema.space.create('withdata')
+---
+...
+lua_code = [[function(tuple) return {tuple[1] + tuple[2], tuple[1] - tuple[2]} end]]
+---
+...
+box.schema.func.create('fextract', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+---
+...
+_ = s:create_index('pk')
+---
+...
+_ = s:create_index('idx', {type = 'rtree', functional_fid = box.func.fextract.id, parts = {{1, 'array'}}})
+---
+- error: 'Can''t create or modify index ''idx'' in space ''withdata'': RTREE index
+    cannot be functional'
+...
+s:drop()
+---
+...
+box.schema.func.drop('fextract')
+---
+...
diff --git a/test/box/rtree_misc.test.lua b/test/box/rtree_misc.test.lua
index 000a928e8..b95e43bb4 100644
--- a/test/box/rtree_misc.test.lua
+++ b/test/box/rtree_misc.test.lua
@@ -243,3 +243,12 @@ s = box.schema.space.create('test')
 _ = s:create_index('primary')
 _ = s:create_index('rtree', {type = 'rtree', parts = {{'[2][*]', 'array'}}})
 s:drop()
+
+-- Rtree index cannot be functional.
+s = box.schema.space.create('withdata')
+lua_code = [[function(tuple) return {tuple[1] + tuple[2], tuple[1] - tuple[2]} end]]
+box.schema.func.create('fextract', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+_ = s:create_index('pk')
+_ = s:create_index('idx', {type = 'rtree', functional_fid = box.func.fextract.id, parts = {{1, 'array'}}})
+s:drop()
+box.schema.func.drop('fextract')
diff --git a/test/engine/engine.cfg b/test/engine/engine.cfg
index 9f07629b4..826e8eb1f 100644
--- a/test/engine/engine.cfg
+++ b/test/engine/engine.cfg
@@ -2,7 +2,10 @@
     "*": {
         "memtx": {"engine": "memtx"}, 
         "vinyl": {"engine": "vinyl"}
-    }
+    },
+    "functional.test.lua": {
+        "memtx": {"engine": "memtx"}
+     }
 }
 
 
diff --git a/test/engine/functional.result b/test/engine/functional.result
new file mode 100644
index 000000000..c06183cda
--- /dev/null
+++ b/test/engine/functional.result
@@ -0,0 +1,708 @@
+-- test-run result file version 2
+test_run = require('test_run').new()
+ | ---
+ | ...
+engine = test_run:get_cfg('engine')
+ | ---
+ | ...
+
+--
+-- gh-1260: Funclional indexes.
+--
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+ | ---
+ | ...
+lua_code2 = [[function(tuple) return {tuple[1] + tuple[2], 2 * tuple[1] + tuple[2]} end]]
+ | ---
+ | ...
+box.schema.func.create('s_nonpersistent')
+ | ---
+ | ...
+box.schema.func.create('s_ivaliddef1', {body = lua_code, is_deterministic = false, is_sandboxed = true})
+ | ---
+ | ...
+box.schema.func.create('s_ivaliddef2', {body = lua_code, is_deterministic = true, is_sandboxed = false})
+ | ---
+ | ...
+
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+box.schema.func.create('ss', {body = lua_code2, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+
+-- Functional index can't be primary.
+_ = s:create_index('idx', {functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+ | ---
+ | - error: 'Can''t create or modify index ''idx'' in space ''withdata'': primary key
+ |     cannot be functional'
+ | ...
+pk = s:create_index('pk')
+ | ---
+ | ...
+-- Invalid fid.
+_ = s:create_index('idx', {functional_fid = 6666, parts = {{1, 'unsigned'}}})
+ | ---
+ | - error: Function '6666' does not exist
+ | ...
+-- Can't use non-persistent function in functional index.
+_ = s:create_index('idx', {functional_fid = box.func.s_nonpersistent.id, parts = {{1, 'unsigned'}}})
+ | ---
+ | - error: 'Wrong index options (field 0): referenced function doesn''t satisfy functional
+ |     index constraints'
+ | ...
+-- Can't use non-deterministic function in functional index.
+_ = s:create_index('idx', {functional_fid = box.func.s_ivaliddef1.id, parts = {{1, 'unsigned'}}})
+ | ---
+ | - error: 'Wrong index options (field 0): referenced function doesn''t satisfy functional
+ |     index constraints'
+ | ...
+-- Can't use non-sandboxed function in functional index.
+_ = s:create_index('idx', {functional_fid = box.func.s_ivaliddef2.id, parts = {{1, 'unsigned'}}})
+ | ---
+ | - error: 'Wrong index options (field 0): referenced function doesn''t satisfy functional
+ |     index constraints'
+ | ...
+-- Can't use non-sequential parts in returned key definition.
+_ = s:create_index('idx', {functional_fid = box.func.ss.id, parts = {{1, 'unsigned'}, {3, 'unsigned'}}})
+ | ---
+ | - error: 'Wrong index options (field 0): invalid functional key definition'
+ | ...
+-- Can't use parts started not by 1 field.
+_ = s:create_index('idx', {functional_fid = box.func.ss.id, parts = {{2, 'unsigned'}, {3, 'unsigned'}}})
+ | ---
+ | - error: 'Wrong index options (field 0): invalid functional key definition'
+ | ...
+-- Can't use JSON paths in returned key definiton.
+_ = s:create_index('idx', {functional_fid = box.func.ss.id, parts = {{"[1]data", 'unsigned'}}})
+ | ---
+ | - error: 'Wrong index options (field 0): invalid functional key definition'
+ | ...
+
+-- Can't drop a function referenced by functional index.
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+ | ---
+ | ...
+box.schema.func.drop('s')
+ | ---
+ | - error: 'Can''t drop function 1: function has references'
+ | ...
+box.snapshot()
+ | ---
+ | - ok
+ | ...
+test_run:cmd("restart server default")
+ | 
+box.schema.func.drop('s')
+ | ---
+ | - error: 'Can''t drop function 1: function has references'
+ | ...
+s = box.space.withdata
+ | ---
+ | ...
+idx = s.index.idx
+ | ---
+ | ...
+idx:drop()
+ | ---
+ | ...
+box.schema.func.drop('s')
+ | ---
+ | ...
+
+test_run = require('test_run').new()
+ | ---
+ | ...
+engine = test_run:get_cfg('engine')
+ | ---
+ | ...
+
+-- Invalid functional index extractor routine return: the extractor must return keys.
+lua_code = [[function(tuple) return "hello" end]]
+ | ---
+ | ...
+box.schema.func.create('invalidreturn0', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+idx = s:create_index('idx', {functional_fid = box.func.invalidreturn0.id, parts = {{1, 'unsigned'}}})
+ | ---
+ | ...
+s:insert({1})
+ | ---
+ | - error: 'Functional index extractor ''invalidreturn0'' error: returned key type is
+ |     invalid'
+ | ...
+idx:drop()
+ | ---
+ | ...
+
+-- Invalid functional index extractor routine return: a stirng instead of unsigned
+lua_code = [[function(tuple) return {"hello"} end]]
+ | ---
+ | ...
+box.schema.func.create('invalidreturn1', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+idx = s:create_index('idx', {functional_fid = box.func.invalidreturn1.id, parts = {{1, 'unsigned'}}})
+ | ---
+ | ...
+s:insert({1})
+ | ---
+ | - error: 'Functional index extractor ''invalidreturn1'' error: Supplied key type of
+ |     part 0 does not match index part type: expected unsigned'
+ | ...
+idx:drop()
+ | ---
+ | ...
+
+-- Invalid functional index extractor routine return: undefined multikey return.
+lua_code = [[function(tuple) return {"hello", "world"}, {"my", "hart"} end]]
+ | ---
+ | ...
+box.schema.func.create('invalidreturn2', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+idx = s:create_index('idx', {functional_fid = box.func.invalidreturn2.id, parts = {{1, 'string'}, {2, 'string'}}})
+ | ---
+ | ...
+s:insert({1})
+ | ---
+ | - error: 'Functional index extractor ''invalidreturn2'' error: to many keys were returned'
+ | ...
+idx:drop()
+ | ---
+ | ...
+
+-- Invalid functional index extractor routine return: the second returned key invalid.
+lua_code = [[function(tuple) return {"hello", "world"}, {1, 2} end]]
+ | ---
+ | ...
+box.schema.func.create('invalidreturn3', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+idx = s:create_index('idx', {functional_fid = box.func.invalidreturn3.id, is_multikey = true, parts = {{1, 'unsigned'}, {2, 'unsigned'}}})
+ | ---
+ | ...
+s:insert({1})
+ | ---
+ | - error: 'Functional index extractor ''invalidreturn3'' error: Supplied key type of
+ |     part 0 does not match index part type: expected unsigned'
+ | ...
+idx:drop()
+ | ---
+ | ...
+
+-- Invalid functional extractor: runtime extractor error
+test_run:cmd("setopt delimiter ';'")
+ | ---
+ | - true
+ | ...
+lua_code = [[function(tuple)
+                local json = require('json')
+                return json.encode(tuple)
+             end]]
+test_run:cmd("setopt delimiter ''");
+ | ---
+ | ...
+box.schema.func.create('runtimeerror', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+idx = s:create_index('idx', {functional_fid = box.func.runtimeerror.id, parts = {{1, 'string'}}})
+ | ---
+ | ...
+s:insert({1})
+ | ---
+ | - error: 'Functional index extractor ''runtimeerror'' error: [string "return function(tuple)                 local
+ |     ..."]:1: attempt to call global ''require'' (a nil value)'
+ | ...
+idx:drop()
+ | ---
+ | ...
+
+-- Remove old persistent functions
+for _, v in pairs(box.func) do if v.is_persistent then box.schema.func.drop(v.name) end end
+ | ---
+ | ...
+s:drop()
+ | ---
+ | ...
+
+-- Functional test cases.
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+ | ---
+ | ...
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+pk = s:create_index('pk')
+ | ---
+ | ...
+s:insert({1, 2})
+ | ---
+ | - [1, 2]
+ | ...
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.extr.id, parts = {{1, 'integer'}}})
+ | ---
+ | ...
+s:insert({2, 1})
+ | ---
+ | - error: Duplicate key exists in unique index 'idx' in space 'withdata'
+ | ...
+idx:get(3)
+ | ---
+ | - [1, 2]
+ | ...
+idx:delete(3)
+ | ---
+ | - [1, 2]
+ | ...
+s:select()
+ | ---
+ | - []
+ | ...
+s:insert({2, 1})
+ | ---
+ | - [2, 1]
+ | ...
+idx:get(3)
+ | ---
+ | - [2, 1]
+ | ...
+s:drop()
+ | ---
+ | ...
+box.schema.func.drop('extr')
+ | ---
+ | ...
+collectgarbage()
+ | ---
+ | - 0
+ | ...
+
+-- Multikey functional index.
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]}, {tuple[1] + tuple[2]}, {tuple[1]} end]]
+ | ---
+ | ...
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+pk = s:create_index('pk')
+ | ---
+ | ...
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.extr.id, is_multikey = true, parts = {{1, 'integer'}}})
+ | ---
+ | ...
+s:insert({1, 2})
+ | ---
+ | - [1, 2]
+ | ...
+s:insert({3, 5})
+ | ---
+ | - error: Duplicate key exists in unique index 'idx' in space 'withdata'
+ | ...
+s:insert({5, 3})
+ | ---
+ | - [5, 3]
+ | ...
+idx:select()
+ | ---
+ | - - [1, 2]
+ |   - [1, 2]
+ |   - [5, 3]
+ |   - [5, 3]
+ | ...
+idx:get(8)
+ | ---
+ | - [5, 3]
+ | ...
+idx:get(3)
+ | ---
+ | - [1, 2]
+ | ...
+idx:get(1)
+ | ---
+ | - [1, 2]
+ | ...
+idx:get(5)
+ | ---
+ | - [5, 3]
+ | ...
+s:drop()
+ | ---
+ | ...
+collectgarbage()
+ | ---
+ | - 0
+ | ...
+box.schema.func.drop('extr')
+ | ---
+ | ...
+
+-- Multikey multipart functional index.
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+lua_code = [[function(tuple) return {600 + tuple[1], 600 + tuple[2]}, {500 + tuple[1], 500 + tuple[2]} end]]
+ | ---
+ | ...
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+pk = s:create_index('pk')
+ | ---
+ | ...
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.extr.id, is_multikey = true, parts = {{1, 'integer'}, {2, 'integer'}}})
+ | ---
+ | ...
+s:insert({1, 2})
+ | ---
+ | - [1, 2]
+ | ...
+s:insert({2, 1})
+ | ---
+ | - [2, 1]
+ | ...
+s:insert({3, 3})
+ | ---
+ | - [3, 3]
+ | ...
+idx:select({600}, {iterator = "GE"})
+ | ---
+ | - - [1, 2]
+ |   - [2, 1]
+ |   - [3, 3]
+ | ...
+idx:get({603, 603})
+ | ---
+ | - [3, 3]
+ | ...
+idx:select({503}, {iterator = "LE"})
+ | ---
+ | - - [3, 3]
+ |   - [2, 1]
+ |   - [1, 2]
+ | ...
+s:drop()
+ | ---
+ | ...
+collectgarbage()
+ | ---
+ | - 0
+ | ...
+box.schema.func.drop('extr')
+ | ---
+ | ...
+
+-- Multikey non-unique functional index.
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+lua_code = [[function(tuple) return {500 + tuple[1]}, {500 + tuple[2]}, {500 + tuple[2]} end]]
+ | ---
+ | ...
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+pk = s:create_index('pk')
+ | ---
+ | ...
+idx = s:create_index('idx', {unique = false, functional_fid = box.func.extr.id, is_multikey = true, parts = {{1, 'integer'}}})
+ | ---
+ | ...
+s:insert({1, 2})
+ | ---
+ | - [1, 2]
+ | ...
+s:insert({2, 1})
+ | ---
+ | - [2, 1]
+ | ...
+idx:select({501})
+ | ---
+ | - - [1, 2]
+ |   - [2, 1]
+ | ...
+idx:select({502})
+ | ---
+ | - - [1, 2]
+ |   - [2, 1]
+ | ...
+s:replace({1, 3})
+ | ---
+ | - [1, 3]
+ | ...
+idx:select({501})
+ | ---
+ | - - [1, 3]
+ |   - [2, 1]
+ | ...
+idx:select({502})
+ | ---
+ | - - [2, 1]
+ | ...
+idx:select({503})
+ | ---
+ | - - [1, 3]
+ | ...
+box.snapshot()
+ | ---
+ | - ok
+ | ...
+test_run:cmd("restart server default")
+ | 
+s = box.space.withdata
+ | ---
+ | ...
+idx = s.index.idx
+ | ---
+ | ...
+idx:select({501})
+ | ---
+ | - - [1, 3]
+ |   - [2, 1]
+ | ...
+idx:select({502})
+ | ---
+ | - - [2, 1]
+ | ...
+idx:select({503})
+ | ---
+ | - - [1, 3]
+ | ...
+s:replace({1, 2})
+ | ---
+ | - [1, 2]
+ | ...
+idx:select({501})
+ | ---
+ | - - [1, 2]
+ |   - [2, 1]
+ | ...
+idx:select({502})
+ | ---
+ | - - [1, 2]
+ |   - [2, 1]
+ | ...
+idx:select({503})
+ | ---
+ | - []
+ | ...
+s:drop()
+ | ---
+ | ...
+collectgarbage()
+ | ---
+ | - 0
+ | ...
+box.schema.func.drop('extr')
+ | ---
+ | ...
+
+-- Multikey UTF-8 address extractor
+test_run = require('test_run').new()
+ | ---
+ | ...
+engine = test_run:get_cfg('engine')
+ | ---
+ | ...
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+pk = s:create_index('name', {parts = {1, 'string'}})
+ | ---
+ | ...
+s:insert({"James", "SIS Building Lambeth London UK"})
+ | ---
+ | - ['James', 'SIS Building Lambeth London UK']
+ | ...
+s:insert({"Sherlock", "221B Baker St Marylebone London NW1 6XE UK"})
+ | ---
+ | - ['Sherlock', '221B Baker St Marylebone London NW1 6XE UK']
+ | ...
+-- Create functional index on space with data
+test_run:cmd("setopt delimiter ';'")
+ | ---
+ | - true
+ | ...
+lua_code = [[function(tuple)
+                local address = string.split(tuple[2])
+                local ret = {}
+                for _, v in pairs(address) do table.insert(ret, {utf8.upper(v)}) end
+                return unpack(ret)
+             end]]
+test_run:cmd("setopt delimiter ''");
+ | ---
+ | ...
+box.schema.func.create('addr_extractor', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+idx = s:create_index('addr', {unique = false, functional_fid = box.func.addr_extractor.id, is_multikey = true, parts = {{1, 'string', collation = 'unicode_ci'}}})
+ | ---
+ | ...
+idx:select('uk')
+ | ---
+ | - - ['James', 'SIS Building Lambeth London UK']
+ |   - ['Sherlock', '221B Baker St Marylebone London NW1 6XE UK']
+ | ...
+idx:select('Sis')
+ | ---
+ | - - ['James', 'SIS Building Lambeth London UK']
+ | ...
+s:drop()
+ | ---
+ | ...
+collectgarbage()
+ | ---
+ | - 0
+ | ...
+box.schema.func.drop('addr_extractor')
+ | ---
+ | ...
+
+-- Partial index with functional index extractor
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+pk = s:create_index('pk')
+ | ---
+ | ...
+lua_code = [[function(tuple) if tuple[1] % 2 == 1 then return {tuple[1]} end end]]
+ | ---
+ | ...
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.extr.id, is_multikey = true, parts = {{1, 'integer'}}})
+ | ---
+ | ...
+s:insert({1})
+ | ---
+ | - [1]
+ | ...
+s:insert({2})
+ | ---
+ | - [2]
+ | ...
+s:insert({3})
+ | ---
+ | - [3]
+ | ...
+s:insert({4})
+ | ---
+ | - [4]
+ | ...
+idx:select()
+ | ---
+ | - - [1]
+ |   - [3]
+ | ...
+s:drop()
+ | ---
+ | ...
+collectgarbage()
+ | ---
+ | - 0
+ | ...
+box.schema.func.drop('extr')
+ | ---
+ | ...
+
+-- Return nil from functional index extractor.
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+pk = s:create_index('pk')
+ | ---
+ | ...
+lua_code = [[function(tuple) return {nil} end]]
+ | ---
+ | ...
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+idx = s:create_index('idx', {unique = false, functional_fid = box.func.extr.id, parts = {{1, 'integer', is_nullable = true}}})
+ | ---
+ | ...
+s:insert({1})
+ | ---
+ | - error: 'Functional index extractor ''extr'' error: Invalid key part count in an
+ |     exact match (expected 1, got 0)'
+ | ...
+s:drop()
+ | ---
+ | ...
+collectgarbage()
+ | ---
+ | - 0
+ | ...
+box.schema.func.drop('extr')
+ | ---
+ | ...
+
+-- Multiple functional indexes
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+ | ---
+ | ...
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+lua_code = [[function(tuple) return {tuple[1] - tuple[2]} end]]
+ | ---
+ | ...
+box.schema.func.create('sub', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+pk = s:create_index('pk')
+ | ---
+ | ...
+idx1 = s:create_index('s_idx', {unique = true, functional_fid = box.func.s.id, parts = {{1, 'integer'}}})
+ | ---
+ | ...
+idx2 = s:create_index('sub_idx', {unique = true, functional_fid = box.func.sub.id, parts = {{1, 'integer'}}})
+ | ---
+ | ...
+s:insert({4, 1})
+ | ---
+ | - [4, 1]
+ | ...
+idx1:get(5)
+ | ---
+ | - [4, 1]
+ | ...
+idx2:get(3)
+ | ---
+ | - [4, 1]
+ | ...
+idx1:drop()
+ | ---
+ | ...
+idx2:get(3)
+ | ---
+ | - [4, 1]
+ | ...
+s:drop()
+ | ---
+ | ...
+collectgarbage()
+ | ---
+ | - 0
+ | ...
+box.schema.func.drop('s')
+ | ---
+ | ...
+box.schema.func.drop('sub')
+ | ---
+ | ...
diff --git a/test/engine/functional.test.lua b/test/engine/functional.test.lua
new file mode 100644
index 000000000..a84b5e38e
--- /dev/null
+++ b/test/engine/functional.test.lua
@@ -0,0 +1,240 @@
+test_run = require('test_run').new()
+engine = test_run:get_cfg('engine')
+
+--
+-- gh-1260: Funclional indexes.
+--
+s = box.schema.space.create('withdata', {engine = engine})
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+lua_code2 = [[function(tuple) return {tuple[1] + tuple[2], 2 * tuple[1] + tuple[2]} end]]
+box.schema.func.create('s_nonpersistent')
+box.schema.func.create('s_ivaliddef1', {body = lua_code, is_deterministic = false, is_sandboxed = true})
+box.schema.func.create('s_ivaliddef2', {body = lua_code, is_deterministic = true, is_sandboxed = false})
+
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+box.schema.func.create('ss', {body = lua_code2, is_deterministic = true, is_sandboxed = true})
+
+-- Functional index can't be primary.
+_ = s:create_index('idx', {functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+pk = s:create_index('pk')
+-- Invalid fid.
+_ = s:create_index('idx', {functional_fid = 6666, parts = {{1, 'unsigned'}}})
+-- Can't use non-persistent function in functional index.
+_ = s:create_index('idx', {functional_fid = box.func.s_nonpersistent.id, parts = {{1, 'unsigned'}}})
+-- Can't use non-deterministic function in functional index.
+_ = s:create_index('idx', {functional_fid = box.func.s_ivaliddef1.id, parts = {{1, 'unsigned'}}})
+-- Can't use non-sandboxed function in functional index.
+_ = s:create_index('idx', {functional_fid = box.func.s_ivaliddef2.id, parts = {{1, 'unsigned'}}})
+-- Can't use non-sequential parts in returned key definition.
+_ = s:create_index('idx', {functional_fid = box.func.ss.id, parts = {{1, 'unsigned'}, {3, 'unsigned'}}})
+-- Can't use parts started not by 1 field.
+_ = s:create_index('idx', {functional_fid = box.func.ss.id, parts = {{2, 'unsigned'}, {3, 'unsigned'}}})
+-- Can't use JSON paths in returned key definiton.
+_ = s:create_index('idx', {functional_fid = box.func.ss.id, parts = {{"[1]data", 'unsigned'}}})
+
+-- Can't drop a function referenced by functional index.
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+box.schema.func.drop('s')
+box.snapshot()
+test_run:cmd("restart server default")
+box.schema.func.drop('s')
+s = box.space.withdata
+idx = s.index.idx
+idx:drop()
+box.schema.func.drop('s')
+
+test_run = require('test_run').new()
+engine = test_run:get_cfg('engine')
+
+-- Invalid functional index extractor routine return: the extractor must return keys.
+lua_code = [[function(tuple) return "hello" end]]
+box.schema.func.create('invalidreturn0', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+idx = s:create_index('idx', {functional_fid = box.func.invalidreturn0.id, parts = {{1, 'unsigned'}}})
+s:insert({1})
+idx:drop()
+
+-- Invalid functional index extractor routine return: a stirng instead of unsigned
+lua_code = [[function(tuple) return {"hello"} end]]
+box.schema.func.create('invalidreturn1', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+idx = s:create_index('idx', {functional_fid = box.func.invalidreturn1.id, parts = {{1, 'unsigned'}}})
+s:insert({1})
+idx:drop()
+
+-- Invalid functional index extractor routine return: undefined multikey return.
+lua_code = [[function(tuple) return {"hello", "world"}, {"my", "hart"} end]]
+box.schema.func.create('invalidreturn2', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+idx = s:create_index('idx', {functional_fid = box.func.invalidreturn2.id, parts = {{1, 'string'}, {2, 'string'}}})
+s:insert({1})
+idx:drop()
+
+-- Invalid functional index extractor routine return: the second returned key invalid.
+lua_code = [[function(tuple) return {"hello", "world"}, {1, 2} end]]
+box.schema.func.create('invalidreturn3', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+idx = s:create_index('idx', {functional_fid = box.func.invalidreturn3.id, is_multikey = true, parts = {{1, 'unsigned'}, {2, 'unsigned'}}})
+s:insert({1})
+idx:drop()
+
+-- Invalid functional extractor: runtime extractor error
+test_run:cmd("setopt delimiter ';'")
+lua_code = [[function(tuple)
+                local json = require('json')
+                return json.encode(tuple)
+             end]]
+test_run:cmd("setopt delimiter ''");
+box.schema.func.create('runtimeerror', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+idx = s:create_index('idx', {functional_fid = box.func.runtimeerror.id, parts = {{1, 'string'}}})
+s:insert({1})
+idx:drop()
+
+-- Remove old persistent functions
+for _, v in pairs(box.func) do if v.is_persistent then box.schema.func.drop(v.name) end end
+s:drop()
+
+-- Functional test cases.
+s = box.schema.space.create('withdata', {engine = engine})
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+pk = s:create_index('pk')
+s:insert({1, 2})
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.extr.id, parts = {{1, 'integer'}}})
+s:insert({2, 1})
+idx:get(3)
+idx:delete(3)
+s:select()
+s:insert({2, 1})
+idx:get(3)
+s:drop()
+box.schema.func.drop('extr')
+collectgarbage()
+
+-- Multikey functional index.
+s = box.schema.space.create('withdata', {engine = engine})
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]}, {tuple[1] + tuple[2]}, {tuple[1]} end]]
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+pk = s:create_index('pk')
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.extr.id, is_multikey = true, parts = {{1, 'integer'}}})
+s:insert({1, 2})
+s:insert({3, 5})
+s:insert({5, 3})
+idx:select()
+idx:get(8)
+idx:get(3)
+idx:get(1)
+idx:get(5)
+s:drop()
+collectgarbage()
+box.schema.func.drop('extr')
+
+-- Multikey multipart functional index.
+s = box.schema.space.create('withdata', {engine = engine})
+lua_code = [[function(tuple) return {600 + tuple[1], 600 + tuple[2]}, {500 + tuple[1], 500 + tuple[2]} end]]
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+pk = s:create_index('pk')
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.extr.id, is_multikey = true, parts = {{1, 'integer'}, {2, 'integer'}}})
+s:insert({1, 2})
+s:insert({2, 1})
+s:insert({3, 3})
+idx:select({600}, {iterator = "GE"})
+idx:get({603, 603})
+idx:select({503}, {iterator = "LE"})
+s:drop()
+collectgarbage()
+box.schema.func.drop('extr')
+
+-- Multikey non-unique functional index.
+s = box.schema.space.create('withdata', {engine = engine})
+lua_code = [[function(tuple) return {500 + tuple[1]}, {500 + tuple[2]}, {500 + tuple[2]} end]]
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+pk = s:create_index('pk')
+idx = s:create_index('idx', {unique = false, functional_fid = box.func.extr.id, is_multikey = true, parts = {{1, 'integer'}}})
+s:insert({1, 2})
+s:insert({2, 1})
+idx:select({501})
+idx:select({502})
+s:replace({1, 3})
+idx:select({501})
+idx:select({502})
+idx:select({503})
+box.snapshot()
+test_run:cmd("restart server default")
+s = box.space.withdata
+idx = s.index.idx
+idx:select({501})
+idx:select({502})
+idx:select({503})
+s:replace({1, 2})
+idx:select({501})
+idx:select({502})
+idx:select({503})
+s:drop()
+collectgarbage()
+box.schema.func.drop('extr')
+
+-- Multikey UTF-8 address extractor
+test_run = require('test_run').new()
+engine = test_run:get_cfg('engine')
+s = box.schema.space.create('withdata', {engine = engine})
+pk = s:create_index('name', {parts = {1, 'string'}})
+s:insert({"James", "SIS Building Lambeth London UK"})
+s:insert({"Sherlock", "221B Baker St Marylebone London NW1 6XE UK"})
+-- Create functional index on space with data
+test_run:cmd("setopt delimiter ';'")
+lua_code = [[function(tuple)
+                local address = string.split(tuple[2])
+                local ret = {}
+                for _, v in pairs(address) do table.insert(ret, {utf8.upper(v)}) end
+                return unpack(ret)
+             end]]
+test_run:cmd("setopt delimiter ''");
+box.schema.func.create('addr_extractor', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+idx = s:create_index('addr', {unique = false, functional_fid = box.func.addr_extractor.id, is_multikey = true, parts = {{1, 'string', collation = 'unicode_ci'}}})
+idx:select('uk')
+idx:select('Sis')
+s:drop()
+collectgarbage()
+box.schema.func.drop('addr_extractor')
+
+-- Partial index with functional index extractor
+s = box.schema.space.create('withdata', {engine = engine})
+pk = s:create_index('pk')
+lua_code = [[function(tuple) if tuple[1] % 2 == 1 then return {tuple[1]} end end]]
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+idx = s:create_index('idx', {unique = true, functional_fid = box.func.extr.id, is_multikey = true, parts = {{1, 'integer'}}})
+s:insert({1})
+s:insert({2})
+s:insert({3})
+s:insert({4})
+idx:select()
+s:drop()
+collectgarbage()
+box.schema.func.drop('extr')
+
+-- Return nil from functional index extractor.
+s = box.schema.space.create('withdata', {engine = engine})
+pk = s:create_index('pk')
+lua_code = [[function(tuple) return {nil} end]]
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+idx = s:create_index('idx', {unique = false, functional_fid = box.func.extr.id, parts = {{1, 'integer', is_nullable = true}}})
+s:insert({1})
+s:drop()
+collectgarbage()
+box.schema.func.drop('extr')
+
+-- Multiple functional indexes
+s = box.schema.space.create('withdata', {engine = engine})
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+lua_code = [[function(tuple) return {tuple[1] - tuple[2]} end]]
+box.schema.func.create('sub', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+pk = s:create_index('pk')
+idx1 = s:create_index('s_idx', {unique = true, functional_fid = box.func.s.id, parts = {{1, 'integer'}}})
+idx2 = s:create_index('sub_idx', {unique = true, functional_fid = box.func.sub.id, parts = {{1, 'integer'}}})
+s:insert({4, 1})
+idx1:get(5)
+idx2:get(3)
+idx1:drop()
+idx2:get(3)
+s:drop()
+collectgarbage()
+box.schema.func.drop('s')
+box.schema.func.drop('sub')
diff --git a/test/unit/luaT_tuple_new.c b/test/unit/luaT_tuple_new.c
index 0a16fa039..d5c59c975 100644
--- a/test/unit/luaT_tuple_new.c
+++ b/test/unit/luaT_tuple_new.c
@@ -124,7 +124,7 @@ test_basic(struct lua_State *L)
 	part.nullable_action = ON_CONFLICT_ACTION_DEFAULT;
 	part.sort_order = SORT_ORDER_ASC;
 	part.path = NULL;
-	struct key_def *key_def = key_def_new(&part, 1);
+	struct key_def *key_def = key_def_new(&part, 1, false, false);
 	box_tuple_format_t *another_format = box_tuple_format_new(&key_def, 1);
 	key_def_delete(key_def);
 
diff --git a/test/unit/merger.test.c b/test/unit/merger.test.c
index b4a989a20..4bf1d8bfb 100644
--- a/test/unit/merger.test.c
+++ b/test/unit/merger.test.c
@@ -214,7 +214,8 @@ test_merger(struct tuple_format *format)
 		merge_source_array_new(true),
 	};
 
-	struct key_def *key_def = key_def_new(&key_part_unsigned, 1);
+	struct key_def *key_def = key_def_new(&key_part_unsigned, 1,
+					      false, false);
 	struct merge_source *merger = merger_new(key_def, sources, source_count,
 						 false);
 	key_def_delete(key_def);
@@ -252,7 +253,8 @@ test_basic()
 	plan(4);
 	header();
 
-	struct key_def *key_def = key_def_new(&key_part_integer, 1);
+	struct key_def *key_def = key_def_new(&key_part_integer, 1,
+					      false, false);
 	struct tuple_format *format = box_tuple_format_new(&key_def, 1);
 	assert(format != NULL);
 
@@ -273,7 +275,7 @@ main()
 {
 	memory_init();
 	fiber_init(fiber_c_invoke);
-	tuple_init(NULL);
+	tuple_init(NULL, NULL, NULL);
 
 	int rc = test_basic();
 
diff --git a/test/unit/tuple_bigref.c b/test/unit/tuple_bigref.c
index 20eab61f6..bc207c1d8 100644
--- a/test/unit/tuple_bigref.c
+++ b/test/unit/tuple_bigref.c
@@ -143,7 +143,7 @@ main()
 
 	memory_init();
 	fiber_init(fiber_c_invoke);
-	tuple_init(NULL);
+	tuple_init(NULL, NULL, NULL);
 
 	tuple_end = mp_encode_array(tuple_end, 1);
 	tuple_end = mp_encode_uint(tuple_end, 2);
diff --git a/test/unit/vy_iterators_helper.c b/test/unit/vy_iterators_helper.c
index 0d20f19ef..9d8522698 100644
--- a/test/unit/vy_iterators_helper.c
+++ b/test/unit/vy_iterators_helper.c
@@ -18,7 +18,7 @@ vy_iterator_C_test_init(size_t cache_size)
 
 	memory_init();
 	fiber_init(fiber_c_invoke);
-	tuple_init(NULL);
+	tuple_init(NULL, NULL, NULL);
 	vy_stmt_env_create(&stmt_env);
 	vy_cache_env_create(&cache_env, cord_slab_cache());
 	vy_cache_env_set_quota(&cache_env, cache_size);
diff --git a/test/vinyl/misc.result b/test/vinyl/misc.result
index b2aacdc55..af8502dba 100644
--- a/test/vinyl/misc.result
+++ b/test/vinyl/misc.result
@@ -432,3 +432,26 @@ stat.bytes_compressed < stat.bytes / 10
 s:drop()
 ---
 ...
+-- Vinyl doesn't support functional indexes.
+s = box.schema.space.create('withdata', {engine = 'vinyl'})
+---
+...
+lua_code = [[function(tuple) return tuple[1] + tuple[2] end]]
+---
+...
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+---
+...
+_ = s:create_index('pk')
+---
+...
+_ = s:create_index('idx', {functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+---
+- error: Vinyl does not support functional indexes
+...
+s:drop()
+---
+...
+box.schema.func.drop('s')
+---
+...
diff --git a/test/vinyl/misc.test.lua b/test/vinyl/misc.test.lua
index f8da578d0..2016ca45f 100644
--- a/test/vinyl/misc.test.lua
+++ b/test/vinyl/misc.test.lua
@@ -182,3 +182,12 @@ test_run:wait_cond(function() return i:stat().disk.compaction.count > 0 end)
 stat = i:stat().disk
 stat.bytes_compressed < stat.bytes / 10
 s:drop()
+
+-- Vinyl doesn't support functional indexes.
+s = box.schema.space.create('withdata', {engine = 'vinyl'})
+lua_code = [[function(tuple) return tuple[1] + tuple[2] end]]
+box.schema.func.create('s', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+_ = s:create_index('pk')
+_ = s:create_index('idx', {functional_fid = box.func.s.id, parts = {{1, 'unsigned'}}})
+s:drop()
+box.schema.func.drop('s')
diff --git a/test/wal_off/alter.result b/test/wal_off/alter.result
index bce15711d..62cb11db7 100644
--- a/test/wal_off/alter.result
+++ b/test/wal_off/alter.result
@@ -28,7 +28,7 @@ end;
 ...
 #spaces;
 ---
-- 65503
+- 65502
 ...
 -- cleanup
 for k, v in pairs(spaces) do
-- 
2.22.0




More information about the Tarantool-patches mailing list