[PATCH v1 04/12] box: introduce Lua persistent functions

Kirill Shcherbatov kshcherbatov at tarantool.org
Mon Jul 8 14:26:21 MSK 2019


Closes #4182
Closes #4219
Needed for #1260

@TarantoolBot document
Title: Persistent Lua functions

Now Tarantool supports 'persistent' Lua functions.
Such functions are stored in snapshot and are available after
restart.
To create a persistent Lua function, specify a function body
in box.schema.func.create call:
e.g. body = "function(a, b) return a + b end"

A Lua persistent function may be 'sandboxed'. The 'sandboxed'
function is executed in isolated environment:
  a. only limited set of Lua functions and modules are available:
    -assert -error -pairs -ipairs -next -pcall -xpcall -type
    -print -select -string -tonumber -tostring -unpack -math -utf8;
  b. global variables are forbidden

Finally, the new 'is_deterministic' flag allows to mark a
registered function as deterministic, i.e. the function that
can produce only one result for a given list of parameters.

The new box.schema.func.create interface is:
box.schema.func.create('funcname', <setuid = true|FALSE>,
	<if_not_exists = true|FALSE>, <language = LUA|c>,
	<body = string ('')>, <is_deterministic = true|FALSE>,
	<is_sandboxed = true|FALSE>, <comment = string ('')>)

This schema change is also reserves names for sql builtin
functions:
    TRIM, TYPEOF, PRINTF, UNICODE, CHAR, HEX, VERSION,
    QUOTE, REPLACE, SUBSTR, GROUP_CONCAT, JULIANDAY, DATE,
    TIME, DATETIME, STRFTIME, CURRENT_TIME, CURRENT_TIMESTAMP,
    CURRENT_DATE, LENGTH, POSITION, ROUND, UPPER, LOWER,
    IFNULL, RANDOM, CEIL, CEILING, CHARACTER_LENGTH,
    CHAR_LENGTH, FLOOR, MOD, OCTET_LENGTH, ROW_COUNT, COUNT,
    LIKE, ABS, EXP, LN, POWER, SQRT, SUM, TOTAL, AVG,
    RANDOMBLOB, NULLIF, ZEROBLOB, MIN, MAX, COALESCE, EVERY,
    EXISTS, EXTRACT, SOME, _sql_record, _sql_stat_get,
    _sql_stat_push, _sql_stat_init, LUA

A new Lua persistent function LUA is introduced to evaluate
LUA strings from SQL in future.

This names could not be used for user-defined functions.

Example:
lua_code = [[function(a, b) return a + b end]]
box.schema.func.create('summarize', {body = lua_code,
		is_deterministic = true, is_sandboxed = true})
box.func.summarize
---
- aggregate: none
  returns: any
  exports:
    lua: true
    sql: false
  id: 60
  is_sandboxed: true
  setuid: false
  is_deterministic: true
  body: function(a, b) return a + b end
  name: summarize
  language: LUA
...
box.func.summarize:call({1, 3})
---
- 4
...
---
 src/box/alter.cc              | 154 +++++++++++++++++--
 src/box/bootstrap.            | Bin 0 -> 5528 bytes
 src/box/bootstrap.snap        | Bin 4475 -> 5541 bytes
 src/box/func.c                |  22 ++-
 src/box/func_def.c            |  24 ++-
 src/box/func_def.h            |  59 +++++++-
 src/box/lua/call.c            | 241 +++++++++++++++++++++++++++++-
 src/box/lua/schema.lua        |  19 ++-
 src/box/lua/upgrade.lua       |  63 +++++++-
 src/box/schema_def.h          |  14 ++
 src/box/sql.h                 |   5 +
 src/box/sql/func.c            |  43 ++++++
 test-run                      |   2 +-
 test/box-py/bootstrap.result  |  12 +-
 test/box-py/bootstrap.test.py |   2 +-
 test/box/access_misc.result   |  10 +-
 test/box/function1.result     | 273 +++++++++++++++++++++++++++++++++-
 test/box/function1.test.lua   |  98 +++++++++++-
 test/wal_off/func_max.result  |   8 +-
 19 files changed, 1000 insertions(+), 49 deletions(-)
 create mode 100644 src/box/bootstrap.

diff --git a/src/box/alter.cc b/src/box/alter.cc
index ce0cf2d9b..c92a1f710 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -2624,35 +2624,88 @@ func_def_get_ids_from_tuple(struct tuple *tuple, uint32_t *fid, uint32_t *uid)
 static struct func_def *
 func_def_new_from_tuple(struct tuple *tuple)
 {
-	uint32_t len;
-	const char *name = tuple_field_str_xc(tuple, BOX_FUNC_FIELD_NAME,
-					      &len);
-	if (len > BOX_NAME_MAX)
+	uint32_t field_count = tuple_field_count(tuple);
+	uint32_t name_len, body_len, comment_len;
+	const char *name, *body, *comment;
+	name = tuple_field_str_xc(tuple, BOX_FUNC_FIELD_NAME, &name_len);
+	if (name_len > BOX_NAME_MAX) {
 		tnt_raise(ClientError, ER_CREATE_FUNCTION,
 			  tt_cstr(name, BOX_INVALID_NAME_MAX),
 			  "function name is too long");
-	identifier_check_xc(name, len);
-	struct func_def *def = (struct func_def *) malloc(func_def_sizeof(len));
+	}
+	identifier_check_xc(name, name_len);
+	if (field_count > BOX_FUNC_FIELD_BODY) {
+		body = tuple_field_str_xc(tuple, BOX_FUNC_FIELD_BODY,
+					  &body_len);
+		comment = tuple_field_str_xc(tuple, BOX_FUNC_FIELD_COMMENT,
+					     &comment_len);
+		uint32_t len;
+		const char *routine_type = tuple_field_str_xc(tuple,
+					BOX_FUNC_FIELD_ROUTINE_TYPE, &len);
+		if (len != strlen("function") ||
+		    strncasecmp(routine_type, "function", len) != 0) {
+			tnt_raise(ClientError, ER_CREATE_FUNCTION, name,
+				  "unsupported routine_type value");
+		}
+		const char *sql_data_access = tuple_field_str_xc(tuple,
+					BOX_FUNC_FIELD_SQL_DATA_ACCESS, &len);
+		if (len != strlen("none") ||
+		    strncasecmp(sql_data_access, "none", len) != 0) {
+			tnt_raise(ClientError, ER_CREATE_FUNCTION, name,
+				  "unsupported sql_data_access value");
+		}
+		bool is_null_call = tuple_field_bool_xc(tuple,
+						BOX_FUNC_FIELD_IS_NULL_CALL);
+		if (is_null_call != true) {
+			tnt_raise(ClientError, ER_CREATE_FUNCTION, name,
+				  "unsupported is_null_call value");
+		}
+	} else {
+		body = NULL;
+		body_len = 0;
+		comment = NULL;
+		comment_len = 0;
+	}
+	uint32_t body_offset, comment_offset;
+	uint32_t def_sz = func_def_sizeof(name_len, body_len, comment_len,
+					  &body_offset, &comment_offset);
+	struct func_def *def =
+		(struct func_def *) malloc(def_sz);
 	if (def == NULL)
-		tnt_raise(OutOfMemory, func_def_sizeof(len), "malloc", "def");
+		tnt_raise(OutOfMemory, def_sz, "malloc", "def");
 	auto def_guard = make_scoped_guard([=] { free(def); });
 	func_def_get_ids_from_tuple(tuple, &def->fid, &def->uid);
 	if (def->fid > BOX_FUNCTION_MAX) {
 		tnt_raise(ClientError, ER_CREATE_FUNCTION,
-			  tt_cstr(name, len), "function id is too big");
+			  tt_cstr(name, name_len), "function id is too big");
+	}
+	memcpy(def->name, name, name_len);
+	def->name[name_len] = 0;
+	def->name_len = name_len;
+	if (body_len > 0) {
+		def->body = (char *)def + body_offset;
+		memcpy(def->body, body, body_len);
+		def->body[body_len] = 0;
+	} else {
+		def->body = NULL;
 	}
-	memcpy(def->name, name, len);
-	def->name[len] = 0;
-	def->name_len = len;
-	if (tuple_field_count(tuple) > BOX_FUNC_FIELD_SETUID)
+	if (comment_len > 0) {
+		def->comment = (char *)def + comment_offset;
+		memcpy(def->comment, comment, comment_len);
+		def->comment[comment_len] = 0;
+	} else {
+		def->comment = NULL;
+	}
+	if (field_count > BOX_FUNC_FIELD_SETUID)
 		def->setuid = tuple_field_u32_xc(tuple, BOX_FUNC_FIELD_SETUID);
 	else
 		def->setuid = false;
-	if (tuple_field_count(tuple) > BOX_FUNC_FIELD_LANGUAGE) {
+	if (field_count > BOX_FUNC_FIELD_LANGUAGE) {
 		const char *language =
 			tuple_field_cstr_xc(tuple, BOX_FUNC_FIELD_LANGUAGE);
 		def->language = STR2ENUM(func_language, language);
-		if (def->language == func_language_MAX) {
+		if (def->language == func_language_MAX ||
+		    def->language == FUNC_LANGUAGE_SQL) {
 			tnt_raise(ClientError, ER_FUNCTION_LANGUAGE,
 				  language, def->name);
 		}
@@ -2660,6 +2713,79 @@ func_def_new_from_tuple(struct tuple *tuple)
 		/* Lua is the default. */
 		def->language = FUNC_LANGUAGE_LUA;
 	}
+	if (field_count > BOX_FUNC_FIELD_BODY) {
+		def->is_deterministic =
+			tuple_field_bool_xc(tuple,
+					    BOX_FUNC_FIELD_IS_DETERMINISTIC);
+		def->is_sandboxed =
+			tuple_field_bool_xc(tuple,
+					    BOX_FUNC_FIELD_IS_SANDBOXED);
+		const char *returns =
+			tuple_field_cstr_xc(tuple, BOX_FUNC_FIELD_RETURNS);
+		def->returns = STR2ENUM(field_type, returns);
+		if (def->returns == field_type_MAX) {
+			tnt_raise(ClientError, ER_CREATE_FUNCTION,
+				  def->name, "invalid returns value");
+		}
+		def->exports.all = 0;
+		const char *exports =
+			tuple_field_with_type_xc(tuple, BOX_FUNC_FIELD_EXPORTS,
+						 MP_ARRAY);
+		uint32_t cnt = mp_decode_array(&exports);
+		for (uint32_t i = 0; i < cnt; i++) {
+			 if (mp_typeof(*exports) != MP_STR) {
+				tnt_raise(ClientError, ER_FIELD_TYPE,
+					  int2str(BOX_FUNC_FIELD_EXPORTS + 1),
+					  mp_type_strs[MP_STR]);
+			}
+			uint32_t len;
+			const char *str = mp_decode_str(&exports, &len);
+			switch (STRN2ENUM(func_language, str, len)) {
+			case FUNC_LANGUAGE_LUA:
+				def->exports.lua = true;
+				break;
+			case FUNC_LANGUAGE_SQL:
+				def->exports.sql = true;
+				break;
+			default:
+				tnt_raise(ClientError, ER_CREATE_FUNCTION,
+					  def->name, "invalid exports value");
+			}
+		}
+		const char *aggregate =
+			tuple_field_cstr_xc(tuple, BOX_FUNC_FIELD_AGGREGATE);
+		def->aggregate = STR2ENUM(func_aggregate, aggregate);
+		if (def->aggregate == func_aggregate_MAX) {
+			tnt_raise(ClientError, ER_CREATE_FUNCTION,
+				  def->name, "invalid aggregate value");
+		}
+		const char *param_list =
+			tuple_field_with_type_xc(tuple,
+					BOX_FUNC_FIELD_PARAM_LIST, MP_ARRAY);
+		uint32_t argc = mp_decode_array(&param_list);
+		for (uint32_t i = 0; i < argc; i++) {
+			 if (mp_typeof(*param_list) != MP_STR) {
+				tnt_raise(ClientError, ER_FIELD_TYPE,
+					  int2str(BOX_FUNC_FIELD_PARAM_LIST + 1),
+					  mp_type_strs[MP_STR]);
+			}
+			uint32_t len;
+			const char *str = mp_decode_str(&param_list, &len);
+			if (STRN2ENUM(field_type, str, len) == field_type_MAX) {
+				tnt_raise(ClientError, ER_CREATE_FUNCTION,
+					  def->name, "invalid argument type");
+			}
+		}
+		def->param_count = argc;
+	} else {
+		def->is_deterministic = false;
+		def->is_sandboxed = false;
+		def->returns = FIELD_TYPE_ANY;
+		def->aggregate = FUNC_AGGREGATE_NONE;
+		def->exports.all = 0;
+		def->exports.lua = true;
+		def->param_count = 0;
+	}
 	def_guard.is_active = false;
 	return def;
 }
diff --git a/src/box/bootstrap. b/src/box/bootstrap.
new file mode 100644
index 0000000000000000000000000000000000000000..c1fd6a6f96746d5de84be79822b11a7b4dd4040d

diff --git a/src/box/func.c b/src/box/func.c
index 8227527ec..8d93a83b2 100644
--- a/src/box/func.c
+++ b/src/box/func.c
@@ -34,7 +34,9 @@
 #include "assoc.h"
 #include "lua/utils.h"
 #include "lua/call.h"
+#include "lua/lua_sql.h"
 #include "error.h"
+#include "sql.h"
 #include "diag.h"
 #include "port.h"
 #include "schema.h"
@@ -385,11 +387,18 @@ struct func *
 func_new(struct func_def *def)
 {
 	struct func *func;
-	if (def->language == FUNC_LANGUAGE_C) {
+	switch (def->language) {
+	case FUNC_LANGUAGE_C:
 		func = func_c_new(def);
-	} else {
-		assert(def->language == FUNC_LANGUAGE_LUA);
+		break;
+	case FUNC_LANGUAGE_LUA:
 		func = func_lua_new(def);
+		break;
+	case FUNC_LANGUAGE_SQL_BUILTIN:
+		func = func_sql_builtin_new(def);
+		break;
+	default:
+		unreachable();
 	}
 	if (func == NULL)
 		return NULL;
@@ -416,8 +425,13 @@ static struct func_vtab func_c_vtab;
 static struct func *
 func_c_new(struct func_def *def)
 {
-	(void) def;
 	assert(def->language == FUNC_LANGUAGE_C);
+	if (def->body != NULL || def->is_sandboxed) {
+		diag_set(ClientError, ER_CREATE_FUNCTION, def->name,
+			 "body and is_sandboxed options are not compatible "
+			 "with C language");
+		return NULL;
+	}
 	struct func_c *func = (struct func_c *) malloc(sizeof(struct func_c));
 	if (func == NULL) {
 		diag_set(OutOfMemory, sizeof(*func), "malloc", "func");
diff --git a/src/box/func_def.c b/src/box/func_def.c
index 2b135e2d7..fb9f77df8 100644
--- a/src/box/func_def.c
+++ b/src/box/func_def.c
@@ -1,7 +1,9 @@
 #include "func_def.h"
 #include "string.h"
 
-const char *func_language_strs[] = {"LUA", "C"};
+const char *func_language_strs[] = {"LUA", "C", "SQL", "SQL_BUILTIN"};
+
+const char *func_aggregate_strs[] = {"none", "group"};
 
 int
 func_def_cmp(struct func_def *def1, struct func_def *def2)
@@ -14,7 +16,27 @@ func_def_cmp(struct func_def *def1, struct func_def *def2)
 		return def1->setuid - def2->setuid;
 	if (def1->language != def2->language)
 		return def1->language - def2->language;
+	if (def1->is_deterministic != def2->is_deterministic)
+		return def1->is_deterministic - def2->is_deterministic;
+	if (def1->is_sandboxed != def2->is_sandboxed)
+		return def1->is_sandboxed - def2->is_sandboxed;
 	if (strcmp(def1->name, def2->name) != 0)
 		return strcmp(def1->name, def2->name);
+	if ((def1->body != NULL) != (def2->body != NULL))
+		return def1->body - def2->body;
+	if (def1->body != NULL && strcmp(def1->body, def2->body) != 0)
+		return strcmp(def1->body, def2->body);
+	if (def1->returns != def2->returns)
+		return def1->returns - def2->returns;
+	if (def1->exports.all != def2->exports.all)
+		return def1->exports.all - def2->exports.all;
+	if (def1->aggregate != def2->aggregate)
+		return def1->aggregate - def2->aggregate;
+	if (def1->param_count != def2->param_count)
+		return def1->param_count - def2->param_count;
+	if ((def1->comment != NULL) != (def2->comment != NULL))
+		return def1->comment - def2->comment;
+	if (def1->comment != NULL && strcmp(def1->comment, def2->comment) != 0)
+		return strcmp(def1->comment, def2->comment);
 	return 0;
 }
diff --git a/src/box/func_def.h b/src/box/func_def.h
index 866d425a1..508580f78 100644
--- a/src/box/func_def.h
+++ b/src/box/func_def.h
@@ -32,6 +32,7 @@
  */
 
 #include "trivia/util.h"
+#include "field_def.h"
 #include <stdbool.h>
 
 #ifdef __cplusplus
@@ -44,11 +45,21 @@ extern "C" {
 enum func_language {
 	FUNC_LANGUAGE_LUA,
 	FUNC_LANGUAGE_C,
+	FUNC_LANGUAGE_SQL,
+	FUNC_LANGUAGE_SQL_BUILTIN,
 	func_language_MAX,
 };
 
 extern const char *func_language_strs[];
 
+enum func_aggregate {
+	FUNC_AGGREGATE_NONE,
+	FUNC_AGGREGATE_GROUP,
+	func_aggregate_MAX,
+};
+
+extern const char *func_aggregate_strs[];
+
 /**
  * Definition of a function. Function body is not stored
  * or replicated (yet).
@@ -58,17 +69,46 @@ struct func_def {
 	uint32_t fid;
 	/** Owner of the function. */
 	uint32_t uid;
+	/** Definition of the persistent function. */
+	char *body;
+	/** User-defined comment for a function. */
+	char *comment;
 	/**
 	 * True if the function requires change of user id before
 	 * invocation.
 	 */
 	bool setuid;
+	/**
+	 * Whether this function is deterministic (can produce
+	 * only one result for a given list of parameters).
+	 */
+	bool is_deterministic;
+	/**
+	 * Whether the routine must be initialized with isolated
+	 * sandbox where only a limited number if functions is
+	 * available.
+	 */
+	bool is_sandboxed;
+	/** The count of function's input arguments. */
+	int param_count;
+	/** The type of the value returned by function. */
+	enum field_type returns;
+	/** Function aggregate option. */
+	enum func_aggregate aggregate;
 	/**
 	 * The language of the stored function.
 	 */
 	enum func_language language;
 	/** The length of the function name. */
 	uint32_t name_len;
+	/** Frontends where function must be available. */
+	union {
+		struct {
+			bool lua : 1;
+			bool sql : 1;
+		};
+		uint8_t all;
+	} exports;
 	/** Function name. */
 	char name[0];
 };
@@ -76,19 +116,32 @@ struct func_def {
 /**
  * @param name_len length of func_def->name
  * @returns size in bytes needed to allocate for struct func_def
- * for a function of length @a a name_len.
+ * for a function of length @a a name_len, body @a body_len and
+ * with comment @a comment_len.
  */
 static inline size_t
-func_def_sizeof(uint32_t name_len)
+func_def_sizeof(uint32_t name_len, uint32_t body_len, uint32_t comment_len,
+		uint32_t *body_offset, uint32_t *comment_offset)
 {
 	/* +1 for '\0' name terminating. */
-	return sizeof(struct func_def) + name_len + 1;
+	size_t sz = sizeof(struct func_def) + name_len + 1;
+	*body_offset = sz;
+	if (body_len > 0)
+		sz += body_len + 1;
+	*comment_offset = sz;
+	if (comment_len > 0)
+		sz += comment_len + 1;
+	return sz;
 }
 
 /** Compare two given function definitions. */
 int
 func_def_cmp(struct func_def *def1, struct func_def *def2);
 
+/** Duplicate a given function defintion object. */
+struct func_def *
+func_def_dup(struct func_def *def);
+
 /**
  * API of C stored function.
  */
diff --git a/src/box/lua/call.c b/src/box/lua/call.c
index 38f2f696b..95fac4834 100644
--- a/src/box/lua/call.c
+++ b/src/box/lua/call.c
@@ -294,6 +294,7 @@ port_lua_create(struct port *port, struct lua_State *L)
 }
 
 struct execute_lua_ctx {
+	int lua_ref;
 	const char *name;
 	uint32_t name_len;
 	struct port *args;
@@ -323,6 +324,24 @@ execute_lua_call(lua_State *L)
 	return lua_gettop(L);
 }
 
+static int
+execute_lua_call_by_ref(lua_State *L)
+{
+	struct execute_lua_ctx *ctx =
+		(struct execute_lua_ctx *) lua_topointer(L, 1);
+	lua_settop(L, 0); /* clear the stack to simplify the logic below */
+
+	lua_rawgeti(L, LUA_REGISTRYINDEX, ctx->lua_ref);
+
+	/* Push the rest of args (a tuple). */
+	int top = lua_gettop(L);
+	port_dump_lua(ctx->args, L, true);
+	int arg_count = lua_gettop(L) - top;
+
+	lua_call(L, arg_count, LUA_MULTRET);
+	return lua_gettop(L);
+}
+
 static int
 execute_lua_eval(lua_State *L)
 {
@@ -534,22 +553,168 @@ box_lua_eval(const char *expr, uint32_t expr_len,
 struct func_lua {
 	/** Function object base class. */
 	struct func base;
+	/**
+	 * For a persistent function: a reference to the
+	 * function body. Otherwise LUA_REFNIL.
+	 */
+	int lua_ref;
 };
 
 static struct func_vtab func_lua_vtab;
+static struct func_vtab func_persistent_lua_vtab;
+
+static const char *default_sandbox_exports[] = {
+	"assert", "error", "ipairs", "math", "next", "pairs", "pcall", "print",
+	"select", "string", "table", "tonumber", "tostring", "type", "unpack",
+	"xpcall", "utf8",
+};
+
+/**
+ * Assemble a new sandbox with given exports table on the top of
+ * a given Lua stack. All modules in exports list are copied
+ * deeply to ensure the immutability of this system object.
+ */
+static int
+prepare_lua_sandbox(struct lua_State *L, const char *exports[],
+		    int export_count)
+{
+	lua_createtable(L, export_count, 0);
+	if (export_count == 0)
+		return 0;
+	int rc = -1;
+	const char *deepcopy = "table.deepcopy";
+	int luaL_deepcopy_func_ref = LUA_REFNIL;
+	int ret = box_lua_find(L, deepcopy, deepcopy + strlen(deepcopy));
+	if (ret < 0)
+		goto end;
+	luaL_deepcopy_func_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+	assert(luaL_deepcopy_func_ref != LUA_REFNIL);
+	for (int i = 0; i < export_count; i++) {
+		uint32_t name_len = strlen(exports[i]);
+		ret = box_lua_find(L, exports[i], exports[i] + name_len);
+		if (ret < 0)
+			goto end;
+		switch (lua_type(L, -1)) {
+		case LUA_TTABLE:
+			lua_rawgeti(L, LUA_REGISTRYINDEX,
+				    luaL_deepcopy_func_ref);
+			lua_insert(L, -2);
+			lua_call(L, 1, 1);
+			break;
+		case LUA_TFUNCTION:
+			break;
+		default:
+			unreachable();
+		}
+		lua_setfield(L, -2, exports[i]);
+	}
+	rc = 0;
+end:
+	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, luaL_deepcopy_func_ref);
+	return rc;
+}
+
+/**
+ * Assemble a Lua function object by user-defined function body.
+ */
+static int
+func_persistent_lua_load(struct func_lua *func)
+{
+	int rc = -1;
+	int top = lua_gettop(tarantool_L);
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	const char *load_pref = "return ";
+	uint32_t load_str_sz =
+		strlen(load_pref) + strlen(func->base.def->body) + 1;
+	char *load_str = region_alloc(region, load_str_sz);
+	if (load_str == NULL) {
+		diag_set(OutOfMemory, load_str_sz, "region", "load_str");
+		return -1;
+	}
+	sprintf(load_str, "%s%s", load_pref, func->base.def->body);
+
+	/*
+	 * Perform loading of the persistent Lua function
+	 * in a new sandboxed Lua thread. The sandbox is
+	 * required to guarantee the safety of executing
+	 * an arbitrary user-defined code
+	 * (e.g. body = 'fiber.yield()').
+	 */
+	struct lua_State *coro_L = lua_newthread(tarantool_L);
+	if (!func->base.def->is_sandboxed) {
+		/*
+		 * Keep an original env to apply for non-sandboxed
+		 * persistent function. It is required because
+		 * built object inherits parent env.
+		 */
+		lua_getfenv(tarantool_L, -1);
+		lua_insert(tarantool_L, -2);
+	}
+	if (prepare_lua_sandbox(tarantool_L, NULL, 0) != 0)
+		unreachable();
+	lua_setfenv(tarantool_L, -2);
+	int coro_ref = luaL_ref(tarantool_L, LUA_REGISTRYINDEX);
+	if (luaL_loadstring(coro_L, load_str) != 0 ||
+	    lua_pcall(coro_L, 0, 1, 0) != 0) {
+		diag_set(ClientError, ER_LOAD_FUNCTION, func->base.def->name,
+			 luaT_tolstring(coro_L, -1, NULL));
+		goto end;
+	}
+	if (!lua_isfunction(coro_L, -1)) {
+		diag_set(ClientError, ER_LOAD_FUNCTION, func->base.def->name,
+			 "given body doesn't define a function");
+		goto end;
+	}
+	lua_xmove(coro_L, tarantool_L, 1);
+	if (func->base.def->is_sandboxed) {
+		if (prepare_lua_sandbox(tarantool_L, default_sandbox_exports,
+					nelem(default_sandbox_exports)) != 0) {
+			diag_set(ClientError, ER_LOAD_FUNCTION,
+				func->base.def->name,
+				diag_last_error(diag_get())->errmsg);
+			goto end;
+		}
+	} else {
+		lua_insert(tarantool_L, -2);
+	}
+	lua_setfenv(tarantool_L, -2);
+	func->lua_ref = luaL_ref(tarantool_L, LUA_REGISTRYINDEX);
+	rc = 0;
+end:
+	lua_settop(tarantool_L, top);
+	region_truncate(region, region_svp);
+	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, coro_ref);
+	return rc;
+}
 
 struct func *
 func_lua_new(struct func_def *def)
 {
-	(void) def;
 	assert(def->language == FUNC_LANGUAGE_LUA);
+	if (def->is_sandboxed && def->body == NULL) {
+		diag_set(ClientError, ER_CREATE_FUNCTION, def->name,
+			 "is_sandboxed option may be set only for persistent "
+			 "Lua function (when body option is set)");
+		return NULL;
+	}
 	struct func_lua *func =
 		(struct func_lua *) malloc(sizeof(struct func_lua));
 	if (func == NULL) {
 		diag_set(OutOfMemory, sizeof(*func), "malloc", "func");
 		return NULL;
 	}
-	func->base.vtab = &func_lua_vtab;
+	if (def->body != NULL) {
+		func->base.def = def;
+		func->base.vtab = &func_persistent_lua_vtab;
+		if (func_persistent_lua_load(func) != 0) {
+			free(func);
+			return NULL;
+		}
+	} else {
+		func->lua_ref = LUA_REFNIL;
+		func->base.vtab = &func_lua_vtab;
+	}
 	return &func->base;
 }
 
@@ -574,6 +739,42 @@ static struct func_vtab func_lua_vtab = {
 	.destroy = func_lua_destroy,
 };
 
+static void
+func_persistent_lua_unload(struct func_lua *func)
+{
+	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, func->lua_ref);
+}
+
+static void
+func_persistent_lua_destroy(struct func *base)
+{
+	assert(base != NULL && base->def->language == FUNC_LANGUAGE_LUA &&
+	       base->def->body != NULL);
+	assert(base->vtab == &func_persistent_lua_vtab);
+	struct func_lua *func = (struct func_lua *) base;
+	func_persistent_lua_unload(func);
+	free(func);
+}
+
+static inline int
+func_persistent_lua_call(struct func *base, struct port *args, struct port *ret)
+{
+	assert(base != NULL && base->def->language == FUNC_LANGUAGE_LUA &&
+	       base->def->body != NULL);
+	assert(base->vtab == &func_persistent_lua_vtab);
+	struct func_lua *func = (struct func_lua *)base;
+	struct execute_lua_ctx ctx;
+	ctx.lua_ref = func->lua_ref;
+	ctx.args = args;
+	return box_process_lua(execute_lua_call_by_ref, &ctx, ret);
+
+}
+
+static struct func_vtab func_persistent_lua_vtab = {
+	.call = func_persistent_lua_call,
+	.destroy = func_persistent_lua_destroy,
+};
+
 static int
 lbox_module_reload(lua_State *L)
 {
@@ -667,6 +868,40 @@ lbox_func_new(struct lua_State *L, struct func *func)
 	lua_pushstring(L, "language");
 	lua_pushstring(L, func_language_strs[func->def->language]);
 	lua_settable(L, top);
+	lua_pushstring(L, "returns");
+	lua_pushstring(L, field_type_strs[func->def->returns]);
+	lua_settable(L, top);
+	lua_pushstring(L, "aggregate");
+	lua_pushstring(L, func_aggregate_strs[func->def->aggregate]);
+	lua_settable(L, top);
+	lua_pushstring(L, "body");
+	if (func->def->body != NULL)
+		lua_pushstring(L, func->def->body);
+	else
+		lua_pushnil(L);
+	lua_settable(L, top);
+	lua_pushstring(L, "comment");
+	if (func->def->comment != NULL)
+		lua_pushstring(L, func->def->comment);
+	else
+		lua_pushnil(L);
+	lua_settable(L, top);
+	lua_pushstring(L, "exports");
+	lua_newtable(L);
+	lua_pushboolean(L, func->def->exports.lua);
+	lua_setfield(L, -2, "lua");
+	lua_pushboolean(L, func->def->exports.sql);
+	lua_setfield(L, -2, "sql");
+	lua_settable(L, -3);
+	lua_pushstring(L, "is_deterministic");
+	lua_pushboolean(L, func->def->is_deterministic);
+	lua_settable(L, top);
+	lua_pushstring(L, "is_sandboxed");
+	if (func->def->body != NULL)
+		lua_pushboolean(L, func->def->is_sandboxed);
+	else
+		lua_pushnil(L);
+	lua_settable(L, top);
 
 	/* Bless func object. */
 	lua_getfield(L, LUA_GLOBALSINDEX, "box");
@@ -712,6 +947,8 @@ lbox_func_new_or_delete(struct trigger *trigger, void *event)
 {
 	struct lua_State *L = (struct lua_State *) trigger->data;
 	struct func *func = (struct func *)event;
+	if (!func->def->exports.lua)
+		return;
 	if (func_by_id(func->def->fid) != NULL)
 		lbox_func_new(L, func);
 	else
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 084addc2c..aadcd3fa9 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -2107,7 +2107,9 @@ box.schema.func.create = function(name, opts)
     opts = opts or {}
     check_param_table(opts, { setuid = 'boolean',
                               if_not_exists = 'boolean',
-                              language = 'string'})
+                              language = 'string', body = 'string',
+                              is_deterministic = 'boolean',
+                              is_sandboxed = 'boolean', comment = 'string' })
     local _func = box.space[box.schema.FUNC_ID]
     local _vfunc = box.space[box.schema.VFUNC_ID]
     local func = _vfunc.index.name:get{name}
@@ -2117,10 +2119,21 @@ box.schema.func.create = function(name, opts)
         end
         return
     end
-    opts = update_param_table(opts, { setuid = false, language = 'lua'})
+    local datetime = os.date("%Y-%m-%d %H:%M:%S")
+    opts = update_param_table(opts, { setuid = false, language = 'lua',
+                    body = '', routine_type = 'function', returns = 'any',
+                    param_list = {}, aggregate = 'none', sql_data_access = 'none',
+                    is_deterministic = false, is_sandboxed = false,
+                    is_null_call = true, exports = {'LUA'}, opts = setmap{},
+                    comment = '', created = datetime, last_altered = datetime})
     opts.language = string.upper(opts.language)
     opts.setuid = opts.setuid and 1 or 0
-    _func:auto_increment{session.euid(), name, opts.setuid, opts.language}
+    _func:auto_increment{session.euid(), name, opts.setuid, opts.language,
+                         opts.body, opts.routine_type, opts.param_list,
+                         opts.returns, opts.aggregate, opts.sql_data_access,
+                         opts.is_deterministic, opts.is_sandboxed,
+                         opts.is_null_call, opts.exports, opts.opts,
+                         opts.comment, opts.created, opts.last_altered}
 end
 
 box.schema.func.drop = function(name, opts)
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index 3385b8e17..ee671275b 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -152,6 +152,7 @@ local function initial_1_7_5()
     local _cluster = box.space[box.schema.CLUSTER_ID]
     local _truncate = box.space[box.schema.TRUNCATE_ID]
     local MAP = setmap({})
+    local datetime = os.date("%Y-%m-%d %H:%M:%S")
 
     --
     -- _schema
@@ -326,7 +327,9 @@ local function initial_1_7_5()
 
     -- create "box.schema.user.info" function
     log.info('create function "box.schema.user.info" with setuid')
-    _func:replace{1, ADMIN, 'box.schema.user.info', 1, 'LUA'}
+    _func:replace({1, ADMIN, 'box.schema.user.info', 1, 'LUA', '', 'function',
+                  {}, 'any', 'none', 'none', false, false, true, {'LUA'},
+                  MAP, '', datetime, datetime})
 
     -- grant 'public' role access to 'box.schema.user.info' function
     log.info('grant execute on function "box.schema.user.info" to public')
@@ -820,10 +823,68 @@ local function create_vcollation_space()
     box.space[box.schema.VCOLLATION_ID]:format(format)
 end
 
+local function upgrade_func_to_2_2_1()
+    log.info("Update _func format")
+    local _func = box.space[box.schema.FUNC_ID]
+    local datetime = os.date("%Y-%m-%d %H:%M:%S")
+    for _, v in box.space._func:pairs() do
+        box.space._func:replace({v.id, v.owner, v.name, v.setuid, v[5] or 'LUA',
+                                 '', 'function', {}, 'any', 'none', 'none',
+                                 false, false, true, v[15] or {'LUA'},
+                                 setmap({}), '', datetime, datetime})
+    end
+    local sql_builtin_list = {
+        "TRIM", "TYPEOF", "PRINTF", "UNICODE", "CHAR", "HEX", "VERSION",
+        "QUOTE", "REPLACE", "SUBSTR", "GROUP_CONCAT", "JULIANDAY", "DATE",
+        "TIME", "DATETIME", "STRFTIME", "CURRENT_TIME", "CURRENT_TIMESTAMP",
+        "CURRENT_DATE", "LENGTH", "POSITION", "ROUND", "UPPER", "LOWER",
+        "IFNULL", "RANDOM", "CEIL", "CEILING", "CHARACTER_LENGTH",
+        "CHAR_LENGTH", "FLOOR", "MOD", "OCTET_LENGTH", "ROW_COUNT", "COUNT",
+        "LIKE", "ABS", "EXP", "LN", "POWER", "SQRT", "SUM", "TOTAL", "AVG",
+        "RANDOMBLOB", "NULLIF", "ZEROBLOB", "MIN", "MAX", "COALESCE", "EVERY",
+        "EXISTS", "EXTRACT", "SOME", "_sql_record", "_sql_stat_get",
+        "_sql_stat_push", "_sql_stat_init",
+    }
+    for _, v in pairs(sql_builtin_list) do
+        _func:auto_increment({ADMIN, v, 1, 'SQL_BUILTIN', '', 'function', {},
+                              'any', 'none', 'none', false, false, true, {},
+                              setmap({}), '', datetime, datetime})
+    end
+    _func:auto_increment({ADMIN, 'LUA', 1, 'LUA',
+                          'function(code) return assert(loadstring(code))() end',
+                          'function', {'string'}, 'any', 'none', 'none',
+                          false, false, true, {'LUA', 'SQL'},
+                          setmap({}), '', datetime, datetime})
+    local format = {}
+    format[1] = {name='id', type='unsigned'}
+    format[2] = {name='owner', type='unsigned'}
+    format[3] = {name='name', type='string'}
+    format[4] = {name='setuid', type='unsigned'}
+    format[5] = {name='language', type='string'}
+    format[6] = {name='body', type='string'}
+    format[7] = {name='routine_type', type='string'}
+    format[8] = {name='param_list', type='array'}
+    format[9] = {name='returns', type='string'}
+    format[10] = {name='aggregate', type='string'}
+    format[11] = {name='sql_data_access', type='string'}
+    format[12] = {name='is_deterministic', type='boolean'}
+    format[13] = {name='is_sandboxed', type='boolean'}
+    format[14] = {name='is_null_call', type='boolean'}
+    format[15] = {name='exports', type='array'}
+    format[16] = {name='opts', type='map'}
+    format[17] = {name='comment', type='string'}
+    format[18] = {name='created', type='string'}
+    format[19] = {name='last_altered', type='string'}
+    _func:format(format)
+    _func.index.name:alter({parts = {{'name', 'string',
+                                      collation = 'unicode_ci'}}})
+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()
 end
 
 --------------------------------------------------------------------------------
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 88b5502b8..a97b6d531 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -167,6 +167,20 @@ enum {
 	BOX_FUNC_FIELD_NAME = 2,
 	BOX_FUNC_FIELD_SETUID = 3,
 	BOX_FUNC_FIELD_LANGUAGE = 4,
+	BOX_FUNC_FIELD_BODY = 5,
+	BOX_FUNC_FIELD_ROUTINE_TYPE = 6,
+	BOX_FUNC_FIELD_PARAM_LIST = 7,
+	BOX_FUNC_FIELD_RETURNS = 8,
+	BOX_FUNC_FIELD_AGGREGATE = 9,
+	BOX_FUNC_FIELD_SQL_DATA_ACCESS = 10,
+	BOX_FUNC_FIELD_IS_DETERMINISTIC = 11,
+	BOX_FUNC_FIELD_IS_SANDBOXED = 12,
+	BOX_FUNC_FIELD_IS_NULL_CALL = 13,
+	BOX_FUNC_FIELD_EXPORTS = 14,
+	BOX_FUNC_FIELD_OPTS = 15,
+	BOX_FUNC_FIELD_COMMENT = 16,
+	BOX_FUNC_FIELD_CREATED = 17,
+	BOX_FUNC_FIELD_LAST_ALTERED = 18,
 };
 
 /** _collation fields. */
diff --git a/src/box/sql.h b/src/box/sql.h
index 9ccecf28c..a078bfdec 100644
--- a/src/box/sql.h
+++ b/src/box/sql.h
@@ -70,6 +70,7 @@ struct Select;
 struct Table;
 struct sql_trigger;
 struct space_def;
+struct func_def;
 
 /**
  * Perform parsing of provided expression. This is done by
@@ -404,6 +405,10 @@ void
 vdbe_field_ref_prepare_tuple(struct vdbe_field_ref *field_ref,
 			     struct tuple *tuple);
 
+/** Construct a SQL builtin function object. */
+struct func *
+func_sql_builtin_new(struct func_def *def);
+
 #if defined(__cplusplus)
 } /* extern "C" { */
 #endif
diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index 29f2b5c6a..be2806e0a 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -38,6 +38,7 @@
 #include "vdbeInt.h"
 #include "version.h"
 #include "coll/coll.h"
+#include "box/func.h"
 #include "tarantoolInt.h"
 #include <unicode/ustring.h>
 #include <unicode/ucasemap.h>
@@ -1864,3 +1865,45 @@ sqlRegisterBuiltinFunctions(void)
 	}
 #endif
 }
+
+struct func_sql_builtin {
+	/** Function object base class. */
+	struct func base;
+};
+
+static struct func_vtab func_sql_builtin_vtab;
+
+struct func *
+func_sql_builtin_new(struct func_def *def)
+{
+	assert(def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	if (def->body != NULL || def->is_sandboxed) {
+		diag_set(ClientError, ER_CREATE_FUNCTION, def->name,
+			 "body and is_sandboxed options are not compatible "
+			 "with SQL language");
+		return NULL;
+	}
+	struct func_sql_builtin *func =
+		(struct func_sql_builtin *) malloc(sizeof(*func));
+	if (func == NULL) {
+		diag_set(OutOfMemory, sizeof(*func), "malloc", "func");
+		return NULL;
+	}
+	/** Don't export SQL builtins in Lua for now. */
+	def->exports.lua = false;
+	func->base.vtab = &func_sql_builtin_vtab;
+	return &func->base;
+}
+
+static void
+func_sql_builtin_destroy(struct func *base)
+{
+	assert(base->vtab == &func_sql_builtin_vtab);
+	assert(base != NULL && base->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	free(base);
+}
+
+static struct func_vtab func_sql_builtin_vtab = {
+	.call = NULL,
+	.destroy = func_sql_builtin_destroy,
+};
diff --git a/test-run b/test-run
index 37d15bd78..4d5244016 160000
--- a/test-run
+++ b/test-run
@@ -1 +1 @@
-Subproject commit 37d15bd781ddfb41dfd75d9b761c180395b4b53f
+Subproject commit 4d52440163e93ad1f90f2454e2bc9ada435969d5
diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result
index b20dc41e5..4e17fbfea 100644
--- a/test/box-py/bootstrap.result
+++ b/test/box-py/bootstrap.result
@@ -53,7 +53,12 @@ box.space._space:select{}
         'type': 'string'}, {'name': 'opts', 'type': 'map'}, {'name': 'parts', 'type': 'array'}]]
   - [296, 1, '_func', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'owner',
         'type': 'unsigned'}, {'name': 'name', 'type': 'string'}, {'name': 'setuid',
-        'type': 'unsigned'}]]
+        'type': 'unsigned'}, {'name': 'language', 'type': 'string'}, {'name': 'body',
+        'type': 'string'}, {'name': 'routine_type', 'type': 'string'}, {'name': 'data_type',
+        'type': 'map'}, {'name': 'sql_data_access', 'type': 'string'}, {'name': 'is_deterministic',
+        'type': 'boolean'}, {'name': 'is_sandboxed', 'type': 'boolean'}, {'name': 'is_null_call',
+        'type': 'boolean'}, {'name': 'opts', 'type': 'map'}, {'name': 'comment', 'type': 'string'},
+      {'name': 'created', 'type': 'string'}, {'name': 'last_altered', 'type': 'string'}]]
   - [297, 1, '_vfunc', 'sysview', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'owner',
         'type': 'unsigned'}, {'name': 'name', 'type': 'string'}, {'name': 'setuid',
         'type': 'unsigned'}]]
@@ -150,9 +155,10 @@ box.space._user:select{}
   - [3, 1, 'replication', 'role', {}]
   - [31, 1, 'super', 'role', {}]
 ...
-box.space._func:select{}
+for _, v in box.space._func:pairs{} do r = {} table.insert(r, v:update({{"=", 15, ""}, {"=", 16, ""}})) return r end
 ---
-- - [1, 1, 'box.schema.user.info', 1, 'LUA']
+- - [1, 1, 'box.schema.user.info', 1, 'LUA', '', 'function', {}, 'none', false, false,
+    true, {}, '', '', '']
 ...
 box.space._priv:select{}
 ---
diff --git a/test/box-py/bootstrap.test.py b/test/box-py/bootstrap.test.py
index 4f2f55a7c..ba0689ae9 100644
--- a/test/box-py/bootstrap.test.py
+++ b/test/box-py/bootstrap.test.py
@@ -4,7 +4,7 @@ server.admin('box.space._cluster:select{}')
 server.admin('box.space._space:select{}')
 server.admin('box.space._index:select{}')
 server.admin('box.space._user:select{}')
-server.admin('box.space._func:select{}')
+server.admin('for _, v in box.space._func:pairs{} do r = {} table.insert(r, v:update({{"=", 15, ""}, {"=", 16, ""}})) return r end')
 server.admin('box.space._priv:select{}')
 
 # Cleanup
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 53d366106..c3ed3a65d 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -793,7 +793,12 @@ box.space._space:select()
         'type': 'string'}, {'name': 'opts', 'type': 'map'}, {'name': 'parts', 'type': 'array'}]]
   - [296, 1, '_func', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'owner',
         'type': 'unsigned'}, {'name': 'name', 'type': 'string'}, {'name': 'setuid',
-        'type': 'unsigned'}]]
+        'type': 'unsigned'}, {'name': 'language', 'type': 'string'}, {'name': 'body',
+        'type': 'string'}, {'name': 'routine_type', 'type': 'string'}, {'name': 'data_type',
+        'type': 'map'}, {'name': 'sql_data_access', 'type': 'string'}, {'name': 'is_deterministic',
+        'type': 'boolean'}, {'name': 'is_sandboxed', 'type': 'boolean'}, {'name': 'is_null_call',
+        'type': 'boolean'}, {'name': 'opts', 'type': 'map'}, {'name': 'comment', 'type': 'string'},
+      {'name': 'created', 'type': 'string'}, {'name': 'last_altered', 'type': 'string'}]]
   - [297, 1, '_vfunc', 'sysview', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'owner',
         'type': 'unsigned'}, {'name': 'name', 'type': 'string'}, {'name': 'setuid',
         'type': 'unsigned'}]]
@@ -829,7 +834,8 @@ box.space._space:select()
 ...
 box.space._func:select()
 ---
-- - [1, 1, 'box.schema.user.info', 1, 'LUA']
+- - [1, 1, 'box.schema.user.info', 1, 'LUA', '', 'function', {}, 'none', false, false,
+    true, {}, '', '2019-06-23 18:11:31', '2019-06-23 18:11:31']
 ...
 session = nil
 ---
diff --git a/test/box/function1.result b/test/box/function1.result
index ec1ab5e6b..a8d017653 100644
--- a/test/box/function1.result
+++ b/test/box/function1.result
@@ -16,7 +16,40 @@ c = net.connect(os.getenv("LISTEN"))
 box.schema.func.create('function1', {language = "C"})
 ---
 ...
-box.space._func:replace{2, 1, 'function1', 0, 'LUA'}
+id = box.func["function1"].id
+---
+...
+function setmap(tab) return setmetatable(tab, { __serialize = 'map' }) end
+---
+...
+datetime = os.date("%Y-%m-%d %H:%M:%S")
+---
+...
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'procedure', {}, 'any', 'none', 'none', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
+---
+- error: 'Failed to create function ''function1'': unsupported routine_type value'
+...
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'none', 'reads', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
+---
+- error: 'Failed to create function ''function1'': unsupported sql_data_access value'
+...
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'none', 'none', false, false, false, {"LUA"}, setmap({}), '', datetime, datetime}
+---
+- error: 'Failed to create function ''function1'': unsupported is_null_call value'
+...
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'data', 'none', 'none', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
+---
+- error: 'Failed to create function ''function1'': invalid returns value'
+...
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'none', 'none', false, false, true, {"LUA", "C"}, setmap({}), '', datetime, datetime}
+---
+- error: 'Failed to create function ''function1'': invalid exports value'
+...
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'aggregate', 'none', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
+---
+- error: 'Failed to create function ''function1'': invalid aggregate value'
+...
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'none', 'none', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
 ---
 - error: function does not support alter
 ...
@@ -59,10 +92,16 @@ c:call('function1.args', { 15 })
 ...
 box.func["function1.args"]
 ---
-- language: C
+- aggregate: none
+  returns: any
+  exports:
+    lua: true
+    sql: false
+  id: 61
   setuid: false
+  is_deterministic: false
   name: function1.args
-  id: 2
+  language: C
 ...
 box.func["function1.args"]:call()
 ---
@@ -330,7 +369,7 @@ c:close()
 function divide(a, b) return a / b end
 ---
 ...
-box.schema.func.create("divide")
+box.schema.func.create("divide", {comment = 'Divide two values'})
 ---
 ...
 func = box.func.divide
@@ -372,10 +411,17 @@ func:drop()
 ...
 func
 ---
-- language: LUA
+- aggregate: none
+  returns: any
+  exports:
+    lua: true
+    sql: false
+  id: 61
   setuid: false
+  is_deterministic: false
+  comment: Divide two values
   name: divide
-  id: 2
+  language: LUA
 ...
 func.drop()
 ---
@@ -436,10 +482,16 @@ box.func["function1.divide"]
 ...
 func
 ---
-- language: C
+- aggregate: none
+  returns: any
+  exports:
+    lua: true
+    sql: false
+  id: 61
   setuid: false
+  is_deterministic: false
   name: function1.divide
-  id: 2
+  language: C
 ...
 func:drop()
 ---
@@ -526,6 +578,177 @@ box.schema.func.drop('secret_leak')
 box.schema.func.drop('secret')
 ---
 ...
+--
+-- gh-4182: Introduce persistent Lua functions.
+--
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+body = [[function(tuple)
+		if type(tuple.address) ~= 'string' then
+			return nil, 'Invalid field type'
+		end
+		local t = tuple.address:upper():split()
+		for k,v in pairs(t) do t[k] = v end
+		return t
+	end
+]]
+test_run:cmd("setopt delimiter ''");
+---
+...
+box.schema.func.create('addrsplit', {body = body, language = "C"})
+---
+- error: 'Failed to create function ''addrsplit'': body and is_sandboxed options are
+    not compatible with C language'
+...
+box.schema.func.create('addrsplit', {is_sandboxed = true, language = "C"})
+---
+- error: 'Failed to create function ''addrsplit'': body and is_sandboxed options are
+    not compatible with C language'
+...
+box.schema.func.create('addrsplit', {is_sandboxed = true})
+---
+- error: 'Failed to create function ''addrsplit'': is_sandboxed option may be set
+    only for persistent Lua function (when body option is set)'
+...
+box.schema.func.create('invalid', {body = "function(tuple) ret tuple"})
+---
+- error: 'Failed to dynamically load function ''invalid'': [string "return function(tuple)
+    ret tuple"]:1: ''='' expected near ''tuple'''
+...
+box.schema.func.create('addrsplit', {body = body, is_deterministic = true})
+---
+...
+box.schema.user.grant('guest', 'execute', 'function', 'addrsplit')
+---
+...
+conn = net.connect(box.cfg.listen)
+---
+...
+conn:call('addrsplit', {{address = "Moscow Dolgoprudny"}})
+---
+- ['MOSCOW', 'DOLGOPRUDNY']
+...
+box.func.addrsplit:call({{address = "Moscow Dolgoprudny"}})
+---
+- - MOSCOW
+  - DOLGOPRUDNY
+...
+conn:close()
+---
+...
+box.snapshot()
+---
+- ok
+...
+test_run:cmd("restart server default")
+test_run = require('test_run').new()
+---
+...
+test_run:cmd("push filter '(.builtin/.*.lua):[0-9]+' to '\\1'")
+---
+- true
+...
+net = require('net.box')
+---
+...
+conn = net.connect(box.cfg.listen)
+---
+...
+conn:call('addrsplit', {{address = "Moscow Dolgoprudny"}})
+---
+- ['MOSCOW', 'DOLGOPRUDNY']
+...
+box.func.addrsplit:call({{address = "Moscow Dolgoprudny"}})
+---
+- - MOSCOW
+  - DOLGOPRUDNY
+...
+conn:close()
+---
+...
+box.schema.user.revoke('guest', 'execute', 'function', 'addrsplit')
+---
+...
+box.func.addrsplit:drop()
+---
+...
+-- Test sandboxed functions.
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+body = [[function(number)
+		math.abs = math.log
+		return math.abs(number)
+	end]]
+test_run:cmd("setopt delimiter ''");
+---
+...
+box.schema.func.create('monkey', {body = body, is_sandboxed = true})
+---
+...
+box.func.monkey:call({1})
+---
+- 0
+...
+math.abs(1)
+---
+- 1
+...
+box.func.monkey:drop()
+---
+...
+sum = 0
+---
+...
+function inc_g(val) sum = sum + val end
+---
+...
+box.schema.func.create('call_inc_g', {body = "function(val) inc_g(val) end"})
+---
+...
+box.func.call_inc_g:call({1})
+---
+...
+assert(sum == 1)
+---
+- true
+...
+box.schema.func.create('call_inc_g_safe', {body = "function(val) inc_g(val) end", is_sandboxed = true})
+---
+...
+box.func.call_inc_g_safe:call({1})
+---
+- error: '[string "return function(val) inc_g(val) end"]:1: attempt to call global
+    ''inc_g'' (a nil value)'
+...
+assert(sum == 1)
+---
+- true
+...
+box.func.call_inc_g:drop()
+---
+...
+box.func.call_inc_g_safe:drop()
+---
+...
+-- Test persistent function assemble corner cases
+box.schema.func.create('compiletime_tablef', {body = "{}"})
+---
+- error: 'Failed to dynamically load function ''compiletime_tablef'': given body doesn''t
+    define a function'
+...
+box.schema.func.create('compiletime_call_inc_g', {body = "inc_g()"})
+---
+- error: 'Failed to dynamically load function ''compiletime_call_inc_g'': [string
+    "return inc_g()"]:1: attempt to call global ''inc_g'' (a nil value)'
+...
+assert(sum == 1)
+---
+- true
+...
 test_run:cmd("clear filter")
 ---
 - true
@@ -564,3 +787,37 @@ box.func.test ~= nil
 box.func.test:drop()
 ---
 ...
+-- Check SQL builtins
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+sql_builtin_list = {
+	"TRIM", "TYPEOF", "PRINTF", "UNICODE", "CHAR", "HEX", "VERSION",
+	"QUOTE", "REPLACE", "SUBSTR", "GROUP_CONCAT", "JULIANDAY", "DATE",
+	"TIME", "DATETIME", "STRFTIME", "CURRENT_TIME", "CURRENT_TIMESTAMP",
+	"CURRENT_DATE", "LENGTH", "POSITION", "ROUND", "UPPER", "LOWER",
+	"IFNULL", "RANDOM", "CEIL", "CEILING", "CHARACTER_LENGTH",
+	"CHAR_LENGTH", "FLOOR", "MOD", "OCTET_LENGTH", "ROW_COUNT", "COUNT",
+	"LIKE", "ABS", "EXP", "LN", "POWER", "SQRT", "SUM", "TOTAL", "AVG",
+	"RANDOMBLOB", "NULLIF", "ZEROBLOB", "MIN", "MAX", "COALESCE", "EVERY",
+	"EXISTS", "EXTRACT", "SOME", "_sql_record", "_sql_stat_get",
+	"_sql_stat_push", "_sql_stat_init",
+}
+test_run:cmd("setopt delimiter ''");
+---
+...
+ok = true
+---
+...
+for _, v in pairs(sql_builtin_list) do ok = ok and (box.space._func.index.name:get(v) ~= nil) end
+---
+...
+ok == true
+---
+- true
+...
+box.func.LUA:call({"return 1 + 1"})
+---
+- 2
+...
diff --git a/test/box/function1.test.lua b/test/box/function1.test.lua
index a891e1921..a6d750f2b 100644
--- a/test/box/function1.test.lua
+++ b/test/box/function1.test.lua
@@ -7,7 +7,16 @@ net = require('net.box')
 c = net.connect(os.getenv("LISTEN"))
 
 box.schema.func.create('function1', {language = "C"})
-box.space._func:replace{2, 1, 'function1', 0, 'LUA'}
+id = box.func["function1"].id
+function setmap(tab) return setmetatable(tab, { __serialize = 'map' }) end
+datetime = os.date("%Y-%m-%d %H:%M:%S")
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'procedure', {}, 'any', 'none', 'none', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'none', 'reads', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'none', 'none', false, false, false, {"LUA"}, setmap({}), '', datetime, datetime}
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'data', 'none', 'none', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'none', 'none', false, false, true, {"LUA", "C"}, setmap({}), '', datetime, datetime}
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'aggregate', 'none', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
+box.space._func:replace{id, 1, 'function1', 0, 'LUA', '', 'function', {}, 'any', 'none', 'none', false, false, true, {"LUA"}, setmap({}), '', datetime, datetime}
 box.schema.user.grant('guest', 'execute', 'function', 'function1')
 _ = box.schema.space.create('test')
 _ = box.space.test:create_index('primary')
@@ -121,7 +130,7 @@ c:close()
 
 -- Test registered functions interface.
 function divide(a, b) return a / b end
-box.schema.func.create("divide")
+box.schema.func.create("divide", {comment = 'Divide two values'})
 func = box.func.divide
 func.call({4, 2})
 func:call(4, 2)
@@ -184,6 +193,70 @@ box.schema.user.revoke('guest', 'execute', 'function', 'secret_leak')
 box.schema.func.drop('secret_leak')
 box.schema.func.drop('secret')
 
+--
+-- gh-4182: Introduce persistent Lua functions.
+--
+test_run:cmd("setopt delimiter ';'")
+body = [[function(tuple)
+		if type(tuple.address) ~= 'string' then
+			return nil, 'Invalid field type'
+		end
+		local t = tuple.address:upper():split()
+		for k,v in pairs(t) do t[k] = v end
+		return t
+	end
+]]
+test_run:cmd("setopt delimiter ''");
+box.schema.func.create('addrsplit', {body = body, language = "C"})
+box.schema.func.create('addrsplit', {is_sandboxed = true, language = "C"})
+box.schema.func.create('addrsplit', {is_sandboxed = true})
+box.schema.func.create('invalid', {body = "function(tuple) ret tuple"})
+box.schema.func.create('addrsplit', {body = body, is_deterministic = true})
+box.schema.user.grant('guest', 'execute', 'function', 'addrsplit')
+conn = net.connect(box.cfg.listen)
+conn:call('addrsplit', {{address = "Moscow Dolgoprudny"}})
+box.func.addrsplit:call({{address = "Moscow Dolgoprudny"}})
+conn:close()
+box.snapshot()
+test_run:cmd("restart server default")
+test_run = require('test_run').new()
+test_run:cmd("push filter '(.builtin/.*.lua):[0-9]+' to '\\1'")
+net = require('net.box')
+conn = net.connect(box.cfg.listen)
+conn:call('addrsplit', {{address = "Moscow Dolgoprudny"}})
+box.func.addrsplit:call({{address = "Moscow Dolgoprudny"}})
+conn:close()
+box.schema.user.revoke('guest', 'execute', 'function', 'addrsplit')
+box.func.addrsplit:drop()
+
+-- Test sandboxed functions.
+test_run:cmd("setopt delimiter ';'")
+body = [[function(number)
+		math.abs = math.log
+		return math.abs(number)
+	end]]
+test_run:cmd("setopt delimiter ''");
+box.schema.func.create('monkey', {body = body, is_sandboxed = true})
+box.func.monkey:call({1})
+math.abs(1)
+box.func.monkey:drop()
+
+sum = 0
+function inc_g(val) sum = sum + val end
+box.schema.func.create('call_inc_g', {body = "function(val) inc_g(val) end"})
+box.func.call_inc_g:call({1})
+assert(sum == 1)
+box.schema.func.create('call_inc_g_safe', {body = "function(val) inc_g(val) end", is_sandboxed = true})
+box.func.call_inc_g_safe:call({1})
+assert(sum == 1)
+box.func.call_inc_g:drop()
+box.func.call_inc_g_safe:drop()
+
+-- Test persistent function assemble corner cases
+box.schema.func.create('compiletime_tablef', {body = "{}"})
+box.schema.func.create('compiletime_call_inc_g', {body = "inc_g()"})
+assert(sum == 1)
+
 test_run:cmd("clear filter")
 
 --
@@ -198,3 +271,24 @@ box.begin() box.space._func:delete{f.id} f = box.func.test box.rollback()
 f == nil
 box.func.test ~= nil
 box.func.test:drop()
+
+-- Check SQL builtins
+test_run:cmd("setopt delimiter ';'")
+sql_builtin_list = {
+	"TRIM", "TYPEOF", "PRINTF", "UNICODE", "CHAR", "HEX", "VERSION",
+	"QUOTE", "REPLACE", "SUBSTR", "GROUP_CONCAT", "JULIANDAY", "DATE",
+	"TIME", "DATETIME", "STRFTIME", "CURRENT_TIME", "CURRENT_TIMESTAMP",
+	"CURRENT_DATE", "LENGTH", "POSITION", "ROUND", "UPPER", "LOWER",
+	"IFNULL", "RANDOM", "CEIL", "CEILING", "CHARACTER_LENGTH",
+	"CHAR_LENGTH", "FLOOR", "MOD", "OCTET_LENGTH", "ROW_COUNT", "COUNT",
+	"LIKE", "ABS", "EXP", "LN", "POWER", "SQRT", "SUM", "TOTAL", "AVG",
+	"RANDOMBLOB", "NULLIF", "ZEROBLOB", "MIN", "MAX", "COALESCE", "EVERY",
+	"EXISTS", "EXTRACT", "SOME", "_sql_record", "_sql_stat_get",
+	"_sql_stat_push", "_sql_stat_init",
+}
+test_run:cmd("setopt delimiter ''");
+ok = true
+for _, v in pairs(sql_builtin_list) do ok = ok and (box.space._func.index.name:get(v) ~= nil) end
+ok == true
+
+box.func.LUA:call({"return 1 + 1"})
diff --git a/test/wal_off/func_max.result b/test/wal_off/func_max.result
index ab4217845..83b05af18 100644
--- a/test/wal_off/func_max.result
+++ b/test/wal_off/func_max.result
@@ -42,11 +42,11 @@ test_run:cmd("setopt delimiter ''");
 ...
 func_limit()
 ---
-- error: 'Failed to create function ''func32000'': function id is too big'
+- error: 'Failed to create function ''func31941'': function id is too big'
 ...
 drop_limit_func()
 ---
-- error: Function 'func32000' does not exist
+- error: Function 'func31941' does not exist
 ...
 box.schema.user.create('testuser')
 ---
@@ -62,11 +62,11 @@ session.su('testuser')
 ...
 func_limit()
 ---
-- error: 'Failed to create function ''func32000'': function id is too big'
+- error: 'Failed to create function ''func31941'': function id is too big'
 ...
 drop_limit_func()
 ---
-- error: Function 'func32000' does not exist
+- error: Function 'func31941' does not exist
 ...
 session.su('admin')
 ---
-- 
2.21.0




More information about the Tarantool-patches mailing list