Tarantool development patches archive
 help / color / mirror / Atom feed
* [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem
@ 2019-07-10 11:00 Kirill Shcherbatov
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 01/12] sql: get rid of SOUNDEX, MATCH Kirill Shcherbatov
                   ` (11 more replies)
  0 siblings, 12 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:00 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

This patchset reworks SQL functions subsystem. Previously Tarantool's
SQL used an own FuncDef function representation and corresponding
FuncDef cache. Replacing it with Tarantool's uniform function
representation makes possible to call any function on any supported
language in SQL.

The commit 'introduce Lua persistent functions' has been reviewed by
Vova in past, but reverted by Kostya, becuase it introduces schema
changes making sense in scope of this new patchset. Mostly it should be
good enough.

Please pay attention, that it updates test-run module and an
pretest_cleanup updates must be merged before it.

Changes in version 2:
  - patches are reordered in the patchset
  - fixed compilation
  - dropped _sql_record entry, introduced GREATER, LESSER entries
  - few minor fixes

Branch: http://github.com/tarantool/tarantool/tree/kshch/gh-4182-persistent-functions
Issue: https://github.com/tarantool/tarantool/issues/4182

Kirill Shcherbatov (12):
  sql: get rid of SOUNDEX, MATCH
  sql: get rid of LIKELY, UNLIKELY and LIKEHOOD
  sql: put analyze helpers to FuncDef cache
  sql: rework LIKE case-insensitive mode
  sql: replace bool is_derived_coll marker with flag
  sql: remove SQL_PreferBuiltin flag
  sql: move LIKE UConverter object to collation library
  sql: rfc for SQL and Lua functions
  box: introduce Lua persistent functions
  sql: refactor builtins signatures with port
  box: use own vtab per each function object
  sql: use schema's func hash instead of FuncDef hash

 doc/rfc/4182-persistent-lua-functions.md |  214 +++
 src/box/CMakeLists.txt                   |    1 -
 src/box/alter.cc                         |  155 +-
 src/box/bootstrap.                       |  Bin 0 -> 5528 bytes
 src/box/bootstrap.snap                   |  Bin 4475 -> 5794 bytes
 src/box/call.c                           |    1 +
 src/box/execute.c                        |    2 +
 src/box/func.c                           |   31 +-
 src/box/func.h                           |    7 +-
 src/box/func_def.c                       |   43 +-
 src/box/func_def.h                       |   61 +-
 src/box/lua/call.c                       |  249 ++-
 src/box/lua/lua_sql.c                    |  204 ---
 src/box/lua/lua_sql.h                    |   39 -
 src/box/lua/schema.lua                   |   21 +-
 src/box/lua/upgrade.lua                  |   67 +-
 src/box/port.c                           |    5 +
 src/box/port.h                           |   19 +
 src/box/schema_def.h                     |   14 +
 src/box/sql.h                            |   18 +
 src/box/sql/analyze.c                    |  174 +-
 src/box/sql/callback.c                   |  211 ---
 src/box/sql/date.c                       |   28 -
 src/box/sql/expr.c                       |   77 +-
 src/box/sql/func.c                       | 2117 ++++++++++++++--------
 src/box/sql/global.c                     |    7 -
 src/box/sql/main.c                       |  160 +-
 src/box/sql/pragma.c                     |    8 -
 src/box/sql/printf.c                     |    7 +-
 src/box/sql/resolve.c                    |  107 +-
 src/box/sql/select.c                     |   10 +-
 src/box/sql/sqlInt.h                     |  258 +--
 src/box/sql/vdbe.c                       |  112 +-
 src/box/sql/vdbe.h                       |    6 +-
 src/box/sql/vdbeInt.h                    |   38 +-
 src/box/sql/vdbeapi.c                    |  194 +-
 src/box/sql/vdbeaux.c                    |   31 +-
 src/box/sql/vdbemem.c                    |  144 +-
 src/box/sql/where.c                      |  166 +-
 src/box/sql/whereInt.h                   |    1 -
 src/box/sql/whereexpr.c                  |   10 +-
 src/lib/coll/coll.c                      |    8 +-
 src/lib/coll/coll.h                      |    2 +
 src/lib/core/port.h                      |   31 +
 test-run                                 |    2 +-
 test/box-py/bootstrap.result             |   76 +-
 test/box-py/bootstrap.test.py            |    2 +-
 test/box/access.result                   |    2 +-
 test/box/access.test.lua                 |    2 +-
 test/box/access_bin.result               |    2 +-
 test/box/access_bin.test.lua             |    2 +-
 test/box/access_misc.result              |  133 +-
 test/box/access_sysview.result           |    8 +-
 test/box/alter.result                    |    2 +-
 test/box/function1.result                |  392 +++-
 test/box/function1.test.lua              |  133 +-
 test/sql-tap/alias.test.lua              |   11 +-
 test/sql-tap/check.test.lua              |   13 +-
 test/sql-tap/collation.test.lua          |    2 +-
 test/sql-tap/e_expr.test.lua             |   26 +-
 test/sql-tap/func.test.lua               |   75 +-
 test/sql-tap/func3.test.lua              |  289 ---
 test/sql-tap/func5.test.lua              |   29 +-
 test/sql-tap/lua_sql.test.lua            |  120 +-
 test/sql-tap/orderby5.test.lua           |    2 +-
 test/sql-tap/select4.test.lua            |   12 +-
 test/sql-tap/sql-errors.test.lua         |   12 +-
 test/sql-tap/subquery.test.lua           |   21 +-
 test/sql-tap/trigger9.test.lua           |    6 +-
 test/sql-tap/where2.test.lua             |    4 +-
 test/sql-tap/whereG.test.lua             |   87 +-
 test/sql/errinj.result                   |   25 -
 test/sql/errinj.test.lua                 |   10 -
 test/sql/func-recreate.result            |   41 +-
 test/sql/func-recreate.test.lua          |   28 +-
 test/wal_off/func_max.result             |    8 +-
 76 files changed, 3653 insertions(+), 2982 deletions(-)
 create mode 100644 doc/rfc/4182-persistent-lua-functions.md
 create mode 100644 src/box/bootstrap.
 delete mode 100644 src/box/lua/lua_sql.c
 delete mode 100644 src/box/lua/lua_sql.h
 delete mode 100755 test/sql-tap/func3.test.lua

-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 01/12] sql: get rid of SOUNDEX, MATCH
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
@ 2019-07-10 11:00 ` Kirill Shcherbatov
  2019-07-10 18:45   ` [tarantool-patches] " Konstantin Osipov
  2019-07-12  8:44   ` Kirill Yukhin
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 10/12] sql: refactor builtins signatures with port Kirill Shcherbatov
                   ` (10 subsequent siblings)
  11 siblings, 2 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:00 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

In relation with FuncDef cache rework we need to clean-up
builtins list. The SOUNDEX function is not in use while MATCH
fucntion is a stub that raises an error, so they could be
dropped.

Needed for #4182
---
 src/box/sql/func.c           | 92 ------------------------------------
 src/box/sql/main.c           |  6 ---
 src/box/sql/sqlInt.h         |  2 -
 src/box/sql/vdbeapi.c        | 20 --------
 test/sql-tap/e_expr.test.lua | 26 +---------
 test/sql-tap/func.test.lua   | 75 +----------------------------
 6 files changed, 2 insertions(+), 219 deletions(-)

diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index 761a3abae..f0e1b1f0c 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -1468,67 +1468,6 @@ trim_func_three_args(struct sql_context *context, int argc, sql_value **argv)
 	sql_free(char_len);
 }
 
-/* IMP: R-25361-16150 This function is omitted from sql by default. It
- * is only available if the SQL_SOUNDEX compile-time option is used
- * when sql is built.
- */
-#ifdef SQL_SOUNDEX
-/*
- * Compute the soundex encoding of a word.
- *
- * IMP: R-59782-00072 The soundex(X) function returns a string that is the
- * soundex encoding of the string X.
- */
-static void
-soundexFunc(sql_context * context, int argc, sql_value ** argv)
-{
-	char zResult[8];
-	const u8 *zIn;
-	int i, j;
-	static const unsigned char iCode[] = {
-		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-		0, 0, 1, 2, 3, 0, 1, 2, 0, 0, 2, 2, 4, 5, 5, 0,
-		1, 2, 6, 2, 3, 0, 1, 0, 2, 0, 2, 0, 0, 0, 0, 0,
-		0, 0, 1, 2, 3, 0, 1, 2, 0, 0, 2, 2, 4, 5, 5, 0,
-		1, 2, 6, 2, 3, 0, 1, 0, 2, 0, 2, 0, 0, 0, 0, 0,
-	};
-	assert(argc == 1);
-	zIn = (u8 *) sql_value_text(argv[0]);
-	if (zIn == 0)
-		zIn = (u8 *) "";
-	for (i = 0; zIn[i] && !sqlIsalpha(zIn[i]); i++) {
-	}
-	if (zIn[i]) {
-		u8 prevcode = iCode[zIn[i] & 0x7f];
-		zResult[0] = sqlToupper(zIn[i]);
-		for (j = 1; j < 4 && zIn[i]; i++) {
-			int code = iCode[zIn[i] & 0x7f];
-			if (code > 0) {
-				if (code != prevcode) {
-					prevcode = code;
-					zResult[j++] = code + '0';
-				}
-			} else {
-				prevcode = 0;
-			}
-		}
-		while (j < 4) {
-			zResult[j++] = '0';
-		}
-		zResult[j] = 0;
-		sql_result_text(context, zResult, 4, SQL_TRANSIENT);
-	} else {
-		/* IMP: R-64894-50321 The string "?000" is returned if the argument
-		 * is NULL or contains no ASCII alphabetic characters.
-		 */
-		sql_result_text(context, "?000", 4, SQL_STATIC);
-	}
-}
-#endif				/* SQL_SOUNDEX */
-
 /*
  * An instance of the following structure holds the context of a
  * sum() or avg() aggregate computation.
@@ -1763,34 +1702,6 @@ groupConcatFinalize(sql_context * context)
 	}
 }
 
-/*
- * If the function already exists as a regular global function, then
- * this routine is a no-op.  If the function does not exist, then create
- * a new one that always throws a run-time error.
- */
-static inline int
-sql_overload_function(sql * db, const char *zName,
-			  enum field_type type, int nArg)
-{
-	if (sqlFindFunction(db, zName, nArg, 0) == 0) {
-		return sqlCreateFunc(db, zName, type, nArg, 0, 0,
-				     sqlInvalidFunction, 0, 0, 0);
-	}
-	return 0;
-}
-
-/*
- * This routine does per-connection function registration.  Most
- * of the built-in functions above are part of the global function set.
- * This routine only deals with those that are not global.
- */
-void
-sqlRegisterPerConnectionBuiltinFunctions(sql * db)
-{
-	if (sql_overload_function(db, "MATCH", FIELD_TYPE_SCALAR, 2) != 0)
-		sqlOomFault(db);
-}
-
 /*
  * Set the LIKEOPT flag on the 2-argument function with the given name.
  */
@@ -1869,9 +1780,6 @@ sqlRegisterBuiltinFunctions(void)
 	 * For peak efficiency, put the most frequently used function last.
 	 */
 	static FuncDef aBuiltinFunc[] = {
-#ifdef SQL_SOUNDEX
-		FUNCTION(soundex, 1, 0, 0, soundexFunc),
-#endif
 		FUNCTION2(unlikely, 1, 0, 0, noopFunc, SQL_FUNC_UNLIKELY,
 			  FIELD_TYPE_BOOLEAN),
 		FUNCTION2(likelihood, 2, 0, 0, noopFunc, SQL_FUNC_UNLIKELY,
diff --git a/src/box/sql/main.c b/src/box/sql/main.c
index bfe3d7186..89d83d242 100644
--- a/src/box/sql/main.c
+++ b/src/box/sql/main.c
@@ -460,12 +460,6 @@ sql_init_db(sql **out_db)
 		return -1;
 	}
 
-	/* Register all built-in functions, but do not attempt to read the
-	 * database schema yet. This is delayed until the first time the database
-	 * is accessed.
-	 */
-	sqlRegisterPerConnectionBuiltinFunctions(db);
-
 	*out_db = db;
 	return 0;
 }
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 73dc6e4d7..9305f7d5c 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -3524,7 +3524,6 @@ void sqlInsertBuiltinFuncs(FuncDef *, int);
 FuncDef *sqlFindFunction(sql *, const char *, int, u8);
 void sqlRegisterBuiltinFunctions(void);
 void sqlRegisterDateTimeFunctions(void);
-void sqlRegisterPerConnectionBuiltinFunctions(sql *);
 
 /**
  * Evaluate a view and store its result in an ephemeral table.
@@ -4359,7 +4358,6 @@ void sqlParser(void *, int, Token, Parse *);
 int sqlParserStackPeak(void *);
 #endif
 
-void sqlInvalidFunction(sql_context *, int, sql_value **);
 sql_int64 sqlStmtCurrentTime(sql_context *);
 int sqlVdbeParameterIndex(Vdbe *, const char *, int);
 int sqlTransferBindings(sql_stmt *, sql_stmt *);
diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
index f470ac6b1..ef0ab79d2 100644
--- a/src/box/sql/vdbeapi.c
+++ b/src/box/sql/vdbeapi.c
@@ -519,26 +519,6 @@ sqlStmtCurrentTime(sql_context * p)
 	return *piTime;
 }
 
-/*
- * The following is the implementation of an SQL function that always
- * fails with an error message stating that the function is used in the
- * wrong context.  The sql_overload_function() API might construct
- * SQL function that use this routine so that the functions will exist
- * for name resolution.
- */
-void
-sqlInvalidFunction(sql_context * context,	/* The function calling context */
-		       int NotUsed,	/* Number of arguments to the function */
-		       sql_value ** NotUsed2	/* Value of each argument */
-    )
-{
-	const char *zName = context->pFunc->zName;
-	UNUSED_PARAMETER2(NotUsed, NotUsed2);
-	const char *err = "unable to use function %s in the requested context";
-	diag_set(ClientError, ER_SQL_EXECUTE, tt_sprintf(err, zName));
-	context->is_aborted = true;
-}
-
 /*
  * Create a new aggregate context for p and return a pointer to
  * its pMem->z element.
diff --git a/test/sql-tap/e_expr.test.lua b/test/sql-tap/e_expr.test.lua
index 7b80651a8..6d56bf58b 100755
--- a/test/sql-tap/e_expr.test.lua
+++ b/test/sql-tap/e_expr.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(10641)
+test:plan(10639)
 
 --!./tcltestrunner.lua
 -- 2010 July 16
@@ -2405,30 +2405,6 @@ test:do_test(
         -- </e_expr-18.2.4>
     })
 
---sql("db", "test.db")
--- EVIDENCE-OF: R-42037-37826 The default match() function implementation
--- raises an exception and is not really useful for anything.
---
-test:do_catchsql_test(
-    "e_expr-19.1.1",
-    [[
-        SELECT 'abc' MATCH 'def'
-    ]], {
-        -- <e_expr-19.1.1>
-        1, "unable to use function MATCH in the requested context"
-        -- </e_expr-19.1.1>
-    })
-
-test:do_catchsql_test(
-    "e_expr-19.1.2",
-    [[
-        SELECT match('abc', 'def')
-    ]], {
-        -- <e_expr-19.1.2>
-        1, "unable to use function MATCH in the requested context"
-        -- </e_expr-19.1.2>
-    })
-
 -- EVIDENCE-OF: R-37916-47407 The MATCH operator is a special syntax for
 -- the match() application-defined function.
 --
diff --git a/test/sql-tap/func.test.lua b/test/sql-tap/func.test.lua
index f9044ad01..64d53bac8 100755
--- a/test/sql-tap/func.test.lua
+++ b/test/sql-tap/func.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(14590)
+test:plan(14586)
 
 --!./tcltestrunner.lua
 -- 2001 September 15
@@ -1734,79 +1734,6 @@ test:do_catchsql_test(
         -- </func-18.32>
     })
 
--- The MATCH function exists but is only a stub and always throws an error.
---
-test:do_execsql_test(
-    "func-19.1",
-    [[
-        SELECT match(a,b) FROM t1 WHERE false;
-    ]], {
-        -- <func-19.1>
-        
-        -- </func-19.1>
-    })
-
-test:do_catchsql_test(
-    "func-19.2",
-    [[
-        SELECT 'abc' MATCH 'xyz';
-    ]], {
-        -- <func-19.2>
-        1, "Failed to execute SQL statement: unable to use function MATCH in the requested context"
-        -- </func-19.2>
-    })
-
-test:do_catchsql_test(
-    "func-19.3",
-    [[
-        SELECT 'abc' NOT MATCH 'xyz';
-    ]], {
-        -- <func-19.3>
-        1, "Failed to execute SQL statement: unable to use function MATCH in the requested context"
-        -- </func-19.3>
-    })
-
-test:do_catchsql_test(
-    "func-19.4",
-    [[
-        SELECT match(1,2,3);
-    ]], {
-        -- <func-19.4>
-        1, "wrong number of arguments to function MATCH()"
-        -- </func-19.4>
-    })
-
--- Soundex tests.
---
--- false condition for current tarantool version
-if pcall( function() test:execsql("SELECT soundex('hello')") end ) then
-    for i, val in ipairs({
-        {"euler", "E460"},
-        {"EULER", "E460"},    
-        {"Euler", "E460"},    
-        {"ellery", "E460"},    
-        {"gauss", "G200"},    
-        {"ghosh", "G200"},    
-        {"hilbert", "H416"},    
-        {"Heilbronn", "H416"},    
-        {"knuth", "K530"},    
-        {"kant", "K530"},    
-        {"Lloyd", "L300"},    
-        {"LADD", "L300"},    
-        {"Lukasiewicz", "L222"},    
-        {"Lissajous", "L222"},    
-        {"A", "A000"},    
-        {"12345", "?000"} }) do
-        local name = val[1]
-        local sdx = val[2]
-        test:do_execsql_test(
-            "func-20."..i,
-            string.format("SELECT soundex('%s')", name), {
-                sdx
-            })
-
-    end
-end
 -- Tests of the REPLACE function.
 --
 test:do_catchsql_test(
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 10/12] sql: refactor builtins signatures with port
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 01/12] sql: get rid of SOUNDEX, MATCH Kirill Shcherbatov
@ 2019-07-10 11:00 ` Kirill Shcherbatov
  2019-07-10 18:47   ` [tarantool-patches] " Konstantin Osipov
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 11/12] box: use own vtab per each function object Kirill Shcherbatov
                   ` (9 subsequent siblings)
  11 siblings, 1 reply; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:00 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

Reworked SQL builtins signatures to use args and ret abstract
ports to pass arguments and return the execution result.
The new approach allows to support calling sql builtins functions
from Lua in future and to use uniform func_call API in
OP_Function opcode.

Needed for #4113, #2200, #2233
---
 src/box/call.c        |    1 +
 src/box/execute.c     |    2 +
 src/box/lua/call.c    |    2 +
 src/box/lua/lua_sql.c |   41 +-
 src/box/port.c        |    2 +
 src/box/port.h        |   19 +
 src/box/sql/analyze.c |  130 ++--
 src/box/sql/func.c    | 1400 +++++++++++++++++++++++++----------------
 src/box/sql/main.c    |   58 +-
 src/box/sql/printf.c  |    7 +-
 src/box/sql/sqlInt.h  |  101 +--
 src/box/sql/vdbe.c    |  101 ++-
 src/box/sql/vdbeInt.h |   18 +-
 src/box/sql/vdbeapi.c |  163 +----
 src/box/sql/vdbemem.c |   51 +-
 src/lib/core/port.h   |   31 +
 16 files changed, 1197 insertions(+), 930 deletions(-)

diff --git a/src/box/call.c b/src/box/call.c
index ac2bf3004..b459a9839 100644
--- a/src/box/call.c
+++ b/src/box/call.c
@@ -73,6 +73,7 @@ static const struct port_vtab port_msgpack_vtab = {
 	.dump_lua = port_msgpack_dump_lua,
 	.dump_plain = NULL,
 	.get_msgpack = port_msgpack_get_msgpack,
+	.get_vdbemem = NULL,
 	.destroy = NULL,
 };
 
diff --git a/src/box/execute.c b/src/box/execute.c
index a9ca2e67a..0dc1bc46d 100644
--- a/src/box/execute.c
+++ b/src/box/execute.c
@@ -109,6 +109,8 @@ const struct port_vtab port_sql_vtab = {
 	/* .dump_lua = */ port_sql_dump_lua,
 	/* .dump_plain = */ NULL,
 	/* .get_msgpack = */ NULL,
+	/* .get_vdbemem = */ NULL,
+	/* .get_context = */ NULL,
 	/* .destroy = */ port_sql_destroy,
 };
 
diff --git a/src/box/lua/call.c b/src/box/lua/call.c
index 95fac4834..4297218cd 100644
--- a/src/box/lua/call.c
+++ b/src/box/lua/call.c
@@ -507,6 +507,8 @@ static const struct port_vtab port_lua_vtab = {
 	.dump_lua = port_lua_dump_lua,
 	.dump_plain = port_lua_dump_plain,
 	.get_msgpack = port_lua_get_msgpack,
+	.get_vdbemem = NULL,
+	.get_context = NULL,
 	.destroy = port_lua_destroy,
 };
 
diff --git a/src/box/lua/lua_sql.c b/src/box/lua/lua_sql.c
index 4d90a53de..0c5797fa2 100644
--- a/src/box/lua/lua_sql.c
+++ b/src/box/lua/lua_sql.c
@@ -34,6 +34,7 @@
 
 #include "box/lua/call.h"
 #include "box/sql/sqlInt.h"
+#include "box/port.h"
 #include "box/sql/vdbeInt.h"
 
 struct lua_sql_func_info {
@@ -47,15 +48,28 @@ struct lua_sql_func_info {
  * Lua func should be previously registered in sql
  * (see lbox_sql_create_function).
  */
-static void
-lua_sql_call(sql_context *pCtx, int nVal, sql_value **apVal) {
+static int
+lua_sql_call(struct func *func, struct port *args, struct port *ret)
+{
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	lua_State *L = lua_newthread(tarantool_L);
 	int coro_ref = luaL_ref(tarantool_L, LUA_REGISTRYINDEX);
-	struct lua_sql_func_info *func_info = sql_user_data(pCtx);
+	struct lua_sql_func_info *func_info = sql_user_data(ctx);
 
 	lua_rawgeti(L, LUA_REGISTRYINDEX, func_info->func_ref);
-	for (int i = 0; i < nVal; i++) {
-		sql_value *param = apVal[i];
+	for (uint32_t i = 0; i < argc; i++) {
+		sql_value *param = (sql_value *)&argv[i];
 		switch (sql_value_type(param)) {
 		case MP_INT:
 			luaL_pushint64(L, sql_value_int64(param));
@@ -79,38 +93,37 @@ lua_sql_call(sql_context *pCtx, int nVal, sql_value **apVal) {
 		default:
 			diag_set(ClientError, ER_SQL_EXECUTE, "Unsupported "\
 				 "type passed to Lua");
-			pCtx->is_aborted = true;
 			goto error;
 		}
 	}
 	if (lua_pcall(L, lua_gettop(L) - 1, 1, 0) != 0){
 		diag_set(ClientError, ER_SQL_EXECUTE, lua_tostring(L, -1));
-		pCtx->is_aborted = true;
 		goto error;
 	}
 	switch(lua_type(L, -1)) {
 	case LUA_TBOOLEAN:
-		sql_result_bool(pCtx, lua_toboolean(L, -1));
+		mem_set_bool(val, lua_toboolean(L, -1));
 		break;
 	case LUA_TNUMBER:
-		sql_result_double(pCtx, lua_tonumber(L, -1));
+		sqlVdbeMemSetDouble(val, lua_tonumber(L, -1));
 		break;
 	case LUA_TSTRING:
-		sql_result_text(pCtx, lua_tostring(L, -1), -1,
-				    SQL_TRANSIENT);
+		 if (sqlVdbeMemSetStr(val, lua_tostring(L, -1), -1,
+					1, SQL_TRANSIENT) != 0)
+			return -1;
 		break;
 	case LUA_TNIL:
-		sql_result_null(pCtx);
+		sqlVdbeMemSetNull(val);
 		break;
 	default:
 		diag_set(ClientError, ER_SQL_EXECUTE, "Unsupported type "\
 			 "passed from Lua");
-		pCtx->is_aborted = true;
 		goto error;
 	}
+	return 0;
 error:
 	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, coro_ref);
-	return;
+	return -1;
 }
 
 static void
diff --git a/src/box/port.c b/src/box/port.c
index 7f552bcfe..9e4ab9453 100644
--- a/src/box/port.c
+++ b/src/box/port.c
@@ -146,5 +146,7 @@ const struct port_vtab port_tuple_vtab = {
 	.dump_lua = port_tuple_dump_lua,
 	.dump_plain = NULL,
 	.get_msgpack = NULL,
+	.get_vdbemem = NULL,
+	.get_context = NULL,
 	.destroy = port_tuple_destroy,
 };
diff --git a/src/box/port.h b/src/box/port.h
index a7f5d81bd..f14d61189 100644
--- a/src/box/port.h
+++ b/src/box/port.h
@@ -95,6 +95,25 @@ static_assert(sizeof(struct port_msgpack) <= sizeof(struct port),
 void
 port_msgpack_create(struct port *port, const char *data, uint32_t data_sz);
 
+struct sql_value;
+struct sql_context;
+
+/** Port implementation used with vdbe memory variables. */
+struct port_vdbemem {
+	const struct port_vtab *vtab;
+	struct sql_context *ctx;
+	struct sql_value *mem;
+	uint32_t size;
+};
+
+static_assert(sizeof(struct port_vdbemem) <= sizeof(struct port),
+	      "sizeof(struct port_vdbemem) must be <= sizeof(struct port)");
+
+/** Initialize a port to dump data in sql vdbe memory. */
+void
+port_vdbemem_create(struct port *base, struct sql_value *mem, uint32_t size,
+		    struct sql_context *ctx);
+
 /** Port for storing the result of a Lua CALL/EVAL. */
 struct port_lua {
 	const struct port_vtab *vtab;
diff --git a/src/box/sql/analyze.c b/src/box/sql/analyze.c
index 512904cc1..7764b48c5 100644
--- a/src/box/sql/analyze.c
+++ b/src/box/sql/analyze.c
@@ -109,6 +109,7 @@
 #include "box/index.h"
 #include "box/key_def.h"
 #include "box/schema.h"
+#include "box/port.h"
 #include "third_party/qsort_arg.h"
 
 #include "sqlInt.h"
@@ -263,9 +264,19 @@ stat4Destructor(void *pOld)
  * return value is BLOB, but it is really just a pointer to the Stat4Accum
  * object.
  */
-static void
-statInit(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_stat_init(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	Stat4Accum *p;
 	int nCol;		/* Number of columns in index being sampled */
 	int nKeyCol;		/* Number of key columns */
@@ -275,8 +286,7 @@ statInit(sql_context * context, int argc, sql_value ** argv)
 	int mxSample = SQL_STAT4_SAMPLES;
 
 	/* Decode the three function arguments */
-	UNUSED_PARAMETER(argc);
-	nCol = sql_value_int(argv[0]);
+	nCol = sql_value_int(&argv[0]);
 	assert(nCol > 0);
 	/* Tarantool: we use an additional artificial column for the reason
 	 * that Tarantool's indexes don't contain PK columns after key columns.
@@ -284,7 +294,7 @@ statInit(sql_context * context, int argc, sql_value ** argv)
 	 * identical rows, we have to use this artificial column.
 	 */
 	nColUp = sizeof(tRowcnt) < 8 ? (nCol + 2) & ~1 : nCol + 1;
-	nKeyCol = sql_value_int(argv[1]);
+	nKeyCol = sql_value_int(&argv[1]);
 	assert(nKeyCol <= nCol);
 	assert(nKeyCol > 0);
 
@@ -295,12 +305,10 @@ statInit(sql_context * context, int argc, sql_value ** argv)
 	    + sizeof(tRowcnt) * nColUp	/* Stat4Accum.anLt */
 	    + sizeof(Stat4Sample) * (nCol + 1 + mxSample)	/* Stat4Accum.aBest[], a[] */
 	    + sizeof(tRowcnt) * 3 * nColUp * (nCol + 1 + mxSample);
-	db = sql_context_db_handle(context);
+	db = sql_get();
 	p = sqlDbMallocZero(db, n);
-	if (p == 0) {
-		context->is_aborted = true;
-		return;
-	}
+	if (p == NULL)
+		return -1;
 
 	p->db = db;
 	p->nRow = 0;
@@ -316,12 +324,12 @@ statInit(sql_context * context, int argc, sql_value ** argv)
 		p->iGet = -1;
 		p->mxSample = mxSample;
 		p->nPSample =
-		    (tRowcnt) (sql_value_int64(argv[2]) /
+		    (tRowcnt) (sql_value_int64(&argv[2]) /
 			       (mxSample / 3 + 1) + 1);
 		p->current.anLt = &p->current.anEq[nColUp];
 		p->iPrn =
 		    0x689e962d * (u32) nCol ^ 0xd0944565 *
-		    (u32) sql_value_int(argv[2]);
+		    (u32) sql_value_int(&argv[2]);
 
 		/* Set up the Stat4Accum.a[] and aBest[] arrays */
 		p->a = (struct Stat4Sample *)&p->current.anLt[nColUp];
@@ -347,7 +355,8 @@ statInit(sql_context * context, int argc, sql_value ** argv)
 	 * (given by the 3rd parameter) is never used and can be any positive
 	 * value.
 	 */
-	sql_result_blob(context, p, sizeof(*p), stat4Destructor);
+	return sqlVdbeMemSetStr(val, (const char *)p, sizeof(*p), 0,
+				stat4Destructor) != 0 ? -1 : 0;
 }
 
 /*
@@ -535,16 +544,21 @@ samplePushPrevious(Stat4Accum * p, int iChng)
  *
  * The R parameter is only used for STAT4
  */
-static void
-statPush(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_stat_push(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	(void) ret;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+
 	int i;
 	/* The three function arguments */
-	Stat4Accum *p = (Stat4Accum *) sql_value_blob(argv[0]);
-	int iChng = sql_value_int(argv[1]);
+	Stat4Accum *p = (Stat4Accum *) sql_value_blob(&argv[0]);
+	int iChng = sql_value_int(&argv[1]);
 
-	UNUSED_PARAMETER(argc);
-	UNUSED_PARAMETER(context);
 	assert(p->nCol > 0);
 	/* iChng == p->nCol means that the current and previous rows are identical */
 	assert(iChng <= p->nCol);
@@ -569,8 +583,8 @@ statPush(sql_context * context, int argc, sql_value ** argv)
 		}
 	}
 	p->nRow++;
-	sampleSetKey(p->db, &p->current, sql_value_bytes(argv[2]),
-		     sql_value_blob(argv[2]));
+	sampleSetKey(p->db, &p->current, sql_value_bytes(&argv[2]),
+		     sql_value_blob(&argv[2]));
 	p->current.iHash = p->iPrn = p->iPrn * 1103515245 + 12345;
 	{
 		tRowcnt nLt = p->current.anLt[p->nCol];
@@ -592,6 +606,7 @@ statPush(sql_context * context, int argc, sql_value ** argv)
 			}
 		}
 	}
+	return 0;
 }
 
 #define STAT_GET_STAT1 0	/* "stat" column of stat1 table */
@@ -608,12 +623,22 @@ statPush(sql_context * context, int argc, sql_value ** argv)
  * The content to returned is determined by the parameter J
  * which is one of the STAT_GET_xxxx values defined above.
  */
-static void
-statGet(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_stat_get(struct func *func, struct port *args, struct port *ret)
 {
-	Stat4Accum *p = (Stat4Accum *) sql_value_blob(argv[0]);
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
+	struct Stat4Accum *p = (struct Stat4Accum *) sql_value_blob(&argv[0]);
 	/* STAT4 have a parameter on this routine. */
-	int eCall = sql_value_int(argv[1]);
+	int eCall = sql_value_int(&argv[1]);
 	assert(argc == 2);
 	assert(eCall == STAT_GET_STAT1 || eCall == STAT_GET_NEQ
 	       || eCall == STAT_GET_KEY || eCall == STAT_GET_NLT
@@ -644,10 +669,8 @@ statGet(sql_context * context, int argc, sql_value ** argv)
 		int i;
 
 		char *zRet = sqlMallocZero((p->nKeyCol + 1) * 25);
-		if (zRet == 0) {
-			context->is_aborted = true;
-			return;
-		}
+		if (zRet == NULL)
+			return -1;
 
 		sql_snprintf(24, zRet, "%llu", (u64) p->nRow);
 		z = zRet + sqlStrlen30(zRet);
@@ -659,8 +682,8 @@ statGet(sql_context * context, int argc, sql_value ** argv)
 			assert(p->current.anEq[i]);
 		}
 		assert(z[0] == '\0' && z > zRet);
-
-		sql_result_text(context, zRet, -1, sql_free);
+		if (sqlVdbeMemSetStr(val, zRet, -1, 1, SQL_DYNAMIC) != 0)
+			return -1;
 	} else if (eCall == STAT_GET_KEY) {
 		if (p->iGet < 0) {
 			samplePushPrevious(p, 0);
@@ -668,8 +691,9 @@ statGet(sql_context * context, int argc, sql_value ** argv)
 		}
 		if (p->iGet < p->nSample) {
 			Stat4Sample *pS = p->a + p->iGet;
-			sql_result_blob(context, pS->aKey, pS->nKey,
-					    SQL_TRANSIENT);
+			if (sqlVdbeMemSetStr(val, (const char *)pS->aKey,
+					     pS->nKey, 0, SQL_TRANSIENT) != 0)
+				return -1;
 		}
 	} else {
 		tRowcnt *aCnt = 0;
@@ -687,12 +711,11 @@ statGet(sql_context * context, int argc, sql_value ** argv)
 			p->iGet++;
 			break;
 		}
-	}
+		}
 
-	char *zRet = sqlMallocZero(p->nCol * 25);
-	if (zRet == 0) {
-		context->is_aborted = true;
-	} else {
+		char *zRet = sqlMallocZero(p->nCol * 25);
+		if (zRet == NULL)
+			return -1;
 		int i;
 		char *z = zRet;
 		for (i = 0; i < p->nCol; i++) {
@@ -701,17 +724,15 @@ statGet(sql_context * context, int argc, sql_value ** argv)
 		}
 		assert(z[0] == '\0' && z > zRet);
 		z[-1] = '\0';
-		sql_result_text(context, zRet, -1, sql_free);
+		if (sqlVdbeMemSetStr(val, zRet, -1, 1, SQL_DYNAMIC) != 0)
+			return -1;
 	}
-
-}
-#ifndef SQL_DEBUG
-UNUSED_PARAMETER(argc);
-#endif
+	return 0;
 }
 
 static void
-callStatGet(Vdbe * v, int regStat4, int iParam, int regOut)
+vdbe_emit_analyze_stat_get(struct Vdbe * v, int regStat4, int iParam,
+			   int regOut)
 {
 	assert(regOut != regStat4 && regOut != regStat4 + 1);
 	sqlVdbeAddOp2(v, OP_Integer, iParam, regStat4 + 1);
@@ -964,7 +985,7 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
 		sqlVdbeChangeP5(v, 3);
 		sqlVdbeAddOp2(v, OP_Next, idx_cursor, next_row_addr);
 		/* Add the entry to the stat1 table. */
-		callStatGet(v, stat4_reg, STAT_GET_STAT1, stat1_reg);
+		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_STAT1, stat1_reg);
 		enum field_type types[4] = { FIELD_TYPE_STRING,
 					     FIELD_TYPE_STRING,
 					     FIELD_TYPE_STRING,
@@ -982,12 +1003,12 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
 		int sample_key_reg = col_reg + part_count;
 		parse->nMem = MAX(parse->nMem, col_reg + part_count);
 		int next_addr = sqlVdbeCurrentAddr(v);
-		callStatGet(v, stat4_reg, STAT_GET_KEY, sample_key_reg);
+		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_KEY, sample_key_reg);
 		int is_null_addr = sqlVdbeAddOp1(v, OP_IsNull,
 						     sample_key_reg);
-		callStatGet(v, stat4_reg, STAT_GET_NEQ, eq_reg);
-		callStatGet(v, stat4_reg, STAT_GET_NLT, lt_reg);
-		callStatGet(v, stat4_reg, STAT_GET_NDLT, dlt_reg);
+		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_NEQ, eq_reg);
+		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_NLT, lt_reg);
+		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_NDLT, dlt_reg);
 		sqlVdbeAddOp4Int(v, OP_NotFound, tab_cursor, next_addr,
 				     sample_key_reg, 0);
 		/*
@@ -1752,9 +1773,12 @@ void
 sql_register_analyze_builtins(void)
 {
 	static FuncDef funcs[] = {
-		FUNCTION(_sql_stat_get, 2, 0, 0, statGet, FIELD_TYPE_ANY),
-		FUNCTION(_sql_stat_push, 3, 0, 0, statPush, FIELD_TYPE_ANY),
-		FUNCTION(_sql_stat_init, 3, 0, 0, statInit, FIELD_TYPE_ANY),
+		FUNCTION(_sql_stat_get, 2, 0, 0, sql_builtin_stat_get,
+			 FIELD_TYPE_ANY),
+		FUNCTION(_sql_stat_push, 3, 0, 0, sql_builtin_stat_push,
+			 FIELD_TYPE_ANY),
+		FUNCTION(_sql_stat_init, 3, 0, 0, sql_builtin_stat_init,
+			 FIELD_TYPE_ANY),
 	};
 	sqlInsertBuiltinFuncs(funcs, nelem(funcs));
 }
diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index d59aba9ee..18db8530d 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -39,6 +39,7 @@
 #include "version.h"
 #include "coll/coll.h"
 #include "box/func.h"
+#include "box/port.h"
 #include "tarantoolInt.h"
 #include "box/session.h"
 #include <unicode/ustring.h>
@@ -71,45 +72,99 @@ sqlSkipAccumulatorLoad(sql_context * context)
 	context->skipFlag = 1;
 }
 
+static const struct port_vtab port_vdbemem_vtab;
+
+void
+port_vdbemem_create(struct port *base, struct sql_value *mem, uint32_t size,
+		    struct sql_context *ctx)
+{
+	struct port_vdbemem *port = (struct port_vdbemem *) base;
+	port->vtab = &port_vdbemem_vtab;
+	port->mem = mem;
+	port->size = size;
+	port->ctx = ctx;
+}
+
+static struct sql_value *
+port_vdbemem_get_vdbemem(struct port *base, uint32_t *size)
+{
+	struct port_vdbemem *port = (struct port_vdbemem *) base;
+	assert(port->vtab == &port_vdbemem_vtab);
+	*size = port->size;
+	return port->mem;
+}
+
+void *
+port_vdbemem_get_context(struct port *base)
+{
+	struct port_vdbemem *port = (struct port_vdbemem *) base;
+	return port->ctx;
+}
+
+static const struct port_vtab port_vdbemem_vtab = {
+	.dump_msgpack = NULL,
+	.dump_msgpack_16 = NULL,
+	.dump_lua = NULL,
+	.dump_plain = NULL,
+	.get_msgpack = NULL,
+	.get_vdbemem = port_vdbemem_get_vdbemem,
+	.get_context = port_vdbemem_get_context,
+	.destroy = NULL,
+};
+
 /*
  * Implementation of the non-aggregate min() and max() functions
  */
-static void
-minmaxFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_minmax(struct func *func, struct port *args, struct port *ret)
 {
-	int i;
-	int mask;		/* 0 for min() or 0xffffffff for max() */
-	int iBest;
-	struct coll *pColl;
-
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
 	assert(argc > 1);
-	mask = sql_user_data(context) == 0 ? 0 : -1;
-	pColl = sqlGetFuncCollSeq(context);
+	/* 0 for min() or 0xffffffff for max() */
+	int mask = sql_user_data(ctx) == 0 ? 0 : -1;
 	assert(mask == -1 || mask == 0);
-	iBest = 0;
-	if (sql_value_is_null(argv[0]))
-		return;
-	for (i = 1; i < argc; i++) {
-		if (sql_value_is_null(argv[i]))
-			return;
-		if ((sqlMemCompare(argv[iBest], argv[i], pColl) ^ mask) >=
-		    0) {
-			testcase(mask == 0);
-			iBest = i;
-		}
+	struct coll *coll = sqlGetFuncCollSeq(ctx);
+
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
+	int best = 0;
+	if (sql_value_is_null(argv))
+		return 0;
+	for (uint32_t i = 1; i < argc; i++) {
+		if (sql_value_is_null(&argv[i]))
+			return 0;
+		if ((sqlMemCompare(&argv[best], &argv[i], coll) ^ mask) >= 0)
+			best = i;
 	}
-	sql_result_value(context, argv[iBest]);
+	return sqlVdbeMemCopy(val, &argv[best]) != 0 ? -1 : 0;
 }
 
 /*
  * Return the type of the argument.
  */
-static void
-typeofFunc(sql_context * context, int NotUsed, sql_value ** argv)
+static int
+sql_builtin_typeof(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
 	const char *z = 0;
-	UNUSED_PARAMETER(NotUsed);
-	switch (sql_value_type(argv[0])) {
+	switch (sql_value_type(&argv[0])) {
 	case MP_INT:
 		z = "integer";
 		break;
@@ -129,40 +184,48 @@ typeofFunc(sql_context * context, int NotUsed, sql_value ** argv)
 		z = "null";
 		break;
 	}
-	sql_result_text(context, z, -1, SQL_STATIC);
+	return sqlVdbeMemSetStr(val, z, -1, 1, SQL_STATIC) != 0 ? -1 : 0;
 }
 
 /*
  * Implementation of the length() function
  */
-static void
-lengthFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_length(struct func *func, struct port *args, struct port *ret)
 {
-	int len;
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
 
 	assert(argc == 1);
-	UNUSED_PARAMETER(argc);
-	switch (sql_value_type(argv[0])) {
+	switch (sql_value_type(&argv[0])) {
 	case MP_BIN:
 	case MP_INT:
 	case MP_DOUBLE:{
-			sql_result_int(context,
-					   sql_value_bytes(argv[0]));
+			sqlVdbeMemSetInt64(val, sql_value_bytes(&argv[0]));
 			break;
 		}
 	case MP_STR:{
-			const unsigned char *z = sql_value_text(argv[0]);
+			const unsigned char *z = sql_value_text(&argv[0]);
 			if (z == 0)
-				return;
-			len = sql_utf8_char_count(z, sql_value_bytes(argv[0]));
-			sql_result_int(context, len);
+				return 0;
+			int len = sql_utf8_char_count(z,
+						sql_value_bytes(&argv[0]));
+			sqlVdbeMemSetInt64(val, len);
 			break;
 		}
 	default:{
-			sql_result_null(context);
+			sqlVdbeMemSetNull(val);
 			break;
 		}
 	}
+	return 0;
 }
 
 /*
@@ -171,36 +234,43 @@ lengthFunc(sql_context * context, int argc, sql_value ** argv)
  * IMP: R-23979-26855 The abs(X) function returns the absolute value of
  * the numeric argument X.
  */
-static void
-absFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_abs(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	assert(argc == 1);
-	UNUSED_PARAMETER(argc);
-	switch (sql_value_type(argv[0])) {
+	switch (sql_value_type(&argv[0])) {
 	case MP_INT:{
-			i64 iVal = sql_value_int64(argv[0]);
+			i64 iVal = sql_value_int64(&argv[0]);
 			if (iVal < 0) {
 				if (iVal == SMALLEST_INT64) {
 					diag_set(ClientError, ER_SQL_EXECUTE,
 						 "integer is overflowed");
-					context->is_aborted = true;
-					return;
+					return -1;
 				}
 				iVal = -iVal;
 			}
-			sql_result_int64(context, iVal);
+			sqlVdbeMemSetInt64(val, iVal);
 			break;
 		}
 	case MP_NIL:{
 			/* IMP: R-37434-19929 Abs(X) returns NULL if X is NULL. */
-			sql_result_null(context);
+			sqlVdbeMemSetNull(val);
 			break;
 		}
 	case MP_BOOL: {
 		diag_set(ClientError, ER_INCONSISTENT_TYPES, "number",
 			 "boolean");
-		context->is_aborted = true;
-		return;
+		return -1;
 	}
 	default:{
 			/* Because sql_value_double() returns 0.0 if the argument is not
@@ -208,13 +278,14 @@ absFunc(sql_context * context, int argc, sql_value ** argv)
 			 * IMP: R-01992-00519 Abs(X) returns 0.0 if X is a string or blob
 			 * that cannot be converted to a numeric value.
 			 */
-			double rVal = sql_value_double(argv[0]);
+			double rVal = sql_value_double(&argv[0]);
 			if (rVal < 0)
 				rVal = -rVal;
-			sql_result_double(context, rVal);
+			sqlVdbeMemSetDouble(val, rVal);
 			break;
 		}
 	}
+	return 0;
 }
 
 /**
@@ -228,17 +299,28 @@ absFunc(sql_context * context, int argc, sql_value ** argv)
  * more than the number of bytes in haystack prior to the first
  * occurrence of needle, or 0 if needle never occurs in haystack.
  */
-static void
-position_func(struct sql_context *context, int argc, struct Mem **argv)
+static int
+sql_builtin_position(struct func *func, struct port *args, struct port *ret)
 {
-	UNUSED_PARAMETER(argc);
-	struct Mem *needle = argv[0];
-	struct Mem *haystack = argv[1];
+	(void)func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
+	struct Mem *needle = &argv[0];
+	struct Mem *haystack = &argv[1];
 	enum mp_type needle_type = sql_value_type(needle);
 	enum mp_type haystack_type = sql_value_type(haystack);
 
 	if (haystack_type == MP_NIL || needle_type == MP_NIL)
-		return;
+		return 0;
 	/*
 	 * Position function can be called only with string
 	 * or blob params.
@@ -251,8 +333,7 @@ position_func(struct sql_context *context, int argc, struct Mem **argv)
 	if (inconsistent_type_arg != NULL) {
 		diag_set(ClientError, ER_INCONSISTENT_TYPES, "TEXT or BLOB",
 			 mem_type_to_str(inconsistent_type_arg));
-		context->is_aborted = true;
-		return;
+		return -1;
 	}
 	/*
 	 * Both params of Position function must be of the same
@@ -261,8 +342,7 @@ position_func(struct sql_context *context, int argc, struct Mem **argv)
 	if (haystack_type != needle_type) {
 		diag_set(ClientError, ER_INCONSISTENT_TYPES,
 			 mem_type_to_str(needle), mem_type_to_str(haystack));
-		context->is_aborted = true;
-		return;
+		return -1;
 	}
 
 	int n_needle_bytes = sql_value_bytes(needle);
@@ -328,7 +408,7 @@ position_func(struct sql_context *context, int argc, struct Mem **argv)
 					       n_haystack_bytes);
 			}
 			int beg_offset = 0;
-			struct coll *coll = sqlGetFuncCollSeq(context);
+			struct coll *coll = sqlGetFuncCollSeq(ctx);
 			int c;
 			for (c = 0; c + n_needle_chars <= n_haystack_chars; c++) {
 				if (coll->cmp((const char *) haystack_str + beg_offset,
@@ -348,34 +428,47 @@ position_func(struct sql_context *context, int argc, struct Mem **argv)
 		}
 	}
 finish:
-	sql_result_int(context, position);
+	sqlVdbeMemSetInt64(val, position);
+	return 0;
 }
 
 /*
  * Implementation of the printf() function.
  */
-static void
-printfFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_printf(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	PrintfArguments x;
 	StrAccum str;
 	const char *zFormat;
 	int n;
-	sql *db = sql_context_db_handle(context);
+	struct sql *db = sql_get();
 
-	if (argc >= 1
-	    && (zFormat = (const char *)sql_value_text(argv[0])) != 0) {
+	if (argc >= 1 &&
+	    (zFormat = (const char *)sql_value_text(&argv[0])) != 0) {
 		x.nArg = argc - 1;
 		x.nUsed = 0;
-		x.apArg = argv + 1;
+		x.apArg = &argv[1];
 		sqlStrAccumInit(&str, db, 0, 0,
 				    db->aLimit[SQL_LIMIT_LENGTH]);
 		str.printfFlags = SQL_PRINTF_SQLFUNC;
 		sqlXPrintf(&str, zFormat, &x);
 		n = str.nChar;
-		sql_result_text(context, sqlStrAccumFinish(&str), n,
-				    SQL_DYNAMIC);
+		if (sqlVdbeMemSetStr(val, sqlStrAccumFinish(&str), n,
+				     1, SQL_DYNAMIC) != 0)
+			return -1;
 	}
+	return 0;
 }
 
 /*
@@ -390,9 +483,20 @@ printfFunc(sql_context * context, int argc, sql_value ** argv)
  *
  * If p2 is negative, return the p2 characters preceding p1.
  */
-static void
-substrFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_substr(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct sql *db = sql_get();
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	const unsigned char *z;
 	const unsigned char *z2;
 	int len;
@@ -401,36 +505,33 @@ substrFunc(sql_context * context, int argc, sql_value ** argv)
 	int negP2 = 0;
 
 	assert(argc == 3 || argc == 2);
-	if (sql_value_is_null(argv[1])
-	    || (argc == 3 && sql_value_is_null(argv[2]))
-	    ) {
-		return;
-	}
-	p0type = sql_value_type(argv[0]);
-	p1 = sql_value_int(argv[1]);
+	if (sql_value_is_null(&argv[1]) ||
+	    (argc == 3 && sql_value_is_null(&argv[2])))
+		return 0;
+	p0type = sql_value_type(&argv[0]);
+	p1 = sql_value_int(&argv[1]);
 	if (p0type == MP_BIN) {
-		len = sql_value_bytes(argv[0]);
-		z = sql_value_blob(argv[0]);
+		len = sql_value_bytes(&argv[0]);
+		z = sql_value_blob(&argv[0]);
 		if (z == 0)
-			return;
-		assert(len == sql_value_bytes(argv[0]));
+			return 0;
+		assert(len == sql_value_bytes(&argv[0]));
 	} else {
-		z = sql_value_text(argv[0]);
+		z = sql_value_text(&argv[0]);
 		if (z == 0)
-			return;
+			return 0;
 		len = 0;
 		if (p1 < 0)
-			len = sql_utf8_char_count(z, sql_value_bytes(argv[0]));
+			len = sql_utf8_char_count(z, sql_value_bytes(&argv[0]));
 	}
 	if (argc == 3) {
-		p2 = sql_value_int(argv[2]);
+		p2 = sql_value_int(&argv[2]);
 		if (p2 < 0) {
 			p2 = -p2;
 			negP2 = 1;
 		}
 	} else {
-		p2 = sql_context_db_handle(context)->
-		    aLimit[SQL_LIMIT_LENGTH];
+		p2 = db->aLimit[SQL_LIMIT_LENGTH];
 	}
 	if (p1 < 0) {
 		p1 += len;
@@ -459,7 +560,7 @@ substrFunc(sql_context * context, int argc, sql_value ** argv)
 		 * used because '\0' is not supposed to be
 		 * end-of-string symbol.
 		 */
-		int byte_size = sql_value_bytes(argv[0]);
+		int byte_size = sql_value_bytes(&argv[0]);
 		int n_chars = sql_utf8_char_count(z, byte_size);
 		int cnt = 0;
 		int i = 0;
@@ -475,38 +576,51 @@ substrFunc(sql_context * context, int argc, sql_value ** argv)
 			cnt++;
 		}
 		z2 += i;
-		sql_result_text64(context, (char *)z, z2 - z,
-				      SQL_TRANSIENT);
+		if (sqlVdbeMemSetStr(val, (char *)z, z2 - z, 1,
+				     SQL_TRANSIENT) != 0)
+			return -1;
 	} else {
 		if (p1 + p2 > len) {
 			p2 = len - p1;
 			if (p2 < 0)
 				p2 = 0;
 		}
-		sql_result_blob64(context, (char *)&z[p1], (u64) p2,
-				      SQL_TRANSIENT);
+		if (sqlVdbeMemSetStr(val, (char *)&z[p1], (u64) p2, 0,
+				     SQL_TRANSIENT) != 0)
+			return -1;
 	}
+	return 0;
 }
 
 /*
  * Implementation of the round() function
  */
-static void
-roundFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_round(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	int n = 0;
 	double r;
 	assert(argc == 1 || argc == 2);
 	if (argc == 2) {
-		if (sql_value_is_null(argv[1]))
-			return;
-		n = sql_value_int(argv[1]);
+		if (sql_value_is_null(&argv[1]))
+			return 0;
+		n = sql_value_int(&argv[1]);
 		if (n < 0)
 			n = 0;
 	}
-	if (sql_value_is_null(argv[0]))
-		return;
-	r = sql_value_double(argv[0]);
+	if (sql_value_is_null(&argv[0]))
+		return 0;
+	r = sql_value_double(&argv[0]);
 	/* If Y==0 and X will fit in a 64-bit int,
 	 * handle the rounding directly,
 	 * otherwise use printf.
@@ -519,7 +633,8 @@ roundFunc(sql_context * context, int argc, sql_value ** argv)
 		const char *rounded_value = tt_sprintf("%.*f", n, r);
 		sqlAtoF(rounded_value, &r, sqlStrlen30(rounded_value));
 	}
-	sql_result_double(context, r);
+	sqlVdbeMemSetDouble(val, r);
+	return 0;
 }
 
 /*
@@ -528,24 +643,15 @@ roundFunc(sql_context * context, int argc, sql_value ** argv)
  * maximum string or blob length, then raise an error and return
  * NULL.
  */
-static void *
-contextMalloc(sql_context * context, i64 nByte)
+static char *
+sql_blob_alloc(int64_t size)
 {
-	char *z;
-	sql *db = sql_context_db_handle(context);
-	assert(nByte > 0);
-	testcase(nByte == db->aLimit[SQL_LIMIT_LENGTH]);
-	testcase(nByte == db->aLimit[SQL_LIMIT_LENGTH] + 1);
-	if (nByte > db->aLimit[SQL_LIMIT_LENGTH]) {
+	struct sql *db = sql_get();
+	if (size > db->aLimit[SQL_LIMIT_LENGTH]) {
 		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob too big");
-		context->is_aborted = true;
-		z = 0;
-	} else {
-		z = sqlMalloc(nByte);
-		if (z == NULL)
-			context->is_aborted = true;
+		return NULL;
 	}
-	return z;
+	return sql_malloc64(size);
 }
 
 /*
@@ -553,29 +659,38 @@ contextMalloc(sql_context * context, i64 nByte)
  */
 
 #define ICU_CASE_CONVERT(case_type)                                            \
-static void                                                                    \
-case_type##ICUFunc(sql_context *context, int argc, sql_value **argv)   \
+static int                                                                     \
+sql_builtin_ICU##case_type(struct func *func, struct port *args,               \
+			   struct port *ret)                                   \
 {                                                                              \
+	(void) func;                                                           \
+	uint32_t argc;                                                         \
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);        \
+	if (argv == NULL)                                                      \
+		return -1;                                                     \
+	struct sql_context *ctx = (struct sql_context *)port_get_context(args);\
+	assert(ctx != NULL);                                                   \
+	struct Mem *val = vdbemem_alloc_on_region(1);                           \
+	if (val == NULL)                                                       \
+		return -1;                                                     \
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);            \
 	char *z1;                                                              \
 	const char *z2;                                                        \
 	int n;                                                                 \
-	UNUSED_PARAMETER(argc);                                                \
-	z2 = (char *)sql_value_text(argv[0]);                              \
-	n = sql_value_bytes(argv[0]);                                      \
+	z2 = (char *)sql_value_text(&argv[0]);                                 \
+	n = sql_value_bytes(&argv[0]);                                         \
 	/*                                                                     \
 	 * Verify that the call to _bytes()                                    \
 	 * does not invalidate the _text() pointer.                            \
 	 */                                                                    \
-	assert(z2 == (char *)sql_value_text(argv[0]));                     \
+	assert(z2 == (char *)sql_value_text(&argv[0]));                        \
 	if (!z2)                                                               \
-		return;                                                        \
-	z1 = contextMalloc(context, ((i64) n) + 1);                            \
-	if (z1 == NULL) {                                                      \
-		context->is_aborted = true;                                    \
-		return;                                                        \
-	}                                                                      \
+		return 0;                                                      \
+	z1 = sql_blob_alloc(((i64) n) + 1);                                    \
+	if (z1 == NULL)                                                        \
+		return -1;                                                     \
 	UErrorCode status = U_ZERO_ERROR;                                      \
-	struct coll *coll = sqlGetFuncCollSeq(context);                    \
+	struct coll *coll = sqlGetFuncCollSeq(ctx);                            \
 	const char *locale = NULL;                                             \
 	if (coll != NULL && coll->type == COLL_TYPE_ICU) {                     \
 		locale = ucol_getLocaleByType(coll->collator,                  \
@@ -586,17 +701,15 @@ case_type##ICUFunc(sql_context *context, int argc, sql_value **argv)   \
 	int len = ucasemap_utf8To##case_type(case_map, z1, n, z2, n, &status); \
 	if (len > n) {                                                         \
 		status = U_ZERO_ERROR;                                         \
-		sql_free(z1);                                              \
-		z1 = contextMalloc(context, ((i64) len) + 1);                  \
-		if (z1 == NULL) {                                              \
-			context->is_aborted = true;                            \
-			return;                                                \
-		}                                                              \
+		sql_free(z1);                                                  \
+		z1 = sql_blob_alloc(((i64) len) + 1);                          \
+		if (z1 == NULL)                                                \
+			return -1;                                             \
 		ucasemap_utf8To##case_type(case_map, z1, len, z2, n, &status); \
 	}                                                                      \
 	ucasemap_close(case_map);                                              \
-	sql_result_text(context, z1, len, sql_free);                   \
-}                                                                              \
+	return sqlVdbeMemSetStr(val, z1, len, 1, SQL_DYNAMIC) != 0 ? -1 : 0;   \
+}
 
 ICU_CASE_CONVERT(Lower);
 ICU_CASE_CONVERT(Upper);
@@ -607,21 +720,27 @@ ICU_CASE_CONVERT(Upper);
  * as VDBE code so that unused argument values do not have to be
  * computed.
  * However, we still need some kind of function implementation for
- * this routines in the function table. The noopFunc macro
- * provides this. noopFunc will never be called so it doesn't
- * matter what the implementation is. We might as well use the
- * "version()" function as a substitute.
+ * this routines in the function table. The sql_builtin_noop macro
+ * provides this. sql_builtin_noop will never be called so it
+ * doesn't matter what the implementation is. We might as well
+ * use the "version()" function as a substitute.
  */
-#define noopFunc sql_func_version /* Substitute function - never called */
+#define sql_builtin_noop sql_builtin_version
 
 /*
  * Implementation of random().  Return a random integer.
  */
-static void
-randomFunc(sql_context * context, int NotUsed, sql_value ** NotUsed2)
+static int
+sql_builtin_random(struct func *func, struct port *args, struct port *ret)
 {
-	sql_int64 r;
-	UNUSED_PARAMETER2(NotUsed, NotUsed2);
+	(void) func;
+	(void) args;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
+	int64_t r;
 	sql_randomness(sizeof(r), &r);
 	if (r < 0) {
 		/* We need to prevent a random number of 0x8000000000000000
@@ -634,28 +753,36 @@ randomFunc(sql_context * context, int NotUsed, sql_value ** NotUsed2)
 		 */
 		r = -(r & LARGEST_INT64);
 	}
-	sql_result_int64(context, r);
+	sqlVdbeMemSetInt64(val, r);
+	return 0;
 }
 
 /*
- * Implementation of randomblob(N).  Return a random blob
+ * Implementation of sql_builtin_random_blob(N). Return a random blob
  * that is N bytes long.
  */
-static void
-randomBlob(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_random_blob(struct func *func, struct port *args, struct port *ret)
 {
-	int n;
-	unsigned char *p;
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	assert(argc == 1);
-	UNUSED_PARAMETER(argc);
-	n = sql_value_int(argv[0]);
+	int n = sql_value_int(&argv[0]);
 	if (n < 1)
-		return;
-	p = contextMalloc(context, n);
-	if (p) {
-		sql_randomness(n, p);
-		sql_result_blob(context, (char *)p, n, sql_free);
-	}
+		return 0;
+	char *p = sql_blob_alloc(n);
+	if (p == NULL)
+		return -1;
+	sql_randomness(n, p);
+	return sqlVdbeMemSetStr(val, p, n, 0, SQL_DYNAMIC) != 0 ? -1 : 0;
 }
 
 #define Utf8Read(s, e) \
@@ -880,49 +1007,57 @@ sql_strlike_ci(const char *zPattern, const char *zStr, unsigned int esc)
  * Both arguments (A and B) must be of type TEXT. If one arguments
  * is NULL then result is NULL as well.
  */
-static void
-likeFunc(sql_context *context, int argc, sql_value **argv)
+static int
+sql_builtin_like(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	u32 escape = SQL_END_OF_STRING;
 	int nPat;
-	sql *db = sql_context_db_handle(context);
+	struct sql *db = sql_get();
 	bool is_like_ci =
 		(current_session()->sql_flags & LIKE_CASE_SENS_FLAG) == 0;
-	int rhs_type = sql_value_type(argv[0]);
-	int lhs_type = sql_value_type(argv[1]);
+	int rhs_type = sql_value_type(&argv[0]);
+	int lhs_type = sql_value_type(&argv[1]);
 
 	if (lhs_type != MP_STR || rhs_type != MP_STR) {
 		if (lhs_type == MP_NIL || rhs_type == MP_NIL)
-			return;
+			return 0;
 		char *inconsistent_type = rhs_type != MP_STR ?
-					  mem_type_to_str(argv[0]) :
-					  mem_type_to_str(argv[1]);
+					  mem_type_to_str(&argv[0]) :
+					  mem_type_to_str(&argv[1]);
 		diag_set(ClientError, ER_INCONSISTENT_TYPES, "TEXT",
 			 inconsistent_type);
-		context->is_aborted = true;
-		return;
+		return -1;
 	}
-	const char *zB = (const char *) sql_value_text(argv[0]);
-	const char *zA = (const char *) sql_value_text(argv[1]);
-	const char *zB_end = zB + sql_value_bytes(argv[0]);
-	const char *zA_end = zA + sql_value_bytes(argv[1]);
+	const char *zB = (const char *) sql_value_text(&argv[0]);
+	const char *zA = (const char *) sql_value_text(&argv[1]);
+	const char *zB_end = zB + sql_value_bytes(&argv[0]);
+	const char *zA_end = zA + sql_value_bytes(&argv[1]);
 
 	/*
 	 * Limit the length of the LIKE pattern to avoid problems
 	 * of deep recursion and N*N behavior in
 	 * sql_utf8_pattern_compare().
 	 */
-	nPat = sql_value_bytes(argv[0]);
+	nPat = sql_value_bytes(&argv[0]);
 	testcase(nPat == db->aLimit[SQL_LIMIT_LIKE_PATTERN_LENGTH]);
 	testcase(nPat == db->aLimit[SQL_LIMIT_LIKE_PATTERN_LENGTH] + 1);
 	if (nPat > db->aLimit[SQL_LIMIT_LIKE_PATTERN_LENGTH]) {
 		diag_set(ClientError, ER_SQL_EXECUTE, "LIKE pattern is too "\
 			 "complex");
-		context->is_aborted = true;
-		return;
+		return -1;
 	}
 	/* Encoding did not change */
-	assert(zB == (const char *) sql_value_text(argv[0]));
+	assert(zB == (const char *) sql_value_text(&argv[0]));
 
 	if (argc == 3) {
 		/*
@@ -930,29 +1065,28 @@ likeFunc(sql_context *context, int argc, sql_value **argv)
 		 * single UTF-8 character. Otherwise, return an
 		 * error.
 		 */
-		const unsigned char *zEsc = sql_value_text(argv[2]);
+		const unsigned char *zEsc = sql_value_text(&argv[2]);
 		if (zEsc == 0)
-			return;
-		if (sql_utf8_char_count(zEsc, sql_value_bytes(argv[2])) != 1) {
+			return 0;
+		if (sql_utf8_char_count(zEsc, sql_value_bytes(&argv[2])) != 1) {
 			diag_set(ClientError, ER_SQL_EXECUTE, "ESCAPE "\
 				 "expression must be a single character");
-			context->is_aborted = true;
-			return;
+			return -1;
 		}
 		escape = sqlUtf8Read(&zEsc);
 	}
 	if (!zA || !zB)
-		return;
+		return 0;
 	int res;
 	res = sql_utf8_pattern_compare(zB, zA, zB_end, zA_end,
 				       is_like_ci, escape);
 	if (res == INVALID_PATTERN) {
 		diag_set(ClientError, ER_SQL_EXECUTE, "LIKE pattern can only "\
 			 "contain UTF-8 characters");
-		context->is_aborted = true;
-		return;
+		return -1;
 	}
-	sql_result_bool(context, res == MATCH);
+	mem_set_bool(val, res == MATCH);
+	return 0;
 }
 
 /*
@@ -960,14 +1094,26 @@ likeFunc(sql_context *context, int argc, sql_value **argv)
  * argument if the arguments are different.  The result is NULL if the
  * arguments are equal to each other.
  */
-static void
-nullifFunc(sql_context * context, int NotUsed, sql_value ** argv)
+static int
+sql_builtin_nullif(struct func *func, struct port *args, struct port *ret)
 {
-	struct coll *pColl = sqlGetFuncCollSeq(context);
-	UNUSED_PARAMETER(NotUsed);
-	if (sqlMemCompare(argv[0], argv[1], pColl) != 0) {
-		sql_result_value(context, argv[0]);
-	}
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
+	struct coll *pColl = sqlGetFuncCollSeq(ctx);
+	if (sqlMemCompare(&argv[0], &argv[1], pColl) != 0 &&
+	    sqlVdbeMemCopy(val, &argv[0]) != 0)
+		return -1;
+	return 0;
 }
 
 /**
@@ -978,12 +1124,17 @@ nullifFunc(sql_context * context, int NotUsed, sql_value ** argv)
  * @param unused1 Unused.
  * @param unused2 Unused.
  */
-static void
-sql_func_version(struct sql_context *context,
-		 MAYBE_UNUSED int unused1,
-		 MAYBE_UNUSED sql_value **unused2)
+static int
+sql_builtin_version(struct func *func, struct port *args, struct port *ret)
 {
-	sql_result_text(context, tarantool_version(), -1, SQL_STATIC);
+	(void) func;
+	(void) args;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+	return sqlVdbeMemSetStr(val, tarantool_version(), -1, 1,
+				SQL_STATIC) != 0 ? -1 : 0;
 }
 
 /* Array for converting from half-bytes (nybbles) into ASCII hex
@@ -1001,108 +1152,126 @@ static const char hexdigits[] = {
  * "NULL".  Otherwise, the argument is enclosed in single quotes with
  * single-quote escapes.
  */
-static void
-quoteFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_quote(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	assert(argc == 1);
-	UNUSED_PARAMETER(argc);
-	switch (sql_value_type(argv[0])) {
+	switch (sql_value_type(&argv[0])) {
 	case MP_DOUBLE:{
 			double r1, r2;
 			char zBuf[50];
-			r1 = sql_value_double(argv[0]);
+			r1 = sql_value_double(&argv[0]);
 			sql_snprintf(sizeof(zBuf), zBuf, "%!.15g", r1);
 			sqlAtoF(zBuf, &r2, 20);
 			if (r1 != r2) {
 				sql_snprintf(sizeof(zBuf), zBuf, "%!.20e",
 						 r1);
 			}
-			sql_result_text(context, zBuf, -1,
-					    SQL_TRANSIENT);
+			if (sqlVdbeMemSetStr(val, zBuf, -1, 1,
+					     SQL_TRANSIENT) != 0)
+				return -1;
 			break;
 		}
 	case MP_INT:{
-			sql_result_value(context, argv[0]);
+			if (sqlVdbeMemCopy(val, &argv[0]) != 0)
+				return -1;
 			break;
 		}
 	case MP_BIN:{
-			char *zText = 0;
-			char const *zBlob = sql_value_blob(argv[0]);
-			int nBlob = sql_value_bytes(argv[0]);
-			assert(zBlob == sql_value_blob(argv[0]));	/* No encoding change */
-			zText =
-			    (char *)contextMalloc(context,
-						  (2 * (i64) nBlob) + 4);
-			if (zText) {
-				int i;
-				for (i = 0; i < nBlob; i++) {
-					zText[(i * 2) + 2] =
-					    hexdigits[(zBlob[i] >> 4) & 0x0F];
-					zText[(i * 2) + 3] =
-					    hexdigits[(zBlob[i]) & 0x0F];
-				}
-				zText[(nBlob * 2) + 2] = '\'';
-				zText[(nBlob * 2) + 3] = '\0';
-				zText[0] = 'X';
-				zText[1] = '\'';
-				sql_result_text(context, zText, -1,
-						    SQL_TRANSIENT);
-				sql_free(zText);
+			char const *zBlob = sql_value_blob(&argv[0]);
+			int nBlob = sql_value_bytes(&argv[0]);
+			assert(zBlob == sql_value_blob(&argv[0]));	/* No encoding change */
+			char *z = sql_blob_alloc((2 * (i64) nBlob) + 4);
+			if (z == NULL)
+				return -1;
+			for (int i = 0; i < nBlob; i++) {
+				z[(i * 2) + 2] =
+					hexdigits[(zBlob[i] >> 4) & 0x0F];
+				z[(i * 2) + 3] =
+					hexdigits[(zBlob[i]) & 0x0F];
 			}
+			z[(nBlob * 2) + 2] = '\'';
+			z[(nBlob * 2) + 3] = '\0';
+			z[0] = 'X';
+			z[1] = '\'';
+			if (sqlVdbeMemSetStr(val, z, -1, 1, SQL_DYNAMIC) != 0)
+				return -1;
 			break;
 		}
 	case MP_STR:{
 			int i, j;
 			u64 n;
-			const unsigned char *zArg = sql_value_text(argv[0]);
-			char *z;
-
+			const unsigned char *zArg = sql_value_text(&argv[0]);
 			if (zArg == 0)
-				return;
+				return 0;
 			for (i = 0, n = 0; zArg[i]; i++) {
 				if (zArg[i] == '\'')
 					n++;
 			}
-			z = contextMalloc(context, ((i64) i) + ((i64) n) + 3);
-			if (z) {
-				z[0] = '\'';
-				for (i = 0, j = 1; zArg[i]; i++) {
-					z[j++] = zArg[i];
-					if (zArg[i] == '\'') {
-						z[j++] = '\'';
-					}
+			char *z = sql_blob_alloc(((i64) i) + ((i64) n) + 3);
+			if (z == NULL)
+				return -1;
+			z[0] = '\'';
+			for (i = 0, j = 1; zArg[i]; i++) {
+				z[j++] = zArg[i];
+				if (zArg[i] == '\'') {
+					z[j++] = '\'';
 				}
-				z[j++] = '\'';
-				z[j] = 0;
-				sql_result_text(context, z, j,
-						    sql_free);
 			}
+			z[j++] = '\'';
+			z[j] = 0;
+			if (sqlVdbeMemSetStr(val, z, j, 1, SQL_DYNAMIC) != 0)
+				return -1;
 			break;
 		}
 	case MP_BOOL: {
-		sql_result_text(context, sql_value_boolean(argv[0]) ?
-				"true" : "false", -1, SQL_TRANSIENT);
+		if (sqlVdbeMemSetStr(val, sql_value_boolean(&argv[0]) ?
+				     "true" : "false", -1, 1, SQL_STATIC) != 0)
+			return -1;
 		break;
 	}
 	default:{
-			assert(sql_value_is_null(argv[0]));
-			sql_result_text(context, "NULL", 4, SQL_STATIC);
+			assert(sql_value_is_null(&argv[0]));
+			if (sqlVdbeMemSetStr(val, "NULL", 4, 1,
+					     SQL_STATIC) != 0)
+				return -1;
 			break;
 		}
 	}
+	return 0;
 }
 
 /*
  * The unicode() function.  Return the integer unicode code-point value
  * for the first character of the input string.
  */
-static void
-unicodeFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_unicode(struct func *func, struct port *args, struct port *ret)
 {
-	const unsigned char *z = sql_value_text(argv[0]);
-	(void)argc;
-	if (z && z[0])
-		sql_result_int(context, sqlUtf8Read(&z));
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
+	const unsigned char *z = sql_value_text(&argv[0]);
+	if (z != NULL && z[0] != '\0')
+		sqlVdbeMemSetInt64(val, sqlUtf8Read(&z));
+	return 0;
 }
 
 /*
@@ -1110,20 +1279,27 @@ unicodeFunc(sql_context * context, int argc, sql_value ** argv)
  * an integer.  It constructs a string where each character of the string
  * is the unicode character for the corresponding integer argument.
  */
-static void
-charFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_char(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	unsigned char *z, *zOut;
-	int i;
 	zOut = z = sql_malloc64(argc * 4 + 1);
-	if (z == NULL) {
-		context->is_aborted = true;
-		return;
-	}
-	for (i = 0; i < argc; i++) {
+	if (z == NULL)
+		return -1;
+	for (uint32_t i = 0; i < argc; i++) {
 		sql_int64 x;
 		unsigned c;
-		x = sql_value_int64(argv[i]);
+		x = sql_value_int64(&argv[i]);
 		if (x < 0 || x > 0x10ffff)
 			x = 0xfffd;
 		c = (unsigned)(x & 0x1fffff);
@@ -1143,52 +1319,79 @@ charFunc(sql_context * context, int argc, sql_value ** argv)
 			*zOut++ = 0x80 + (u8) (c & 0x3F);
 		}
 	}
-	sql_result_text64(context, (char *)z, zOut - z, sql_free);
+	if (zOut - z > sql_get()->aLimit[SQL_LIMIT_LENGTH]) {
+		sql_free(z);
+		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob too big");
+		return -1;
+	}
+	return sqlVdbeMemSetStr(val, (char *)z, zOut - z, 1,
+				SQL_DYNAMIC) != 0 ? -1 : 0;
 }
 
 /*
  * The hex() function.  Interpret the argument as a blob.  Return
  * a hexadecimal rendering as text.
  */
-static void
-hexFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_hex(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	assert(argc == 1);
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	int i, n;
 	const unsigned char *pBlob;
 	char *zHex, *z;
-	assert(argc == 1);
-	UNUSED_PARAMETER(argc);
-	pBlob = sql_value_blob(argv[0]);
-	n = sql_value_bytes(argv[0]);
-	assert(pBlob == sql_value_blob(argv[0]));	/* No encoding change */
-	z = zHex = contextMalloc(context, ((i64) n) * 2 + 1);
-	if (zHex) {
-		for (i = 0; i < n; i++, pBlob++) {
-			unsigned char c = *pBlob;
-			*(z++) = hexdigits[(c >> 4) & 0xf];
-			*(z++) = hexdigits[c & 0xf];
-		}
-		*z = 0;
-		sql_result_text(context, zHex, n * 2, sql_free);
+	pBlob = sql_value_blob(&argv[0]);
+	n = sql_value_bytes(&argv[0]);
+	assert(pBlob == sql_value_blob(&argv[0]));	/* No encoding change */
+	z = zHex = sql_blob_alloc(((i64) n) * 2 + 1);
+	if (z == NULL)
+		return -1;
+	for (i = 0; i < n; i++, pBlob++) {
+		unsigned char c = *pBlob;
+		*(z++) = hexdigits[(c >> 4) & 0xf];
+		*(z++) = hexdigits[c & 0xf];
 	}
+	*z = 0;
+	return sqlVdbeMemSetStr(val, zHex, n * 2, 1, SQL_DYNAMIC) != 0 ? -1 : 0;
 }
 
 /*
  * The zeroblob(N) function returns a zero-filled blob of size N bytes.
  */
-static void
-zeroblobFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_zeroblob(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	i64 n;
 	assert(argc == 1);
 	UNUSED_PARAMETER(argc);
-	n = sql_value_int64(argv[0]);
+	n = sql_value_int64(&argv[0]);
 	if (n < 0)
 		n = 0;
-	if (sql_result_zeroblob64(context, n) != 0) {
+	if (n > sql_get()->aLimit[SQL_LIMIT_LENGTH]) {
 		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob too big");
-		context->is_aborted = true;
+		return -1;
 	}
+	sqlVdbeMemSetZeroBlob(val, n);
+	return 0;
 }
 
 /*
@@ -1197,9 +1400,19 @@ zeroblobFunc(sql_context * context, int argc, sql_value ** argv)
  * from A by replacing every occurrence of B with C.  The match
  * must be exact.  Collating sequences are not used.
  */
-static void
-replaceFunc(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_replace(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+
 	const unsigned char *zStr;	/* The input string A */
 	const unsigned char *zPattern;	/* The pattern string B */
 	const unsigned char *zRep;	/* The replacement string C */
@@ -1213,35 +1426,30 @@ replaceFunc(sql_context * context, int argc, sql_value ** argv)
 
 	assert(argc == 3);
 	UNUSED_PARAMETER(argc);
-	zStr = sql_value_text(argv[0]);
+	zStr = sql_value_text(&argv[0]);
 	if (zStr == 0)
-		return;
-	nStr = sql_value_bytes(argv[0]);
-	assert(zStr == sql_value_text(argv[0]));	/* No encoding change */
-	zPattern = sql_value_text(argv[1]);
-	if (zPattern == 0) {
-		assert(sql_value_is_null(argv[1])
-		       || sql_context_db_handle(context)->mallocFailed);
-		return;
-	}
-	nPattern = sql_value_bytes(argv[1]);
+		return 0;
+	nStr = sql_value_bytes(&argv[0]);
+	assert(zStr == sql_value_text(&argv[0]));
+	zPattern = sql_value_text(&argv[1]);
+	if (zPattern == 0)
+		return 0;
+	nPattern = sql_value_bytes(&argv[1]);
 	if (nPattern == 0) {
-		assert(! sql_value_is_null(argv[1]));
-		sql_result_value(context, argv[0]);
-		return;
+		assert(! sql_value_is_null(&argv[1]));
+		return sqlVdbeMemCopy(val, &argv[0]) != 0 ? -1 : 0;
 	}
-	assert(zPattern == sql_value_text(argv[1]));	/* No encoding change */
-	zRep = sql_value_text(argv[2]);
+	assert(zPattern == sql_value_text(&argv[1]));
+	zRep = sql_value_text(&argv[2]);
 	if (zRep == 0)
-		return;
-	nRep = sql_value_bytes(argv[2]);
-	assert(zRep == sql_value_text(argv[2]));
+		return 0;
+	nRep = sql_value_bytes(&argv[2]);
+	assert(zRep == sql_value_text(&argv[2]));
 	nOut = nStr + 1;
 	assert(nOut < SQL_MAX_LENGTH);
-	zOut = contextMalloc(context, (i64) nOut);
-	if (zOut == 0) {
-		return;
-	}
+	zOut = (unsigned char *)sql_blob_alloc(nOut);
+	if (zOut == NULL)
+		return -1;
 	loopLimit = nStr - nPattern;
 	for (i = j = 0; i <= loopLimit; i++) {
 		if (zStr[i] != zPattern[0]
@@ -1249,23 +1457,21 @@ replaceFunc(sql_context * context, int argc, sql_value ** argv)
 			zOut[j++] = zStr[i];
 		} else {
 			u8 *zOld;
-			sql *db = sql_context_db_handle(context);
+			struct sql *db = sql_get();
 			nOut += nRep - nPattern;
 			testcase(nOut - 1 == db->aLimit[SQL_LIMIT_LENGTH]);
 			testcase(nOut - 2 == db->aLimit[SQL_LIMIT_LENGTH]);
 			if (nOut - 1 > db->aLimit[SQL_LIMIT_LENGTH]) {
 				diag_set(ClientError, ER_SQL_EXECUTE, "string "\
 					 "or blob too big");
-				context->is_aborted = true;
 				sql_free(zOut);
-				return;
+				return -1;
 			}
 			zOld = zOut;
 			zOut = sql_realloc64(zOut, (int)nOut);
 			if (zOut == 0) {
-				context->is_aborted = true;
 				sql_free(zOld);
-				return;
+				return -1;
 			}
 			memcpy(&zOut[j], zRep, nRep);
 			j += nRep;
@@ -1277,23 +1483,25 @@ replaceFunc(sql_context * context, int argc, sql_value ** argv)
 	j += nStr - i;
 	assert(j <= nOut);
 	zOut[j] = 0;
-	sql_result_text(context, (char *)zOut, j, sql_free);
+	return sqlVdbeMemSetStr(val, (char *)zOut, j, 1,
+				SQL_DYNAMIC) != 0 ? -1 : 0;
 }
 
 /**
  * Remove characters included in @a trim_set from @a input_str
  * until encounter a character that doesn't belong to @a trim_set.
  * Remove from the side specified by @a flags.
- * @param context SQL context.
+ * @param ret SQL value to store result.
  * @param flags Trim specification: left, right or both.
  * @param trim_set The set of characters for trimming.
  * @param char_len Lengths of each UTF-8 character in @a trim_set.
  * @param char_cnt A number of UTF-8 characters in @a trim_set.
  * @param input_str Input string for trimming.
  * @param input_str_sz Input string size in bytes.
+ * @return 0 On success, -1 otherwise.
  */
-static void
-trim_procedure(struct sql_context *context, enum trim_side_mask flags,
+static int
+trim_procedure(struct Mem *val, enum trim_side_mask flags,
 	       const unsigned char *trim_set, const uint8_t *char_len,
 	       int char_cnt, const unsigned char *input_str, int input_str_sz)
 {
@@ -1332,8 +1540,8 @@ trim_procedure(struct sql_context *context, enum trim_side_mask flags,
 		}
 	}
 finish:
-	sql_result_text(context, (char *)input_str, input_str_sz,
-			SQL_TRANSIENT);
+	return sqlVdbeMemSetStr(val, (char *)input_str, input_str_sz, 1,
+				SQL_TRANSIENT) != 0 ? -1 : 0;
 }
 
 /**
@@ -1348,8 +1556,7 @@ finish:
  * @retval -1 Memory allocation error.
  */
 static int
-trim_prepare_char_len(struct sql_context *context,
-		      const unsigned char *trim_set, int trim_set_sz,
+trim_prepare_char_len(const unsigned char *trim_set, int trim_set_sz,
 		      uint8_t **char_len)
 {
 	/*
@@ -1364,7 +1571,7 @@ trim_prepare_char_len(struct sql_context *context,
 		return 0;
 	}
 
-	if ((*char_len = (uint8_t *)contextMalloc(context, char_cnt)) == NULL)
+	if ((*char_len = (uint8_t *)sql_blob_alloc(char_cnt)) == NULL)
 		return -1;
 
 	int i = 0, j = 0;
@@ -1385,20 +1592,29 @@ trim_prepare_char_len(struct sql_context *context,
  * Call trimming procedure with TRIM_BOTH as the flags and " " as
  * the trimming set.
  */
-static void
-trim_func_one_arg(struct sql_context *context, int argc, sql_value **argv)
+static int
+sql_builtin_trim_one_arg(struct func *func, struct port *args, struct port *ret)
 {
-	assert(argc == 1);
-	(void) argc;
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
 
+	assert(argc == 1);
 	const unsigned char *input_str;
-	if ((input_str = sql_value_text(argv[0])) == NULL)
-		return;
+	if ((input_str = sql_value_text(&argv[0])) == NULL)
+		return 0;
 
-	int input_str_sz = sql_value_bytes(argv[0]);
+	int input_str_sz = sql_value_bytes(&argv[0]);
 	uint8_t len_one = 1;
-	trim_procedure(context, TRIM_BOTH, (const unsigned char *) " ",
+	trim_procedure(val, TRIM_BOTH, (const unsigned char *) " ",
 		       &len_one, 1, input_str, input_str_sz);
+	return 0;
 }
 
 /**
@@ -1412,33 +1628,46 @@ trim_func_one_arg(struct sql_context *context, int argc, sql_value **argv)
  * If user has specified side keyword only, then call trimming
  * procedure with the specified side and " " as the trimming set.
  */
-static void
-trim_func_two_args(struct sql_context *context, int argc, sql_value **argv)
+static int
+sql_builtin_trim_two_args(struct func *func, struct port *args,
+			  struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
 	assert(argc == 2);
-	(void) argc;
 
 	const unsigned char *input_str, *trim_set;
-	if ((input_str = sql_value_text(argv[1])) == NULL)
-		return;
+	if ((input_str = sql_value_text(&argv[1])) == NULL)
+		return 0;
 
-	int input_str_sz = sql_value_bytes(argv[1]);
-	if (sql_value_type(argv[0]) == MP_INT) {
+	int input_str_sz = sql_value_bytes(&argv[1]);
+	if (sql_value_type(&argv[0]) == MP_INT) {
 		uint8_t len_one = 1;
-		trim_procedure(context, sql_value_int(argv[0]),
-			       (const unsigned char *) " ", &len_one, 1,
-			       input_str, input_str_sz);
-	} else if ((trim_set = sql_value_text(argv[0])) != NULL) {
-		int trim_set_sz = sql_value_bytes(argv[0]);
-		uint8_t *char_len;
-		int char_cnt = trim_prepare_char_len(context, trim_set,
-						     trim_set_sz, &char_len);
-		if (char_cnt == -1)
-			return;
-		trim_procedure(context, TRIM_BOTH, trim_set, char_len, char_cnt,
-			       input_str, input_str_sz);
+		if (trim_procedure(val, sql_value_int(&argv[0]),
+				   (const unsigned char *) " ", &len_one, 1,
+				   input_str, input_str_sz) != 0)
+			return -1;
+	} else if ((trim_set = sql_value_text(&argv[0])) != NULL) {
+		int trim_set_sz = sql_value_bytes(&argv[0]);
+		uint8_t *char_len = NULL;
+		int char_cnt =
+			trim_prepare_char_len(trim_set, trim_set_sz, &char_len);
+		if (char_cnt == -1 ||
+		    trim_procedure(val, TRIM_BOTH, trim_set, char_len,
+				   char_cnt, input_str, input_str_sz) != 0) {
+			sql_free(char_len);
+			return -1;
+		}
 		sql_free(char_len);
 	}
+	return 0;
 }
 
 /**
@@ -1448,28 +1677,37 @@ trim_func_two_args(struct sql_context *context, int argc, sql_value **argv)
  * If user has specified side keyword and <character_set>, then
  * call trimming procedure with that args.
  */
-static void
-trim_func_three_args(struct sql_context *context, int argc, sql_value **argv)
+static int
+sql_builtin_trim_three_args(struct func *func, struct port *args,
+			    struct port *ret)
 {
+	(void) func;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
 	assert(argc == 3);
-	(void) argc;
 
-	assert(sql_value_type(argv[0]) == MP_INT);
+	assert(sql_value_type(&argv[0]) == MP_INT);
 	const unsigned char *input_str, *trim_set;
-	if ((input_str = sql_value_text(argv[2])) == NULL ||
-	    (trim_set = sql_value_text(argv[1])) == NULL)
-		return;
+	if ((input_str = sql_value_text(&argv[2])) == NULL ||
+	    (trim_set = sql_value_text(&argv[1])) == NULL)
+		return 0;
 
-	int trim_set_sz = sql_value_bytes(argv[1]);
-	int input_str_sz = sql_value_bytes(argv[2]);
+	int trim_set_sz = sql_value_bytes(&argv[1]);
+	int input_str_sz = sql_value_bytes(&argv[2]);
 	uint8_t *char_len;
-	int char_cnt = trim_prepare_char_len(context, trim_set, trim_set_sz,
-					     &char_len);
+	int char_cnt = trim_prepare_char_len(trim_set, trim_set_sz, &char_len);
 	if (char_cnt == -1)
-		return;
-	trim_procedure(context, sql_value_int(argv[0]), trim_set, char_len,
-		       char_cnt, input_str, input_str_sz);
+		return -1;
+	int rc = trim_procedure(val, sql_value_int(&argv[0]), trim_set, char_len,
+				char_cnt, input_str, input_str_sz);
 	sql_free(char_len);
+	return rc;
 }
 
 /*
@@ -1495,72 +1733,101 @@ struct SumCtx {
  * value.  TOTAL never fails, but SUM might through an exception if
  * it overflows an integer.
  */
-static void
-sum_step(struct sql_context *context, int argc, sql_value **argv)
+static int
+sql_builtin_sum_step(struct func *func, struct port *args, struct port *ret)
 {
+	(void) func;
+	(void) ret;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
 	assert(argc == 1);
-	UNUSED_PARAMETER(argc);
-	struct SumCtx *p = sql_aggregate_context(context, sizeof(*p));
-	int type = sql_value_type(argv[0]);
+	struct SumCtx *p = sql_aggregate_context(ctx, sizeof(*p));
+	int type = sql_value_type(&argv[0]);
 	if (type == MP_NIL || p == NULL)
-		return;
+		return 0;
 	if (type != MP_DOUBLE && type != MP_INT) {
-		if (mem_apply_numeric_type(argv[0]) != 0) {
+		if (mem_apply_numeric_type(&argv[0]) != 0) {
 			diag_set(ClientError, ER_SQL_TYPE_MISMATCH,
-				 sql_value_text(argv[0]), "number");
-			context->is_aborted = true;
-			return;
+				 sql_value_text(&argv[0]), "number");
+			return -1;
 		}
-		type = sql_value_type(argv[0]);
+		type = sql_value_type(&argv[0]);
 	}
 	p->cnt++;
 	if (type == MP_INT) {
-		int64_t v = sql_value_int64(argv[0]);
+		int64_t v = sql_value_int64(&argv[0]);
 		p->rSum += v;
 		if ((p->approx | p->overflow) == 0 &&
 		    sqlAddInt64(&p->iSum, v) != 0) {
 			p->overflow = 1;
 		}
 	} else {
-		p->rSum += sql_value_double(argv[0]);
+		p->rSum += sql_value_double(&argv[0]);
 		p->approx = 1;
 	}
+	return 0;
 }
 
-static void
-sumFinalize(sql_context * context)
+static int
+sql_builtin_sum_finalize(struct func *func, struct port *args, struct port *ret)
 {
-	SumCtx *p;
-	p = sql_aggregate_context(context, 0);
+	(void) func;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	struct SumCtx *p = sql_aggregate_context(ctx, 0);
 	if (p && p->cnt > 0) {
 		if (p->overflow) {
-			diag_set(ClientError, ER_SQL_EXECUTE, "integer "\
-				 "overflow");
-			context->is_aborted = true;
+			diag_set(ClientError, ER_SQL_EXECUTE,
+				 "integer overflow");
+			return -1;
 		} else if (p->approx) {
-			sql_result_double(context, p->rSum);
+			sqlVdbeMemSetDouble(val, p->rSum);
 		} else {
-			sql_result_int64(context, p->iSum);
+			sqlVdbeMemSetInt64(val, p->iSum);
 		}
 	}
+	return 0;
 }
 
-static void
-avgFinalize(sql_context * context)
+static int
+sql_builtin_avg_finalize(struct func *func, struct port *args,
+			 struct port *ret)
 {
-	SumCtx *p;
-	p = sql_aggregate_context(context, 0);
-	if (p && p->cnt > 0) {
-		sql_result_double(context, p->rSum / (double)p->cnt);
-	}
+	(void) func;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	struct SumCtx *p = sql_aggregate_context(ctx, 0);
+	if (p && p->cnt > 0)
+		sqlVdbeMemSetDouble(val, p->rSum / (double)p->cnt);
+	return 0;
 }
 
-static void
-totalFinalize(sql_context * context)
+static int
+sql_builtin_total_finalize(struct func *func, struct port *args,
+			   struct port *ret)
 {
-	SumCtx *p;
-	p = sql_aggregate_context(context, 0);
-	sql_result_double(context, p ? p->rSum : (double)0);
+	(void) func;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	struct SumCtx *p = sql_aggregate_context(ctx, 0);
+	sqlVdbeMemSetDouble(val, p != NULL ? p->rSum : (double)0);
+	return 0;
 }
 
 /*
@@ -1575,45 +1842,67 @@ struct CountCtx {
 /*
  * Routines to implement the count() aggregate function.
  */
-static void
-countStep(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_count_step(struct func *func, struct port *args, struct port *ret)
 {
-	CountCtx *p;
-	p = sql_aggregate_context(context, sizeof(*p));
-	if ((argc == 0 || ! sql_value_is_null(argv[0])) && p) {
+	(void) func;
+	(void) ret;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	struct CountCtx *p = sql_aggregate_context(ctx, sizeof(*p));
+	if ((argc == 0 || ! sql_value_is_null(&argv[0])) && p)
 		p->n++;
-	}
+	return 0;
 }
 
-static void
-countFinalize(sql_context * context)
+static int
+sql_builtin_count_finalize(struct func *func, struct port *args,
+			   struct port *ret)
 {
-	CountCtx *p;
-	p = sql_aggregate_context(context, 0);
-	sql_result_int64(context, p ? p->n : 0);
+	(void) func;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	struct CountCtx *p = sql_aggregate_context(ctx, 0);
+	sqlVdbeMemSetInt64(val, p != NULL ? p->n : 0);
+	return 0;
 }
 
 /*
  * Routines to implement min() and max() aggregate functions.
  */
-static void
-minmaxStep(sql_context * context, int NotUsed, sql_value ** argv)
+static int
+sql_builtin_minmax_step(struct func *func, struct port *args, struct port *ret)
 {
-	Mem *pArg = (Mem *) argv[0];
+	(void) func;
+	(void) ret;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	Mem *pArg = (Mem *) &argv[0];
 	Mem *pBest;
-	UNUSED_PARAMETER(NotUsed);
 
-	pBest = (Mem *) sql_aggregate_context(context, sizeof(*pBest));
-	if (!pBest)
-		return;
+	pBest = (Mem *) sql_aggregate_context(ctx, sizeof(*pBest));
+	if (pBest == NULL)
+		return -1;
 
-	if (sql_value_is_null(argv[0])) {
+	if (sql_value_is_null(&argv[0])) {
 		if (pBest->flags)
-			sqlSkipAccumulatorLoad(context);
+			sqlSkipAccumulatorLoad(ctx);
 	} else if (pBest->flags) {
 		int max;
 		int cmp;
-		struct coll *pColl = sqlGetFuncCollSeq(context);
+		struct coll *pColl = sqlGetFuncCollSeq(ctx);
 		/* This step function is used for both the min() and max() aggregates,
 		 * the only difference between the two being that the sense of the
 		 * comparison is inverted. For the max() aggregate, the
@@ -1622,88 +1911,120 @@ minmaxStep(sql_context * context, int NotUsed, sql_value ** argv)
 		 * Therefore the next statement sets variable 'max' to 1 for the max()
 		 * aggregate, or 0 for min().
 		 */
-		max = sql_user_data(context) != 0;
+		max = sql_user_data(ctx) != 0;
 		cmp = sqlMemCompare(pBest, pArg, pColl);
 		if ((max && cmp < 0) || (!max && cmp > 0)) {
-			sqlVdbeMemCopy(pBest, pArg);
+			if (sqlVdbeMemCopy(pBest, pArg) != 0)
+				return -1;
 		} else {
-			sqlSkipAccumulatorLoad(context);
+			sqlSkipAccumulatorLoad(ctx);
 		}
 	} else {
-		pBest->db = sql_context_db_handle(context);
-		sqlVdbeMemCopy(pBest, pArg);
+		pBest->db = sql_get();
+		if (sqlVdbeMemCopy(pBest, pArg) != 0)
+			return -1;
 	}
+	return 0;
 }
 
-static void
-minMaxFinalize(sql_context * context)
+static int
+sql_builtin_minmax_finalize(struct func *func, struct port *args,
+			    struct port *ret)
 {
-	sql_value *pRes;
-	pRes = (sql_value *) sql_aggregate_context(context, 0);
-	if (pRes) {
-		if (pRes->flags) {
-			sql_result_value(context, pRes);
+	(void) func;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+	struct Mem *res =
+		(struct Mem *) sql_aggregate_context(ctx, 0);
+	if (res != NULL) {
+		if (res->flags != 0 &&
+		    sqlVdbeMemCopy(val, res) != 0) {
+			sqlVdbeMemRelease(res);
+			return -1;
 		}
-		sqlVdbeMemRelease(pRes);
+		sqlVdbeMemRelease(res);
 	}
+	return 0;
 }
 
 /*
  * group_concat(EXPR, ?SEPARATOR?)
  */
-static void
-groupConcatStep(sql_context * context, int argc, sql_value ** argv)
+static int
+sql_builtin_group_concat_step(struct func *func, struct port *args,
+			      struct port *ret)
 {
-	const char *zVal;
-	StrAccum *pAccum;
-	const char *zSep;
-	int nVal, nSep;
+	(void) func;
+	(void) ret;
+	uint32_t argc;
+	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
+	if (argv == NULL)
+		return -1;
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+
 	assert(argc == 1 || argc == 2);
-	if (sql_value_is_null(argv[0]))
-		return;
-	pAccum =
-	    (StrAccum *) sql_aggregate_context(context, sizeof(*pAccum));
-
-	if (pAccum) {
-		sql *db = sql_context_db_handle(context);
-		int firstTerm = pAccum->mxAlloc == 0;
-		pAccum->mxAlloc = db->aLimit[SQL_LIMIT_LENGTH];
-		if (!firstTerm) {
-			if (argc == 2) {
-				zSep = (char *)sql_value_text(argv[1]);
-				nSep = sql_value_bytes(argv[1]);
-			} else {
-				zSep = ",";
-				nSep = 1;
-			}
-			if (zSep)
-				sqlStrAccumAppend(pAccum, zSep, nSep);
+	if (sql_value_is_null(&argv[0]))
+		return 0;
+	struct StrAccum *p =
+		(struct StrAccum *) sql_aggregate_context(ctx, sizeof(*p));
+	if (p == NULL)
+		return -1;
+
+	struct sql *db = sql_get();
+	int firstTerm = p->mxAlloc == 0;
+	p->mxAlloc = db->aLimit[SQL_LIMIT_LENGTH];
+	if (!firstTerm) {
+		const char *zSep = NULL;
+		int nSep;
+		if (argc == 2) {
+			zSep = (char *)sql_value_text(&argv[1]);
+			nSep = sql_value_bytes(&argv[1]);
+		} else {
+			zSep = ",";
+			nSep = 1;
 		}
-		zVal = (char *)sql_value_text(argv[0]);
-		nVal = sql_value_bytes(argv[0]);
-		if (zVal)
-			sqlStrAccumAppend(pAccum, zVal, nVal);
+		if (zSep != NULL)
+			sqlStrAccumAppend(p, zSep, nSep);
 	}
+	const char *z = (char *)sql_value_text(&argv[0]);
+	int n = sql_value_bytes(&argv[0]);
+	if (z != NULL)
+		sqlStrAccumAppend(p, z, n);
+	return 0;
 }
 
-static void
-groupConcatFinalize(sql_context * context)
+static int
+sql_builtin_group_concat_finalize(struct func *func, struct port *args,
+				  struct port *ret)
 {
-	StrAccum *pAccum;
-	pAccum = sql_aggregate_context(context, 0);
-	if (pAccum) {
-		if (pAccum->accError == STRACCUM_TOOBIG) {
-			diag_set(ClientError, ER_SQL_EXECUTE, "string or blob "\
-				 "too big");
-			context->is_aborted = true;
-		} else if (pAccum->accError == STRACCUM_NOMEM) {
-			context->is_aborted = true;
-		} else {
-			sql_result_text(context,
-					    sqlStrAccumFinish(pAccum),
-					    pAccum->nChar, sql_free);
-		}
+	(void) func;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
+	assert(ctx != NULL);
+
+
+	struct StrAccum *p = sql_aggregate_context(ctx, 0);
+	if (p == NULL)
+		return 0;
+	if (p->accError == STRACCUM_TOOBIG) {
+		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob too big");
+		return -1;
+	} else if (p->accError == STRACCUM_NOMEM) {
+		return -1;
+	} else {
+		if (sqlVdbeMemSetStr(val, sqlStrAccumFinish(p), p->nChar,
+				     1, SQL_DYNAMIC) != 0)
+			return -1;
 	}
+	return 0;
 }
 
 int
@@ -1743,66 +2064,77 @@ sqlRegisterBuiltinFunctions(void)
 	 * For peak efficiency, put the most frequently used function last.
 	 */
 	static FuncDef aBuiltinFunc[] = {
-		FUNCTION_COLL(trim, 1, 3, 0, trim_func_one_arg),
-		FUNCTION_COLL(trim, 2, 3, 0, trim_func_two_args),
-		FUNCTION_COLL(trim, 3, 3, 0, trim_func_three_args),
-		FUNCTION(min, -1, 0, 1, minmaxFunc, FIELD_TYPE_SCALAR),
+		FUNCTION_COLL(trim, 1, 3, 0, sql_builtin_trim_one_arg),
+		FUNCTION_COLL(trim, 2, 3, 0, sql_builtin_trim_two_args),
+		FUNCTION_COLL(trim, 3, 3, 0, sql_builtin_trim_three_args),
+		FUNCTION(min, -1, 0, 1, sql_builtin_minmax, FIELD_TYPE_SCALAR),
 		FUNCTION(min, 0, 0, 1, 0, FIELD_TYPE_SCALAR),
-		AGGREGATE2(min, 1, 0, 1, minmaxStep, minMaxFinalize,
-			   SQL_FUNC_MINMAX, FIELD_TYPE_SCALAR),
-		FUNCTION(max, -1, 1, 1, minmaxFunc, FIELD_TYPE_SCALAR),
+		AGGREGATE2(min, 1, 0, 1, sql_builtin_minmax_step,
+			   sql_builtin_minmax_finalize, SQL_FUNC_MINMAX,
+			   FIELD_TYPE_SCALAR),
+		FUNCTION(max, -1, 1, 1, sql_builtin_minmax, FIELD_TYPE_SCALAR),
 		FUNCTION(max, 0, 1, 1, 0, FIELD_TYPE_SCALAR),
-		AGGREGATE2(max, 1, 1, 1, minmaxStep, minMaxFinalize,
-			   SQL_FUNC_MINMAX, FIELD_TYPE_SCALAR),
-		FUNCTION2(typeof, 1, 0, 0, typeofFunc, SQL_FUNC_TYPEOF,
+		AGGREGATE2(max, 1, 1, 1, sql_builtin_minmax_step,
+			   sql_builtin_minmax_finalize, SQL_FUNC_MINMAX,
+			   FIELD_TYPE_SCALAR),
+		FUNCTION2(typeof, 1, 0, 0, sql_builtin_typeof, SQL_FUNC_TYPEOF,
 			  FIELD_TYPE_STRING),
-		FUNCTION2(length, 1, 0, 0, lengthFunc, SQL_FUNC_LENGTH,
+		FUNCTION2(length, 1, 0, 0, sql_builtin_length, SQL_FUNC_LENGTH,
 			  FIELD_TYPE_INTEGER),
-		FUNCTION(position, 2, 0, 1, position_func, FIELD_TYPE_INTEGER),
-		FUNCTION(printf, -1, 0, 0, printfFunc, FIELD_TYPE_STRING),
-		FUNCTION(unicode, 1, 0, 0, unicodeFunc, FIELD_TYPE_STRING),
-		FUNCTION(char, -1, 0, 0, charFunc, FIELD_TYPE_STRING),
-		FUNCTION(abs, 1, 0, 0, absFunc, FIELD_TYPE_NUMBER),
-		FUNCTION(round, 1, 0, 0, roundFunc, FIELD_TYPE_INTEGER),
-		FUNCTION(round, 2, 0, 0, roundFunc, FIELD_TYPE_INTEGER),
-		FUNCTION_COLL(upper, 1, 0, 1, UpperICUFunc),
-		FUNCTION_COLL(lower, 1, 0, 1, LowerICUFunc),
-		FUNCTION(hex, 1, 0, 0, hexFunc, FIELD_TYPE_STRING),
-		FUNCTION2(ifnull, 2, 0, 0, noopFunc, SQL_FUNC_COALESCE,
+		FUNCTION(position, 2, 0, 1, sql_builtin_position,
+			 FIELD_TYPE_INTEGER),
+		FUNCTION(printf, -1, 0, 0, sql_builtin_printf,
+			 FIELD_TYPE_STRING),
+		FUNCTION(unicode, 1, 0, 0, sql_builtin_unicode,
+			 FIELD_TYPE_STRING),
+		FUNCTION(char, -1, 0, 0, sql_builtin_char, FIELD_TYPE_STRING),
+		FUNCTION(abs, 1, 0, 0, sql_builtin_abs, FIELD_TYPE_NUMBER),
+		FUNCTION(round, 1, 0, 0, sql_builtin_round, FIELD_TYPE_INTEGER),
+		FUNCTION(round, 2, 0, 0, sql_builtin_round, FIELD_TYPE_INTEGER),
+		FUNCTION_COLL(upper, 1, 0, 1, sql_builtin_ICUUpper),
+		FUNCTION_COLL(lower, 1, 0, 1, sql_builtin_ICULower),
+		FUNCTION(hex, 1, 0, 0, sql_builtin_hex, FIELD_TYPE_STRING),
+		FUNCTION2(ifnull, 2, 0, 0, sql_builtin_noop, SQL_FUNC_COALESCE,
 			  FIELD_TYPE_INTEGER),
-		VFUNCTION(random, 0, 0, 0, randomFunc, FIELD_TYPE_INTEGER),
-		VFUNCTION(randomblob, 1, 0, 0, randomBlob, FIELD_TYPE_SCALAR),
-		FUNCTION(nullif, 2, 0, 1, nullifFunc, FIELD_TYPE_SCALAR),
-		FUNCTION(version, 0, 0, 0, sql_func_version, FIELD_TYPE_STRING),
-		FUNCTION(quote, 1, 0, 0, quoteFunc, FIELD_TYPE_STRING),
-		VFUNCTION(row_count, 0, 0, 0, sql_row_count, FIELD_TYPE_INTEGER),
-		FUNCTION_COLL(replace, 3, 0, 0, replaceFunc),
-		FUNCTION(zeroblob, 1, 0, 0, zeroblobFunc, FIELD_TYPE_SCALAR),
-		FUNCTION_COLL(substr, 2, 0, 0, substrFunc),
-		FUNCTION_COLL(substr, 3, 0, 0, substrFunc),
-		AGGREGATE(sum, 1, 0, 0, sum_step, sumFinalize,
-			  FIELD_TYPE_NUMBER),
-		AGGREGATE(total, 1, 0, 0, sum_step, totalFinalize,
-			  FIELD_TYPE_NUMBER),
-		AGGREGATE(avg, 1, 0, 0, sum_step, avgFinalize,
-			  FIELD_TYPE_NUMBER),
-		AGGREGATE2(count, 0, 0, 0, countStep, countFinalize,
-			   SQL_FUNC_COUNT, FIELD_TYPE_INTEGER),
-		AGGREGATE(count, 1, 0, 0, countStep, countFinalize,
+		VFUNCTION(random, 0, 0, 0, sql_builtin_random,
+			  FIELD_TYPE_INTEGER),
+		VFUNCTION(randomblob, 1, 0, 0, sql_builtin_random_blob,
+			  FIELD_TYPE_SCALAR),
+		FUNCTION(nullif, 2, 0, 1, sql_builtin_nullif,
+			 FIELD_TYPE_SCALAR),
+		FUNCTION(version, 0, 0, 0, sql_builtin_version,
+			 FIELD_TYPE_STRING),
+		FUNCTION(quote, 1, 0, 0, sql_builtin_quote, FIELD_TYPE_STRING),
+		VFUNCTION(row_count, 0, 0, 0, sql_builtin_row_count,
+			  FIELD_TYPE_INTEGER),
+		FUNCTION_COLL(replace, 3, 0, 0, sql_builtin_replace),
+		FUNCTION(zeroblob, 1, 0, 0, sql_builtin_zeroblob,
+			 FIELD_TYPE_SCALAR),
+		FUNCTION_COLL(substr, 2, 0, 0, sql_builtin_substr),
+		FUNCTION_COLL(substr, 3, 0, 0, sql_builtin_substr),
+		AGGREGATE(sum, 1, 0, 0, sql_builtin_sum_step,
+			  sql_builtin_sum_finalize, FIELD_TYPE_NUMBER),
+		AGGREGATE(total, 1, 0, 0, sql_builtin_sum_step,
+			  sql_builtin_total_finalize, FIELD_TYPE_NUMBER),
+		AGGREGATE(avg, 1, 0, 0, sql_builtin_sum_step,
+			  sql_builtin_avg_finalize, FIELD_TYPE_NUMBER),
+		AGGREGATE2(count, 0, 0, 0, sql_builtin_count_step,
+			   sql_builtin_count_finalize, SQL_FUNC_COUNT,
+			   FIELD_TYPE_INTEGER),
+		AGGREGATE(count, 1, 0, 0, sql_builtin_count_step,
+			  sql_builtin_count_finalize, FIELD_TYPE_INTEGER),
+		AGGREGATE(group_concat, 1, 0, 0, sql_builtin_group_concat_step,
+			  sql_builtin_group_concat_finalize, FIELD_TYPE_STRING),
+		AGGREGATE(group_concat, 2, 0, 0, sql_builtin_group_concat_step,
+			  sql_builtin_group_concat_finalize, FIELD_TYPE_STRING),
+		FUNCTION2(like, 2, 1, 0, sql_builtin_like, SQL_FUNC_LIKE,
+			  FIELD_TYPE_INTEGER),
+		FUNCTION2(like, 3, 1, 0, sql_builtin_like, SQL_FUNC_LIKE,
 			  FIELD_TYPE_INTEGER),
-		AGGREGATE(group_concat, 1, 0, 0, groupConcatStep,
-			  groupConcatFinalize, FIELD_TYPE_STRING),
-		AGGREGATE(group_concat, 2, 0, 0, groupConcatStep,
-			  groupConcatFinalize, FIELD_TYPE_STRING),
-
-		LIKEFUNC(like, 2, 1, SQL_FUNC_LIKE,
-			 FIELD_TYPE_INTEGER),
-		LIKEFUNC(like, 3, 1, SQL_FUNC_LIKE,
-			 FIELD_TYPE_INTEGER),
 		FUNCTION(coalesce, 1, 0, 0, 0, FIELD_TYPE_SCALAR),
 		FUNCTION(coalesce, 0, 0, 0, 0, FIELD_TYPE_SCALAR),
-		FUNCTION2(coalesce, -1, 0, 0, noopFunc, SQL_FUNC_COALESCE,
-			  FIELD_TYPE_SCALAR),
+		FUNCTION2(coalesce, -1, 0, 0, sql_builtin_noop,
+			  SQL_FUNC_COALESCE, FIELD_TYPE_SCALAR),
 	};
 	sql_register_analyze_builtins();
 	sqlRegisterDateTimeFunctions();
diff --git a/src/box/sql/main.c b/src/box/sql/main.c
index 89d83d242..baaf7dcc9 100644
--- a/src/box/sql/main.c
+++ b/src/box/sql/main.c
@@ -38,6 +38,7 @@
 #include "sqlInt.h"
 #include "vdbeInt.h"
 #include "version.h"
+#include "box/port.h"
 #include "box/session.h"
 
 /*
@@ -151,12 +152,18 @@ sql_initialize(void)
 	return 0;
 }
 
-void
-sql_row_count(struct sql_context *context, MAYBE_UNUSED int unused1,
-	      MAYBE_UNUSED sql_value **unused2)
+int
+sql_builtin_row_count(struct func *func, struct port *args, struct port *ret)
 {
-	sql *db = sql_context_db_handle(context);
-	sql_result_int(context, db->nChange);
+	(void) func;
+	(void) args;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
+	struct sql *db = sql_get();
+	sqlVdbeMemSetInt64(val, db->nChange);
+	return 0;
 }
 
 /*
@@ -214,16 +221,15 @@ sqlRollbackAll(Vdbe * pVdbe)
  * is returned and the mallocFailed flag cleared.
  */
 int
-sqlCreateFunc(sql * db,
-		  const char *zFunctionName,
-		  enum field_type type,
-		  int nArg,
-		  int flags,
-		  void *pUserData,
-		  void (*xSFunc) (sql_context *, int, sql_value **),
-		  void (*xStep) (sql_context *, int, sql_value **),
-		  void (*xFinal) (sql_context *),
-		  FuncDestructor * pDestructor)
+sqlCreateFunc(struct sql * db, const char *zFunctionName, enum field_type type,
+	      int nArg, int flags, void *pUserData,
+	      int (*xSFunc) (struct func *func, struct port *args,
+			     struct port *ret),
+	      int (*xStep) (struct func *func, struct port *args,
+			    struct port *ret),
+	      int (*xFinal) (struct func *func, struct port *args,
+			     struct port *ret),
+	      struct FuncDestructor * pDestructor)
 {
 	FuncDef *p;
 	int extraFlags;
@@ -284,19 +290,17 @@ sqlCreateFunc(sql * db,
 	return 0;
 }
 
+
 int
-sql_create_function_v2(sql * db,
-			   const char *zFunc,
-			   enum field_type type,
-			   int nArg,
-			   int flags,
-			   void *p,
-			   void (*xSFunc) (sql_context *, int,
-					   sql_value **),
-			   void (*xStep) (sql_context *, int,
-					  sql_value **),
-			   void (*xFinal) (sql_context *),
-			   void (*xDestroy) (void *))
+sql_create_function_v2(struct sql * db, const char *zFunc, enum field_type type,
+		       int nArg, int flags, void *p,
+		       int (*xSFunc) (struct func *func, struct port *args,
+				      struct port *ret),
+		       int (*xStep) (struct func *func, struct port *args,
+				     struct port *ret),
+		       int (*xFinal) (struct func *func, struct port *args,
+				      struct port *ret),
+		       void (*xDestroy) (void *ctx))
 {
 	FuncDestructor *pArg = 0;
 
diff --git a/src/box/sql/printf.c b/src/box/sql/printf.c
index 98372f028..b9e6fbc31 100644
--- a/src/box/sql/printf.c
+++ b/src/box/sql/printf.c
@@ -10,6 +10,7 @@
  * sql.
  */
 #include "sqlInt.h"
+#include "vdbeInt.h"
 
 /*
  * Conversion types fall into various categories as defined by the
@@ -143,7 +144,7 @@ getIntArg(PrintfArguments * p)
 {
 	if (p->nArg <= p->nUsed)
 		return 0;
-	return sql_value_int64(p->apArg[p->nUsed++]);
+	return sql_value_int64(&p->apArg[p->nUsed++]);
 }
 
 static double
@@ -151,7 +152,7 @@ getDoubleArg(PrintfArguments * p)
 {
 	if (p->nArg <= p->nUsed)
 		return 0.0;
-	return sql_value_double(p->apArg[p->nUsed++]);
+	return sql_value_double(&p->apArg[p->nUsed++]);
 }
 
 static char *
@@ -159,7 +160,7 @@ getTextArg(PrintfArguments * p)
 {
 	if (p->nArg <= p->nUsed)
 		return 0;
-	return (char *)sql_value_text(p->apArg[p->nUsed++]);
+	return (char *)sql_value_text(&p->apArg[p->nUsed++]);
 }
 
 /*
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 515fce3a9..ce15500e5 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -263,6 +263,7 @@
 #include <assert.h>
 #include <stddef.h>
 
+struct port;
 typedef long long int sql_int64;
 typedef unsigned long long int sql_uint64;
 typedef sql_int64 sql_int64;
@@ -379,52 +380,6 @@ sql_value_is_null(sql_value *value)
 	return sql_value_type(value) == MP_NIL;
 }
 
-sql *
-sql_context_db_handle(sql_context *);
-
-
-void
-sql_result_blob(sql_context *, const void *,
-		    int, void (*)(void *));
-
-void
-sql_result_blob64(sql_context *, const void *,
-		      sql_uint64, void (*)(void *));
-
-void
-sql_result_double(sql_context *, double);
-
-void
-sql_result_int(sql_context *, int);
-
-void
-sql_result_bool(struct sql_context *ctx, bool value);
-
-void
-sql_result_int64(sql_context *, sql_int64);
-
-void
-sql_result_null(sql_context *);
-
-void
-sql_result_text(sql_context *, const char *,
-		    int, void (*)(void *));
-
-void
-sql_result_text64(sql_context *, const char *,
-		      sql_uint64, void (*)(void *));
-
-void
-sql_result_value(sql_context *,
-		     sql_value *);
-
-void
-sql_result_zeroblob(sql_context *, int n);
-
-int
-sql_result_zeroblob64(sql_context *,
-			  sql_uint64 n);
-
 char *
 sql_mprintf(const char *, ...);
 char *
@@ -537,9 +492,8 @@ sql_randomness(int N, void *P);
 /**
  * Return the number of affected rows in the last SQL statement.
  */
-void
-sql_row_count(struct sql_context *context, MAYBE_UNUSED int unused1,
-	      MAYBE_UNUSED sql_value **unused2);
+int
+sql_builtin_row_count(struct func *func, struct port *args, struct port *ret);
 
 void *
 sql_user_data(sql_context *);
@@ -569,22 +523,15 @@ sql_initialize(void);
 #define SQL_DETERMINISTIC    0x800
 
 int
-sql_create_function_v2(sql * db,
-			   const char *zFunctionName,
-			   enum field_type type,
-			   int nArg,
-			   int flags,
-			   void *pApp,
-			   void (*xFunc) (sql_context *,
-					  int,
-					  sql_value **),
-			   void (*xStep) (sql_context *,
-					  int,
-					  sql_value **),
-			   void (*xFinal)
-			   (sql_context *),
-			   void (*xDestroy) (void *)
-	);
+sql_create_function_v2(struct sql * db, const char *zFunc, enum field_type type,
+		       int nArg, int flags, void *p,
+		       int (*xSFunc) (struct func *func, struct port *args,
+				      struct port *ret),
+		       int (*xStep) (struct func *func, struct port *args,
+				     struct port *ret),
+		       int (*xFinal) (struct func *func, struct port *args,
+				      struct port *ret),
+		       void (*xDestroy) (void *ctx));
 
 #define SQL_OPEN_READONLY         0x00000001	/* Ok for sql_open_v2() */
 #define SQL_OPEN_READWRITE        0x00000002	/* Ok for sql_open_v2() */
@@ -1109,7 +1056,7 @@ struct sql {
 	u8 dfltLockMode;	/* Default locking-mode for attached dbs */
 	u8 mTrace;		/* zero or more sql_TRACE flags */
 	u32 magic;		/* Magic number for detect library misuse */
-	/** Value returned by sql_row_count(). */
+	/** Value returned by sql_builtin_row_count(). */
 	int nChange;
 	int aLimit[SQL_N_LIMIT];	/* Limits */
 	int nMaxSorterMmap;	/* Maximum size of regions mapped by sorter */
@@ -1219,8 +1166,8 @@ struct FuncDef {
 	u16 funcFlags;		/* Some combination of sql_FUNC_* */
 	void *pUserData;	/* User data parameter */
 	FuncDef *pNext;		/* Next function with same name */
-	void (*xSFunc) (sql_context *, int, sql_value **);	/* func or agg-step */
-	void (*xFinalize) (sql_context *);	/* Agg finalizer */
+	int (*xSFunc) (struct func *func, struct port *args, struct port *ret);
+	int (*xFinalize) (struct func *func, struct port *args, struct port *ret);
 	const char *zName;	/* SQL name of the function. */
 	union {
 		FuncDef *pHash;	/* Next with a different name but the same hash */
@@ -2667,7 +2614,7 @@ int sqlIsNaN(double);
 struct PrintfArguments {
 	int nArg;		/* Total number of arguments */
 	int nUsed;		/* Number of arguments used so far */
-	sql_value **apArg;	/* The argument values */
+	struct Mem *apArg;	/* The argument values */
 };
 
 void sqlVXPrintf(StrAccum *, const char *, va_list);
@@ -4273,12 +4220,16 @@ sql_key_info_to_key_def(struct sql_key_info *key_info);
 int
 sql_is_like_func(struct sql *db, struct Expr *expr, int *is_like_ci);
 
-int sqlCreateFunc(sql *, const char *, enum field_type,
-		      int, int, void *,
-		      void (*)(sql_context *, int, sql_value **),
-		      void (*)(sql_context *, int, sql_value **),
-		      void (*)(sql_context *),
-		      FuncDestructor * pDestructor);
+int
+sqlCreateFunc(struct sql * db, const char *zFunctionName, enum field_type type,
+	      int nArg, int flags, void *pUserData,
+	      int (*xSFunc) (struct func *func, struct port *args,
+			     struct port *ret),
+	      int (*xStep) (struct func *func, struct port *args,
+			    struct port *ret),
+	      int (*xFinal) (struct func *func, struct port *args,
+			     struct port *ret),
+	      struct FuncDestructor * pDestructor);
 
 /** Set OOM error flag. */
 static inline void
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index c8887f9b7..92c99877a 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -44,6 +44,7 @@
 #include "box/fk_constraint.h"
 #include "box/txn.h"
 #include "box/tuple.h"
+#include "box/port.h"
 #include "sqlInt.h"
 #include "vdbeInt.h"
 #include "tarantoolInt.h"
@@ -1719,21 +1720,18 @@ case OP_CollSeq: {
  * See also: Function0, AggStep, AggFinal
  */
 case OP_Function0: {
-	int n;
 	sql_context *pCtx;
 
 	assert(pOp->p4type==P4_FUNCDEF);
-	n = pOp->p5;
 	assert(pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor));
-	assert(n==0 || (pOp->p2>0 && pOp->p2+n<=(p->nMem+1 - p->nCursor)+1));
-	assert(pOp->p3<pOp->p2 || pOp->p3>=pOp->p2+n);
-	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx) + (n-1)*sizeof(sql_value*));
+	assert(pOp->p5 == 0 ||
+	       (pOp->p2 > 0 && pOp->p2 + pOp->p5 <= (p->nMem+1 - p->nCursor)+1));
+	assert(pOp->p3 < pOp->p2 || pOp->p3>=pOp->p2 + pOp->p5);
+	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx));
 	if (pCtx==0) goto no_mem;
-	pCtx->pOut = 0;
 	pCtx->pFunc = pOp->p4.pFunc;
 	pCtx->iOp = (int)(pOp - aOp);
 	pCtx->pVdbe = p;
-	pCtx->argc = n;
 	pOp->p4type = P4_FUNCCTX;
 	pOp->p4.pCtx = pCtx;
 	pOp->opcode = OP_Function;
@@ -1741,7 +1739,6 @@ case OP_Function0: {
 	FALLTHROUGH;
 }
 case OP_Function: {
-	int i;
 	sql_context *pCtx;
 
 	assert(pOp->p4type==P4_FUNCCTX);
@@ -1753,33 +1750,39 @@ case OP_Function: {
 	 * reinitializes the relavant parts of the sql_context object
 	 */
 	pOut = &aMem[pOp->p3];
-	if (pCtx->pOut != pOut) {
-		pCtx->pOut = pOut;
-		for(i=pCtx->argc-1; i>=0; i--) pCtx->argv[i] = &aMem[pOp->p2+i];
-	}
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	struct port args, ret;
+	port_vdbemem_create(&args, (struct sql_value *)&aMem[pOp->p2],
+			    pOp->p5, pCtx);
 
-	memAboutToChange(p, pCtx->pOut);
+	memAboutToChange(p, pOut);
 #ifdef SQL_DEBUG
-	for(i=0; i<pCtx->argc; i++) {
-		assert(memIsValid(pCtx->argv[i]));
-		REGISTER_TRACE(p, pOp->p2+i, pCtx->argv[i]);
+	for(int i = 0; i < pOp->p5; i++) {
+		assert(memIsValid(&aMem[pOp->p2 + i]));
+		REGISTER_TRACE(p, pOp->p2+i, &aMem[pOp->p2 + i]);
 	}
 #endif
-	MemSetTypeFlag(pCtx->pOut, MEM_Null);
-	pCtx->is_aborted = false;
-	(*pCtx->pFunc->xSFunc)(pCtx, pCtx->argc, pCtx->argv);/* IMP: R-24505-23230 */
-
-	/* If the function returned an error, throw an exception */
-	if (pCtx->is_aborted)
+	MemSetTypeFlag(pOut, MEM_Null);
+	rc = (*pCtx->pFunc->xSFunc)(NULL, &args, &ret);
+	if (rc != 0)
+		goto abort_due_to_error;
+	uint32_t size;
+	struct Mem *mem = (struct Mem *)port_get_vdbemem(&ret, &size);
+	if (mem == NULL) {
+		region_truncate(region, region_svp);
 		goto abort_due_to_error;
+	}
+	*pOut = mem[0];
+	region_truncate(region, region_svp);
 
 	/* Copy the result of the function into register P3 */
 	if (pOut->flags & (MEM_Str|MEM_Blob)) {
-		if (sqlVdbeMemTooBig(pCtx->pOut)) goto too_big;
+		if (sqlVdbeMemTooBig(pOut)) goto too_big;
 	}
 
-	REGISTER_TRACE(p, pOp->p3, pCtx->pOut);
-	UPDATE_MAX_BLOBSIZE(pCtx->pOut);
+	REGISTER_TRACE(p, pOp->p3, pOut);
+	UPDATE_MAX_BLOBSIZE(pOut);
 	break;
 }
 
@@ -4964,21 +4967,19 @@ case OP_DecrJumpZero: {      /* jump, in1 */
  * step function.
  */
 case OP_AggStep0: {
-	int n;
 	sql_context *pCtx;
 
-	assert(pOp->p4type==P4_FUNCDEF);
-	n = pOp->p5;
+	assert(pOp->p4type == P4_FUNCDEF);
 	assert(pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor));
-	assert(n==0 || (pOp->p2>0 && pOp->p2+n<=(p->nMem+1 - p->nCursor)+1));
-	assert(pOp->p3<pOp->p2 || pOp->p3>=pOp->p2+n);
-	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx) + (n-1)*sizeof(sql_value*));
+	assert(pOp->p5 == 0 ||
+	       (pOp->p2 > 0 && pOp->p2 + pOp->p5 <= (p->nMem+1 - p->nCursor)+1));
+	assert(pOp->p3 < pOp->p2 || pOp->p3 >= pOp->p2 + pOp->p5);
+	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx));
 	if (pCtx==0) goto no_mem;
 	pCtx->pMem = 0;
 	pCtx->pFunc = pOp->p4.pFunc;
 	pCtx->iOp = (int)(pOp - aOp);
 	pCtx->pVdbe = p;
-	pCtx->argc = n;
 	pOp->p4type = P4_FUNCCTX;
 	pOp->p4.pCtx = pCtx;
 	pOp->opcode = OP_AggStep;
@@ -4988,41 +4989,33 @@ case OP_AggStep0: {
 case OP_AggStep: {
 	int i;
 	sql_context *pCtx;
-	Mem *pMem;
 	Mem t;
 
 	assert(pOp->p4type==P4_FUNCCTX);
 	pCtx = pOp->p4.pCtx;
-	pMem = &aMem[pOp->p3];
+	pCtx->pMem = &aMem[pOp->p3];
 
-	/* If this function is inside of a trigger, the register array in aMem[]
-	 * might change from one evaluation to the next.  The next block of code
-	 * checks to see if the register array has changed, and if so it
-	 * reinitializes the relavant parts of the sql_context object
-	 */
-	if (pCtx->pMem != pMem) {
-		pCtx->pMem = pMem;
-		for(i=pCtx->argc-1; i>=0; i--) pCtx->argv[i] = &aMem[pOp->p2+i];
-	}
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	struct port args, ret;
+	port_vdbemem_create(&args, (struct sql_value *)&aMem[pOp->p2],
+			    pOp->p5, pCtx);
 
 #ifdef SQL_DEBUG
-	for(i=0; i<pCtx->argc; i++) {
-		assert(memIsValid(pCtx->argv[i]));
-		REGISTER_TRACE(p, pOp->p2+i, pCtx->argv[i]);
+	for(i = 0; i < pOp->p5; i++) {
+		assert(memIsValid(&aMem[pOp->p2 + i]));
+		REGISTER_TRACE(p, pOp->p2+i, &aMem[pOp->p2 + i]);
 	}
 #endif
 
-	pMem->n++;
 	sqlVdbeMemInit(&t, db, MEM_Null);
-	pCtx->pOut = &t;
-	pCtx->is_aborted = false;
+	pOut = &t;
 	pCtx->skipFlag = 0;
-	(pCtx->pFunc->xSFunc)(pCtx,pCtx->argc,pCtx->argv); /* IMP: R-24505-23230 */
-	if (pCtx->is_aborted) {
-		sqlVdbeMemRelease(&t);
+	rc = (*pCtx->pFunc->xSFunc)(NULL, &args, &ret);
+	region_truncate(region, region_svp);
+	if (rc != 0)
 		goto abort_due_to_error;
-	}
-	assert(t.flags==MEM_Null);
+
 	if (pCtx->skipFlag) {
 		assert(pOp[-1].opcode==OP_CollSeq);
 		i = pOp[-1].p1;
diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
index 6bfeecc85..4c8363a22 100644
--- a/src/box/sql/vdbeInt.h
+++ b/src/box/sql/vdbeInt.h
@@ -181,10 +181,7 @@ struct Mem {
 	u32 uTemp;		/* Transient storage for serial_type in OP_MakeRecord */
 	sql *db;		/* The associated database connection */
 	void (*xDel) (void *);	/* Destructor for Mem.z - only valid if MEM_Dyn */
-#ifdef SQL_DEBUG
 	Mem *pScopyFrom;	/* This Mem is a shallow copy of pScopyFrom */
-	void *pFiller;		/* So that sizeof(Mem) is a multiple of 8 */
-#endif
 };
 
 /*
@@ -294,19 +291,11 @@ mem_apply_numeric_type(struct Mem *record);
  * (Mem) which are only defined there.
  */
 struct sql_context {
-	Mem *pOut;		/* The return value is stored here */
 	FuncDef *pFunc;		/* Pointer to function information */
 	Mem *pMem;		/* Memory cell used to store aggregate context */
 	Vdbe *pVdbe;		/* The VM that owns this context */
 	int iOp;		/* Instruction number of OP_Function */
-	/*
-	 * True, if an error occurred during the execution of the
-	 * function.
-	 */
-	bool is_aborted;
 	u8 skipFlag;		/* Skip accumulator loading if true */
-	u8 argc;		/* Number of arguments */
-	sql_value *argv[1];	/* Argument set */
 };
 
 /* A bitfield type for use inside of structures.  Always follow with :N where
@@ -584,4 +573,11 @@ char *
 sql_vdbe_mem_encode_tuple(struct Mem *fields, uint32_t field_count,
 			  uint32_t *tuple_size, struct region *region);
 
+/**
+ * Allocate a sequence of initialized vdbe memory registers
+ * on region.
+ */
+struct Mem *
+vdbemem_alloc_on_region(uint32_t count);
+
 #endif				/* !defined(SQL_VDBEINT_H) */
diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
index ef0ab79d2..d014e8258 100644
--- a/src/box/sql/vdbeapi.c
+++ b/src/box/sql/vdbeapi.c
@@ -69,6 +69,23 @@ invokeProfileCallback(sql * db, Vdbe * p)
 #define checkProfileCallback(DB,P) \
    if( ((P)->startTime)>0 ){ invokeProfileCallback(DB,P); }
 
+struct Mem *
+vdbemem_alloc_on_region(uint32_t count)
+{
+	struct region *region = &fiber()->gc;
+	struct Mem *ret = region_alloc(region, count * sizeof(*ret));
+	if (ret == NULL) {
+		diag_set(OutOfMemory, count * sizeof(*ret), "sqlValueNew", "ret");
+		return NULL;
+	}
+	memset(ret, 0, count * sizeof(*ret));
+	for (uint32_t i = 0; i < count; i++) {
+		sqlVdbeMemInit(&ret[i], sql_get(), MEM_Null);
+		assert(memIsValid(&ret[i]));
+	}
+	return ret;
+}
+
 /*
  * The following routine destroys a virtual machine that is created by
  * the sql_compile() routine. The integer returned is an SQL_
@@ -254,22 +271,9 @@ sql_value_free(sql_value * pOld)
  * The invokeValueDestructor(P,X) routine invokes destructor function X()
  * on value P is not going to be used and need to be destroyed.
  */
-static void
-setResultStrOrError(sql_context * pCtx,	/* Function context */
-		    const char *z,	/* String pointer */
-		    int n,	/* Bytes in string, or negative */
-		    void (*xDel) (void *)	/* Destructor function */
-    )
-{
-	if (sqlVdbeMemSetStr(pCtx->pOut, z, n, 1, xDel) != 0)
-		pCtx->is_aborted = true;
-}
 
 static int
-invokeValueDestructor(const void *p,	/* Value to destroy */
-		      void (*xDel) (void *),	/* The destructor */
-		      sql_context *pCtx	/* Set an error if no NULL */
-    )
+invokeValueDestructor(const void *p, void (*xDel) (void *))
 {
 	assert(xDel != SQL_DYNAMIC);
 	if (xDel == 0) {
@@ -279,114 +283,9 @@ invokeValueDestructor(const void *p,	/* Value to destroy */
 	} else {
 		xDel((void *)p);
 	}
-	if (pCtx) {
-		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob is too "\
-			 "big");
-		pCtx->is_aborted = true;
-	}
 	return -1;
 }
 
-void
-sql_result_blob(sql_context * pCtx,
-		    const void *z, int n, void (*xDel) (void *)
-    )
-{
-	assert(n >= 0);
-	if (sqlVdbeMemSetStr(pCtx->pOut, z, n, 0, xDel) != 0)
-		pCtx->is_aborted = true;
-}
-
-void
-sql_result_blob64(sql_context * pCtx,
-		      const void *z, sql_uint64 n, void (*xDel) (void *)
-    )
-{
-	assert(xDel != SQL_DYNAMIC);
-	if (n > 0x7fffffff) {
-		(void)invokeValueDestructor(z, xDel, pCtx);
-	} else {
-		setResultStrOrError(pCtx, z, (int)n, xDel);
-	}
-}
-
-void
-sql_result_double(sql_context * pCtx, double rVal)
-{
-	sqlVdbeMemSetDouble(pCtx->pOut, rVal);
-}
-
-void
-sql_result_int(sql_context * pCtx, int iVal)
-{
-	sqlVdbeMemSetInt64(pCtx->pOut, (i64) iVal);
-}
-
-void
-sql_result_bool(struct sql_context *ctx, bool value)
-{
-	mem_set_bool(ctx->pOut, value);
-}
-
-void
-sql_result_int64(sql_context * pCtx, i64 iVal)
-{
-	sqlVdbeMemSetInt64(pCtx->pOut, iVal);
-}
-
-void
-sql_result_null(sql_context * pCtx)
-{
-	sqlVdbeMemSetNull(pCtx->pOut);
-}
-
-void
-sql_result_text(sql_context * pCtx,
-		    const char *z, int n, void (*xDel) (void *)
-    )
-{
-	setResultStrOrError(pCtx, z, n, xDel);
-}
-
-void
-sql_result_text64(sql_context * pCtx,
-		      const char *z,
-		      sql_uint64 n,
-		      void (*xDel) (void *))
-{
-	assert(xDel != SQL_DYNAMIC);
-	if (n > 0x7fffffff) {
-		(void)invokeValueDestructor(z, xDel, pCtx);
-	} else {
-		setResultStrOrError(pCtx, z, (int)n, xDel);
-	}
-}
-
-void
-sql_result_value(sql_context * pCtx, sql_value * pValue)
-{
-	sqlVdbeMemCopy(pCtx->pOut, pValue);
-}
-
-void
-sql_result_zeroblob(sql_context * pCtx, int n)
-{
-	sqlVdbeMemSetZeroBlob(pCtx->pOut, n);
-}
-
-int
-sql_result_zeroblob64(sql_context * pCtx, u64 n)
-{
-	Mem *pOut = pCtx->pOut;
-	if (n > (u64) pOut->db->aLimit[SQL_LIMIT_LENGTH]) {
-		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob is too "\
-			 "big");
-		return -1;
-	}
-	sqlVdbeMemSetZeroBlob(pCtx->pOut, (int)n);
-	return 0;
-}
-
 /*
  * Execute the statement pStmt, either until a row of data is ready, the
  * statement is completely executed or an error occurs.
@@ -475,23 +374,6 @@ sql_user_data(sql_context * p)
 	return p->pFunc->pUserData;
 }
 
-/*
- * Extract the user data from a sql_context structure and return a
- * pointer to it.
- *
- * IMPLEMENTATION-OF: R-46798-50301 The sql_context_db_handle() interface
- * returns a copy of the pointer to the database connection (the 1st
- * parameter) of the sql_create_function() and
- * sql_create_function16() routines that originally registered the
- * application defined function.
- */
-sql *
-sql_context_db_handle(sql_context * p)
-{
-	assert(p && p->pOut);
-	return p->pOut->db;
-}
-
 /*
  * Return the current time for a statement.  If the current time
  * is requested more than once within the same run of a single prepared
@@ -512,7 +394,7 @@ sqlStmtCurrentTime(sql_context * p)
 	    p->pVdbe != 0 ? &p->pVdbe->iCurrentTime : &iTime;
 #endif
 	if (*piTime == 0) {
-		rc = sqlOsCurrentTimeInt64(p->pOut->db->pVfs, piTime);
+		rc = sqlOsCurrentTimeInt64(sql_get()->pVfs, piTime);
 		if (rc)
 			*piTime = 0;
 	}
@@ -614,10 +496,7 @@ columnNullValue(void)
 		    /* .uTemp      = */ (u32) 0,
 		    /* .db         = */ (sql *) 0,
 		    /* .xDel       = */ (void (*)(void *))0,
-#ifdef SQL_DEBUG
 		    /* .pScopyFrom = */ (Mem *) 0,
-		    /* .pFiller    = */ (void *)0,
-#endif
 	};
 	return &nullMem;
 }
@@ -934,7 +813,7 @@ sql_bind_blob64(sql_stmt * pStmt,
 {
 	assert(xDel != SQL_DYNAMIC);
 	if (nData > 0x7fffffff) {
-		return invokeValueDestructor(zData, xDel, 0);
+		return invokeValueDestructor(zData, xDel);
 	} else {
 		return sql_bind_blob(pStmt, i, zData, (int)nData, xDel);
 	}
@@ -1017,7 +896,7 @@ sql_bind_text64(sql_stmt * pStmt,
 {
 	assert(xDel != SQL_DYNAMIC);
 	if (nData > 0x7fffffff) {
-		return invokeValueDestructor(zData, xDel, 0);
+		return invokeValueDestructor(zData, xDel);
 	} else {
 		return bindText(pStmt, i, zData, (int)nData, xDel);
 	}
diff --git a/src/box/sql/vdbemem.c b/src/box/sql/vdbemem.c
index f52035b0e..7bd5cdd71 100644
--- a/src/box/sql/vdbemem.c
+++ b/src/box/sql/vdbemem.c
@@ -41,6 +41,7 @@
 #include "tarantoolInt.h"
 #include "box/schema.h"
 #include "box/tuple.h"
+#include "box/port.h"
 #include "mpstream.h"
 
 #ifdef SQL_DEBUG
@@ -315,26 +316,31 @@ sqlVdbeMemStringify(Mem * pMem, u8 bForce)
 int
 sqlVdbeMemFinalize(Mem * pMem, FuncDef * pFunc)
 {
+	int rc = 0;
 	if (ALWAYS(pFunc && pFunc->xFinalize)) {
-		sql_context ctx;
-		Mem t;
 		assert((pMem->flags & MEM_Null) != 0 || pFunc == pMem->u.pDef);
+		struct sql_context ctx;
 		memset(&ctx, 0, sizeof(ctx));
-		memset(&t, 0, sizeof(t));
-		t.flags = MEM_Null;
-		t.db = pMem->db;
-		ctx.pOut = &t;
 		ctx.pMem = pMem;
 		ctx.pFunc = pFunc;
-		pFunc->xFinalize(&ctx);	/* IMP: R-24505-23230 */
+		struct region *region = &fiber()->gc;
+		size_t region_svp = region_used(region);
+		struct port args, ret;
+		port_vdbemem_create(&args, NULL, 0, &ctx);
+		rc = pFunc->xFinalize(NULL, &args, &ret);
 		assert((pMem->flags & MEM_Dyn) == 0);
 		if (pMem->szMalloc > 0)
 			sqlDbFree(pMem->db, pMem->zMalloc);
-		memcpy(pMem, &t, sizeof(t));
-		if (ctx.is_aborted)
-			return -1;
+		uint32_t size;
+		struct Mem *mem = (struct Mem *)port_get_vdbemem(&ret, &size);
+		if (mem != NULL) {
+			*pMem = mem[0];
+		} else {
+			rc = -1;
+		}
+		region_truncate(region, region_svp);
 	}
-	return 0;
+	return rc;
 }
 
 /*
@@ -1239,7 +1245,6 @@ valueFromFunction(sql * db,	/* The database connection */
 		  struct ValueNewStat4Ctx *pCtx	/* Second argument for valueNew() */
     )
 {
-	sql_context ctx;	/* Context object for function invocation */
 	sql_value **apVal = 0;	/* Function arguments */
 	int nVal = 0;		/* Size of apVal[] array */
 	FuncDef *pFunc = 0;	/* Function definition */
@@ -1260,6 +1265,8 @@ valueFromFunction(sql * db,	/* The database connection */
 	    ) {
 		return 0;
 	}
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
 
 	if (pList) {
 		apVal =
@@ -1285,15 +1292,25 @@ valueFromFunction(sql * db,	/* The database connection */
 	}
 
 	assert(!pCtx->pParse->is_aborted);
-	memset(&ctx, 0, sizeof(ctx));
-	ctx.pOut = pVal;
-	ctx.pFunc = pFunc;
-	pFunc->xSFunc(&ctx, nVal, apVal);
-	assert(!ctx.is_aborted);
+
+	struct port args, ret;
+	port_vdbemem_create(&args, (struct sql_value *)apVal, nVal, NULL);
+	if ((*pFunc->xSFunc)(NULL, &args, &ret) != 0) {
+		rc = -1;
+		goto value_from_function_out;
+	}
 	sql_value_apply_type(pVal, type);
 	assert(rc == 0);
+	uint32_t size;
+	struct Mem *mem = (struct Mem *)port_get_vdbemem(&ret, &size);
+	if (mem == NULL) {
+		rc = -1;
+		goto value_from_function_out;
+	}
+	*pVal = mem[0];
 
  value_from_function_out:
+	region_truncate(region, region_svp);
 	if (rc != 0)
 		pVal = 0;
 	if (apVal) {
diff --git a/src/lib/core/port.h b/src/lib/core/port.h
index 09a026df5..cd8cd31f6 100644
--- a/src/lib/core/port.h
+++ b/src/lib/core/port.h
@@ -40,6 +40,7 @@ extern "C" {
 struct obuf;
 struct lua_State;
 struct port;
+struct sql_value;
 
 /**
  * A single port represents a destination of any output. One such
@@ -98,6 +99,24 @@ struct port_vtab {
 	 * is responsible for cleaning up.
 	 **/
 	const char *(*get_msgpack)(struct port *port, uint32_t *size);
+	/**
+	 * Get the content of a port as a sequence of vdbe memory
+	 * variables. The SQL VDBE uses this representation for
+	 * process data. This API is usefull to pass VDBE
+	 * variables to sql builtin functions.
+	 * The lifecycle of the returned value is
+	 * implementation-specific: it may either be returned
+	 * directly from the port, in which case the data will
+	 * stay alive as long as the port is alive, or it may be
+	 * allocated on the fiber()->gc, in which case the caller
+	 * is responsible for cleaning up.
+	 */
+	struct sql_value *(*get_vdbemem)(struct port *port, uint32_t *size);
+	/**
+	 * Get additional implementation-specific information
+	 * stored in port. Used for aggregate functions in SQL.
+	 */
+	void *(*get_context)(struct port *port);
 	/** Destroy a port and release associated resources. */
 	void (*destroy)(struct port *port);
 };
@@ -150,6 +169,18 @@ port_get_msgpack(struct port *port, uint32_t *size)
 	return port->vtab->get_msgpack(port, size);
 }
 
+static inline struct sql_value *
+port_get_vdbemem(struct port *port, uint32_t *size)
+{
+	return port->vtab->get_vdbemem(port, size);
+}
+
+static inline const void *
+port_get_context(struct port *port)
+{
+	return port->vtab->get_context(port);
+}
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined __cplusplus */
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 11/12] box: use own vtab per each function object
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 01/12] sql: get rid of SOUNDEX, MATCH Kirill Shcherbatov
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 10/12] sql: refactor builtins signatures with port Kirill Shcherbatov
@ 2019-07-10 11:00 ` Kirill Shcherbatov
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 12/12] sql: use schema's func hash instead of FuncDef hash Kirill Shcherbatov
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:00 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

Tarantool functions used to refer to a static func_vtab object
based on it's type. With this patch, each function has own vtab
object, so it's virtual method may be overridden.
It is required to call builtin functions in func_call directly
in further patches.

Needed for #4113, #2200, #2233
---
 src/box/func.c     | 8 +++-----
 src/box/func.h     | 2 +-
 src/box/lua/call.c | 8 ++------
 src/box/sql/func.c | 3 +--
 4 files changed, 7 insertions(+), 14 deletions(-)

diff --git a/src/box/func.c b/src/box/func.c
index 8d93a83b2..e36649d15 100644
--- a/src/box/func.c
+++ b/src/box/func.c
@@ -437,7 +437,7 @@ func_c_new(struct func_def *def)
 		diag_set(OutOfMemory, sizeof(*func), "malloc", "func");
 		return NULL;
 	}
-	func->base.vtab = &func_c_vtab;
+	func->base.vtab = func_c_vtab;
 	func->func = NULL;
 	func->module = NULL;
 	return &func->base;
@@ -462,7 +462,6 @@ func_c_unload(struct func_c *func)
 static void
 func_c_destroy(struct func *base)
 {
-	assert(base->vtab == &func_c_vtab);
 	assert(base != NULL && base->def->language == FUNC_LANGUAGE_C);
 	struct func_c *func = (struct func_c *) base;
 	func_c_unload(func);
@@ -505,7 +504,6 @@ func_c_load(struct func_c *func)
 int
 func_c_call(struct func *base, struct port *args, struct port *ret)
 {
-	assert(base->vtab == &func_c_vtab);
 	assert(base != NULL && base->def->language == FUNC_LANGUAGE_C);
 	struct func_c *func = (struct func_c *) base;
 	if (func->func == NULL) {
@@ -551,7 +549,7 @@ void
 func_delete(struct func *func)
 {
 	struct func_def *def = func->def;
-	func->vtab->destroy(func);
+	func->vtab.destroy(func);
 	free(def);
 }
 
@@ -617,7 +615,7 @@ func_call(struct func *base, struct port *args, struct port *ret)
 		}
 		fiber_set_user(fiber(), &base->owner_credentials);
 	}
-	int rc = base->vtab->call(base, args, ret);
+	int rc = base->vtab.call(base, args, ret);
 	/* Restore the original user */
 	if (orig_credentials)
 		fiber_set_user(fiber(), orig_credentials);
diff --git a/src/box/func.h b/src/box/func.h
index 2236fd873..7e4dd37a3 100644
--- a/src/box/func.h
+++ b/src/box/func.h
@@ -72,7 +72,7 @@ struct func_vtab {
 struct func {
 	struct func_def *def;
 	/** Virtual method table. */
-	const struct func_vtab *vtab;
+	struct func_vtab vtab;
 	/**
 	 * Authentication id of the owner of the function,
 	 * used for set-user-id functions.
diff --git a/src/box/lua/call.c b/src/box/lua/call.c
index 4297218cd..752f05745 100644
--- a/src/box/lua/call.c
+++ b/src/box/lua/call.c
@@ -708,14 +708,14 @@ func_lua_new(struct func_def *def)
 	}
 	if (def->body != NULL) {
 		func->base.def = def;
-		func->base.vtab = &func_persistent_lua_vtab;
+		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;
+		func->base.vtab = func_lua_vtab;
 	}
 	return &func->base;
 }
@@ -724,7 +724,6 @@ static void
 func_lua_destroy(struct func *func)
 {
 	assert(func != NULL && func->def->language == FUNC_LANGUAGE_LUA);
-	assert(func->vtab == &func_lua_vtab);
 	free(func);
 }
 
@@ -732,7 +731,6 @@ static inline int
 func_lua_call(struct func *func, struct port *args, struct port *ret)
 {
 	assert(func != NULL && func->def->language == FUNC_LANGUAGE_LUA);
-	assert(func->vtab == &func_lua_vtab);
 	return box_lua_call(func->def->name, func->def->name_len, args, ret);
 }
 
@@ -752,7 +750,6 @@ 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);
@@ -763,7 +760,6 @@ 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;
diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index 18db8530d..f79939cb6 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -2183,14 +2183,13 @@ func_sql_builtin_new(struct func_def *def)
 	}
 	/** Don't export SQL builtins in Lua for now. */
 	def->exports.lua = false;
-	func->base.vtab = &func_sql_builtin_vtab;
+	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);
 }
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 12/12] sql: use schema's func hash instead of FuncDef hash
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
                   ` (2 preceding siblings ...)
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 11/12] box: use own vtab per each function object Kirill Shcherbatov
@ 2019-07-10 11:01 ` Kirill Shcherbatov
  2019-07-10 20:22   ` [tarantool-patches] " Konstantin Osipov
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 02/12] sql: get rid of LIKELY, UNLIKELY and LIKEHOOD Kirill Shcherbatov
                   ` (7 subsequent siblings)
  11 siblings, 1 reply; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:01 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

Part of #2200
Closes #4113
Closes #2233

@TarantoolBot document
Title: Reworked SQL functions machinery

Now all functions in Tarantool use uniform API and calling
convention. Therefore it is possible to call C and Lua functions
from SQL.

To make a function available in SQL, you need to define it
with list of types of it's arguments 'param_list', the type
of returned value 'returns' and mention 'SQL' language in
exports parameter. Deterministic functions must be defined with
is_deterministic = true (the query planner use this information
to build an effective vdbe code).

Example:
-- C function is called in SQL
box.schema.func.create("function1.divide", {language = 'C',
                       returns = 'number', param_list = {'number', 'number'},
                       is_deterministic = true, exports = {'LUA', 'SQL'}})
box.execute('SELECT "function1.divide"(6, 3)')
---
- metadata:
  - name: '"function1.divide"(6, 3)'
    type: number
  rows:
  - [2]
...

-- Persistent Lua function is called in SQL
box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number',
			body = 'function (a, b) return a + b end',
			is_deterministic = true,
			param_list = {'number', 'number'},
			exports = {'LUA', 'SQL'}})
box.execute('SELECT summarize(1, 2)')
---
- metadata:
  - name: summarize(1, 2)
    type: number
  rows:
  - [3]
...

Moreover there is a predefined Lua function "LUA" that allows to
call Lua code in SQL. The argument of LUA function is a valid
Lua code that returns some scalar type.

Example:
box.execute('SELECT lua(\'return box.cfg.memtx_memory\')')
---
- metadata:
  - name: lua('return box.cfg.memtx_memory')
    type: any
  rows:
  - [268435456]
...
---
 src/box/CMakeLists.txt          |   1 -
 src/box/alter.cc                |   1 +
 src/box/func.c                  |   3 +-
 src/box/func.h                  |   5 +
 src/box/func_def.c              |  19 +
 src/box/func_def.h              |   2 +
 src/box/lua/call.c              |   8 +-
 src/box/lua/lua_sql.c           | 217 ----------
 src/box/lua/lua_sql.h           |  39 --
 src/box/lua/schema.lua          |   4 +-
 src/box/port.c                  |   5 +-
 src/box/sql.h                   |  13 +
 src/box/sql/analyze.c           |  40 +-
 src/box/sql/callback.c          | 208 ----------
 src/box/sql/date.c              |  28 --
 src/box/sql/expr.c              |  51 ++-
 src/box/sql/func.c              | 697 +++++++++++++++++++++++++-------
 src/box/sql/global.c            |   7 -
 src/box/sql/main.c              | 134 ------
 src/box/sql/resolve.c           |  55 ++-
 src/box/sql/select.c            |  10 +-
 src/box/sql/sqlInt.h            | 180 +--------
 src/box/sql/vdbe.c              |  19 +-
 src/box/sql/vdbe.h              |   6 +-
 src/box/sql/vdbeInt.h           |  20 +-
 src/box/sql/vdbeapi.c           |  11 +-
 src/box/sql/vdbeaux.c           |  31 +-
 src/box/sql/vdbemem.c           |  73 ++--
 src/box/sql/whereexpr.c         |   4 +-
 src/lib/coll/coll.c             |   1 +
 test/box/function1.result       | 119 ++++++
 test/box/function1.test.lua     |  35 ++
 test/sql-tap/alias.test.lua     |  11 +-
 test/sql-tap/check.test.lua     |  13 +-
 test/sql-tap/func5.test.lua     |  29 +-
 test/sql-tap/lua_sql.test.lua   | 120 +++---
 test/sql-tap/subquery.test.lua  |  21 +-
 test/sql-tap/trigger9.test.lua  |   6 +-
 test/sql-tap/where2.test.lua    |   4 +-
 test/sql/errinj.result          |  25 --
 test/sql/errinj.test.lua        |  10 -
 test/sql/func-recreate.result   |  41 +-
 test/sql/func-recreate.test.lua |  28 +-
 43 files changed, 1109 insertions(+), 1245 deletions(-)
 delete mode 100644 src/box/lua/lua_sql.c
 delete mode 100644 src/box/lua/lua_sql.h

diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 481842a39..a0160912d 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -128,7 +128,6 @@ add_library(box STATIC
     ${lua_sources}
     lua/init.c
     lua/call.c
-    lua/lua_sql.c
     lua/cfg.cc
     lua/console.c
     lua/tuple.c
diff --git a/src/box/alter.cc b/src/box/alter.cc
index c92a1f710..cd85552a2 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -2786,6 +2786,7 @@ func_def_new_from_tuple(struct tuple *tuple)
 		def->exports.lua = true;
 		def->param_count = 0;
 	}
+	def->sql_flags = 0;
 	def_guard.is_active = false;
 	return def;
 }
diff --git a/src/box/func.c b/src/box/func.c
index e36649d15..6e7d04f25 100644
--- a/src/box/func.c
+++ b/src/box/func.c
@@ -34,13 +34,13 @@
 #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"
 #include "session.h"
+#include "small/region.h"
 #include <dlfcn.h>
 
 /**
@@ -438,6 +438,7 @@ func_c_new(struct func_def *def)
 		return NULL;
 	}
 	func->base.vtab = func_c_vtab;
+	rlist_create(&func->base.signature);
 	func->func = NULL;
 	func->module = NULL;
 	return &func->base;
diff --git a/src/box/func.h b/src/box/func.h
index 7e4dd37a3..133172bf0 100644
--- a/src/box/func.h
+++ b/src/box/func.h
@@ -82,6 +82,11 @@ struct func {
 	 * Cached runtime access information.
 	 */
 	struct access access[BOX_USER_MAX];
+	/**
+	 * A list of other functions with given name.
+	 * Is valid for SQL builtins.
+	 */
+	struct rlist signature;
 };
 
 /**
diff --git a/src/box/func_def.c b/src/box/func_def.c
index fb9f77df8..ffd76a514 100644
--- a/src/box/func_def.c
+++ b/src/box/func_def.c
@@ -34,9 +34,28 @@ func_def_cmp(struct func_def *def1, struct func_def *def2)
 		return def1->aggregate - def2->aggregate;
 	if (def1->param_count != def2->param_count)
 		return def1->param_count - def2->param_count;
+	if (def1->sql_flags != def2->sql_flags)
+		return def1->sql_flags - def2->sql_flags;
 	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;
 }
+
+struct func_def *
+func_def_dup(struct func_def *def)
+{
+	uint32_t body_offset, comment_offset;
+	uint32_t sz = func_def_sizeof(def->name_len,
+				def->body != NULL ? strlen(def->body) : 0,
+				def->comment != NULL ? strlen(def->comment) : 0,
+				&body_offset, &comment_offset);
+	struct func_def *new = (struct func_def *) malloc(sz);
+	memcpy(new, def, sz);
+	if (new->body != NULL)
+		new->body = (char *)new + body_offset;
+	if (new->comment != NULL)
+		new->comment = (char *)new + comment_offset;
+	return new;
+}
diff --git a/src/box/func_def.h b/src/box/func_def.h
index 508580f78..809d74c42 100644
--- a/src/box/func_def.h
+++ b/src/box/func_def.h
@@ -89,6 +89,8 @@ struct func_def {
 	 * available.
 	 */
 	bool is_sandboxed;
+	/** A set of SQL_FUNCTION_* flags. */
+	uint16_t sql_flags;
 	/** The count of function's input arguments. */
 	int param_count;
 	/** The type of the value returned by function. */
diff --git a/src/box/lua/call.c b/src/box/lua/call.c
index 752f05745..528c41310 100644
--- a/src/box/lua/call.c
+++ b/src/box/lua/call.c
@@ -44,7 +44,6 @@
 #include "box/port.h"
 #include "box/lua/tuple.h"
 #include "small/obuf.h"
-#include "lua_sql.h"
 #include "trivia/util.h"
 #include "mpstream.h"
 
@@ -501,13 +500,16 @@ port_lua_destroy(struct port *base)
 extern const char *
 port_lua_dump_plain(struct port *port, uint32_t *size);
 
+extern struct sql_value *
+port_lua_get_vdbemem(struct port *base, uint32_t *size);
+
 static const struct port_vtab port_lua_vtab = {
 	.dump_msgpack = port_lua_dump,
 	.dump_msgpack_16 = port_lua_dump_16,
 	.dump_lua = port_lua_dump_lua,
 	.dump_plain = port_lua_dump_plain,
 	.get_msgpack = port_lua_get_msgpack,
-	.get_vdbemem = NULL,
+	.get_vdbemem = port_lua_get_vdbemem,
 	.get_context = NULL,
 	.destroy = port_lua_destroy,
 };
@@ -717,6 +719,7 @@ func_lua_new(struct func_def *def)
 		func->lua_ref = LUA_REFNIL;
 		func->base.vtab = func_lua_vtab;
 	}
+	rlist_create(&func->base.signature);
 	return &func->base;
 }
 
@@ -959,7 +962,6 @@ static struct trigger on_alter_func_in_lua = {
 
 static const struct luaL_Reg boxlib_internal[] = {
 	{"call_loadproc",  lbox_call_loadproc},
-	{"sql_create_function",  lbox_sql_create_function},
 	{"module_reload", lbox_module_reload},
 	{"func_call", lbox_func_call},
 	{NULL, NULL}
diff --git a/src/box/lua/lua_sql.c b/src/box/lua/lua_sql.c
deleted file mode 100644
index 0c5797fa2..000000000
--- a/src/box/lua/lua_sql.c
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Copyright 2010-2017, 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 "lua.h"
-#include "lua/utils.h"
-
-#include "box/lua/call.h"
-#include "box/sql/sqlInt.h"
-#include "box/port.h"
-#include "box/sql/vdbeInt.h"
-
-struct lua_sql_func_info {
-	int func_ref;
-};
-
-/**
- * This function is callback which is called by sql engine.
- *
- * Purpose of this function is to call lua func from sql.
- * Lua func should be previously registered in sql
- * (see lbox_sql_create_function).
- */
-static int
-lua_sql_call(struct func *func, struct port *args, struct port *ret)
-{
-	(void) func;
-	uint32_t argc;
-	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
-	if (argv == NULL)
-		return -1;
-	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
-	assert(ctx != NULL);
-	struct Mem *val = vdbemem_alloc_on_region(1);
-	if (val == NULL)
-		return -1;
-	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
-
-	lua_State *L = lua_newthread(tarantool_L);
-	int coro_ref = luaL_ref(tarantool_L, LUA_REGISTRYINDEX);
-	struct lua_sql_func_info *func_info = sql_user_data(ctx);
-
-	lua_rawgeti(L, LUA_REGISTRYINDEX, func_info->func_ref);
-	for (uint32_t i = 0; i < argc; i++) {
-		sql_value *param = (sql_value *)&argv[i];
-		switch (sql_value_type(param)) {
-		case MP_INT:
-			luaL_pushint64(L, sql_value_int64(param));
-			break;
-		case MP_DOUBLE:
-			lua_pushnumber(L, sql_value_double(param));
-			break;
-		case MP_STR:
-			lua_pushstring(L, (const char *) sql_value_text(param));
-			break;
-		case MP_BIN:
-			lua_pushlstring(L, sql_value_blob(param),
-					(size_t) sql_value_bytes(param));
-			break;
-		case MP_NIL:
-			lua_rawgeti(L, LUA_REGISTRYINDEX, luaL_nil_ref);
-			break;
-		case MP_BOOL:
-			lua_pushboolean(L, sql_value_boolean(param));
-			break;
-		default:
-			diag_set(ClientError, ER_SQL_EXECUTE, "Unsupported "\
-				 "type passed to Lua");
-			goto error;
-		}
-	}
-	if (lua_pcall(L, lua_gettop(L) - 1, 1, 0) != 0){
-		diag_set(ClientError, ER_SQL_EXECUTE, lua_tostring(L, -1));
-		goto error;
-	}
-	switch(lua_type(L, -1)) {
-	case LUA_TBOOLEAN:
-		mem_set_bool(val, lua_toboolean(L, -1));
-		break;
-	case LUA_TNUMBER:
-		sqlVdbeMemSetDouble(val, lua_tonumber(L, -1));
-		break;
-	case LUA_TSTRING:
-		 if (sqlVdbeMemSetStr(val, lua_tostring(L, -1), -1,
-					1, SQL_TRANSIENT) != 0)
-			return -1;
-		break;
-	case LUA_TNIL:
-		sqlVdbeMemSetNull(val);
-		break;
-	default:
-		diag_set(ClientError, ER_SQL_EXECUTE, "Unsupported type "\
-			 "passed from Lua");
-		goto error;
-	}
-	return 0;
-error:
-	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, coro_ref);
-	return -1;
-}
-
-static void
-lua_sql_destroy(void *p)
-{
-	struct lua_sql_func_info *func_info = p;
-	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, func_info->func_ref);
-	free(func_info);
-	return;
-}
-
-/**
- * A helper to register lua function in SQL during runtime.
- * It makes available queries like this: "SELECT lua_func(arg);"
- *
- * sql_create_function *p argument is used to store func ref
- * to lua function (it identifies actual lua func to call if there
- * are many of them). SQL function must have name and type of
- * returning value. Additionally, it can feature number of
- * arguments and deterministic flag.
- */
-int
-lbox_sql_create_function(struct lua_State *L)
-{
-	struct sql *db = sql_get();
-	if (db == NULL)
-		return luaL_error(L, "Please call box.cfg{} first");
-	int argc = lua_gettop(L);
-	/*
-	 * Three function prototypes are possible:
-	 * 1. sql_create_function("func_name", "type", func);
-	 * 2. sql_create_function("func_name", "type", func,
-	 *                        func_arg_num);
-	 * 3. sql_create_function("func_name", "type", func,
-	 *                        func_arg_num, is_deterministic);
-	 */
-	if (!(argc == 3 && lua_isstring(L, 1) && lua_isstring(L, 2) &&
-	    lua_isfunction(L, 3)) &&
-	    !(argc == 4 && lua_isstring(L, 1) && lua_isstring(L, 2) &&
-	      lua_isfunction(L, 3) && lua_isnumber(L, 4)) &&
-	    !(argc == 5 && lua_isstring(L, 1) && lua_isstring(L, 2) &&
-	      lua_isfunction(L, 3) && lua_isnumber(L, 4) &&
-	      lua_isboolean(L, 5)))
-		return luaL_error(L, "Invalid arguments");
-	enum field_type type;
-	const char *type_arg = lua_tostring(L, 2);
-	if (strcmp(type_arg, "INT") == 0 || strcmp(type_arg, "INTEGER") == 0)
-		type = FIELD_TYPE_INTEGER;
-	else if (strcmp(type_arg, "TEXT") == 0)
-		type = FIELD_TYPE_STRING;
-	else if (strcmp(type_arg, "FLOAT") == 0)
-		type = FIELD_TYPE_NUMBER;
-	else if (strcmp(type_arg, "NUM") == 0)
-		type = FIELD_TYPE_NUMBER;
-	else if (strcmp(type_arg, "BLOB") == 0)
-		type = FIELD_TYPE_SCALAR;
-	else if (strcmp(type_arg, "BOOL") == 0 ||
-		 strcmp(type_arg, "BOOLEAN") == 0)
-		type = FIELD_TYPE_BOOLEAN;
-	else
-		return luaL_error(L, "Unknown type");
-	/* -1 indicates any number of arguments. */
-	int func_arg_num = -1;
-	bool is_deterministic = false;
-	if (argc == 4) {
-		func_arg_num = lua_tointeger(L, 4);
-		lua_pop(L, 1);
-	} else if (argc == 5) {
-		is_deterministic = lua_toboolean(L, 5);
-		func_arg_num = lua_tointeger(L, 4);
-		lua_pop(L, 2);
-	}
-	size_t name_len;
-	const char *name = lua_tolstring(L, 1, &name_len);
-	char *normalized_name =
-		sql_normalized_name_region_new(&fiber()->gc, name, name_len);
-	if (normalized_name == NULL)
-		return luaT_error(L);
-	struct lua_sql_func_info *func_info =
-		(struct lua_sql_func_info *) malloc(sizeof(*func_info));
-	if (func_info == NULL)
-		return luaL_error(L, "out of memory");
-	func_info->func_ref = luaL_ref(L, LUA_REGISTRYINDEX);
-	int rc = sql_create_function_v2(db, normalized_name, type, func_arg_num,
-					   is_deterministic ? SQL_DETERMINISTIC : 0,
-					   func_info, lua_sql_call, NULL, NULL,
-					   lua_sql_destroy);
-	if (rc != 0)
-		return luaT_error(L);
-	return 0;
-}
diff --git a/src/box/lua/lua_sql.h b/src/box/lua/lua_sql.h
deleted file mode 100644
index b81093eca..000000000
--- a/src/box/lua/lua_sql.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2010-2017, 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.
- */
-
-#ifndef TARANTOOL_LUA_SQL_H
-#define TARANTOOL_LUA_SQL_H
-
-int
-lbox_sql_create_function(struct lua_State *L);
-
-#endif //TARANTOOL_LUA_SQL_H
-
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index aadcd3fa9..5c65eb792 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -2109,7 +2109,9 @@ box.schema.func.create = function(name, opts)
                               if_not_exists = 'boolean',
                               language = 'string', body = 'string',
                               is_deterministic = 'boolean',
-                              is_sandboxed = 'boolean', comment = 'string' })
+                              is_sandboxed = 'boolean', comment = 'string',
+                              returns = 'string', param_list = 'table',
+                              exports = 'table'})
     local _func = box.space[box.schema.FUNC_ID]
     local _vfunc = box.space[box.schema.VFUNC_ID]
     local func = _vfunc.index.name:get{name}
diff --git a/src/box/port.c b/src/box/port.c
index 9e4ab9453..02c6a2245 100644
--- a/src/box/port.c
+++ b/src/box/port.c
@@ -140,13 +140,16 @@ port_free(void)
 	mempool_destroy(&port_tuple_entry_pool);
 }
 
+extern struct sql_value *
+port_tuple_get_vdbemem(struct port *base, uint32_t *size);
+
 const struct port_vtab port_tuple_vtab = {
 	.dump_msgpack = port_tuple_dump_msgpack,
 	.dump_msgpack_16 = port_tuple_dump_msgpack_16,
 	.dump_lua = port_tuple_dump_lua,
 	.dump_plain = NULL,
 	.get_msgpack = NULL,
-	.get_vdbemem = NULL,
+	.get_vdbemem = port_tuple_get_vdbemem,
 	.get_context = NULL,
 	.destroy = port_tuple_destroy,
 };
diff --git a/src/box/sql.h b/src/box/sql.h
index a078bfdec..ac10ae400 100644
--- a/src/box/sql.h
+++ b/src/box/sql.h
@@ -33,6 +33,7 @@
 
 #include <stdbool.h>
 #include <stdint.h>
+#include "box/func.h"
 
 #if defined(__cplusplus)
 extern "C" {
@@ -409,6 +410,18 @@ vdbe_field_ref_prepare_tuple(struct vdbe_field_ref *field_ref,
 struct func *
 func_sql_builtin_new(struct func_def *def);
 
+struct func_sql_builtin {
+	/** Function object base class. */
+	struct func base;
+	/** User data to pass in call method. */
+	void *user_data;
+	/** Finilize method for aggregate function. */
+	int (*finalize)(struct func *func, struct port *args, struct port *ret);
+};
+
+struct func *
+sql_func_by_signature(const char *name, uint32_t name_len, int argc);
+
 #if defined(__cplusplus)
 } /* extern "C" { */
 #endif
diff --git a/src/box/sql/analyze.c b/src/box/sql/analyze.c
index 7764b48c5..02c922314 100644
--- a/src/box/sql/analyze.c
+++ b/src/box/sql/analyze.c
@@ -264,7 +264,7 @@ stat4Destructor(void *pOld)
  * return value is BLOB, but it is really just a pointer to the Stat4Accum
  * object.
  */
-static int
+MAYBE_UNUSED int
 sql_builtin_stat_init(struct func *func, struct port *args, struct port *ret)
 {
 	(void) func;
@@ -544,7 +544,7 @@ samplePushPrevious(Stat4Accum * p, int iChng)
  *
  * The R parameter is only used for STAT4
  */
-static int
+MAYBE_UNUSED int
 sql_builtin_stat_push(struct func *func, struct port *args, struct port *ret)
 {
 	(void) func;
@@ -623,7 +623,7 @@ sql_builtin_stat_push(struct func *func, struct port *args, struct port *ret)
  * The content to returned is determined by the parameter J
  * which is one of the STAT_GET_xxxx values defined above.
  */
-static int
+MAYBE_UNUSED int
 sql_builtin_stat_get(struct func *func, struct port *args, struct port *ret)
 {
 	(void) func;
@@ -736,11 +736,11 @@ vdbe_emit_analyze_stat_get(struct Vdbe * v, int regStat4, int iParam,
 {
 	assert(regOut != regStat4 && regOut != regStat4 + 1);
 	sqlVdbeAddOp2(v, OP_Integer, iParam, regStat4 + 1);
-	struct FuncDef *func =
-		sqlFindFunction(sql_get(), "_sql_stat_get", 2, 0);
+	struct func *func =
+		sql_func_by_signature("_sql_stat_get", strlen("_sql_stat_get"), 2);
 	assert(func != NULL);
 	sqlVdbeAddOp4(v, OP_Function0, 0, regStat4, regOut,
-		      (char *)func, P4_FUNCDEF);
+		      (char *)func, P4_FUNC);
 	sqlVdbeChangeP5(v, 2);
 }
 
@@ -876,11 +876,12 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
 		sqlVdbeAddOp2(v, OP_Count, idx_cursor, stat4_reg + 3);
 		sqlVdbeAddOp2(v, OP_Integer, part_count, stat4_reg + 1);
 		sqlVdbeAddOp2(v, OP_Integer, part_count, stat4_reg + 2);
-		struct FuncDef *init_func =
-			sqlFindFunction(sql_get(), "_sql_stat_init", 3, 0);
+		struct func *init_func =
+			sql_func_by_signature("_sql_stat_init",
+					  strlen("_sql_stat_init"), 3);
 		assert(init_func != NULL);
 		sqlVdbeAddOp4(v, OP_Function0, 0, stat4_reg + 1, stat4_reg,
-			      (char *)init_func, P4_FUNCDEF);
+			      (char *)init_func, P4_FUNC);
 		sqlVdbeChangeP5(v, 3);
 		/*
 		 * Implementation of the following:
@@ -977,11 +978,12 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
 		sqlVdbeAddOp3(v, OP_MakeRecord, stat_key_reg,
 				  pk_part_count, key_reg);
 		assert(chng_reg == (stat4_reg + 1));
-		struct FuncDef *push_func =
-			sqlFindFunction(sql_get(), "_sql_stat_push", 3, 0);
+		struct func *push_func =
+			sql_func_by_signature("_sql_stat_push",
+					  strlen("_sql_stat_push"), 3);
 		assert(push_func != NULL);
 		sqlVdbeAddOp4(v, OP_Function0, 1, stat4_reg, tmp_reg,
-			      (char *)push_func, P4_FUNCDEF);
+			      (char *)push_func, P4_FUNC);
 		sqlVdbeChangeP5(v, 3);
 		sqlVdbeAddOp2(v, OP_Next, idx_cursor, next_row_addr);
 		/* Add the entry to the stat1 table. */
@@ -1768,17 +1770,3 @@ fail:
 	box_txn_rollback();
 	return -1;
 }
-
-void
-sql_register_analyze_builtins(void)
-{
-	static FuncDef funcs[] = {
-		FUNCTION(_sql_stat_get, 2, 0, 0, sql_builtin_stat_get,
-			 FIELD_TYPE_ANY),
-		FUNCTION(_sql_stat_push, 3, 0, 0, sql_builtin_stat_push,
-			 FIELD_TYPE_ANY),
-		FUNCTION(_sql_stat_init, 3, 0, 0, sql_builtin_stat_init,
-			 FIELD_TYPE_ANY),
-	};
-	sqlInsertBuiltinFuncs(funcs, nelem(funcs));
-}
diff --git a/src/box/sql/callback.c b/src/box/sql/callback.c
index 42fec2c6a..290363db6 100644
--- a/src/box/sql/callback.c
+++ b/src/box/sql/callback.c
@@ -56,211 +56,3 @@ sql_get_coll_seq(Parse *parser, const char *name, uint32_t *coll_id)
 		return p->coll;
 	}
 }
-
-/* During the search for the best function definition, this procedure
- * is called to test how well the function passed as the first argument
- * matches the request for a function with nArg arguments in a system
- * that uses encoding enc. The value returned indicates how well the
- * request is matched. A higher value indicates a better match.
- *
- * If nArg is -1 that means to only return a match (non-zero) if p->nArg
- * is also -1.  In other words, we are searching for a function that
- * takes a variable number of arguments.
- *
- * If nArg is -2 that means that we are searching for any function
- * regardless of the number of arguments it uses, so return a positive
- * match score for any
- *
- * The returned value is always between 0 and 6, as follows:
- *
- * 0: Not a match.
- * 1: UTF8/16 conversion required and function takes any number of arguments.
- * 2: UTF16 byte order change required and function takes any number of args.
- * 3: encoding matches and function takes any number of arguments
- * 4: UTF8/16 conversion required - argument count matches exactly
- * 5: UTF16 byte order conversion required - argument count matches exactly
- * 6: Perfect match:  encoding and argument count match exactly.
- *
- * If nArg==(-2) then any function with a non-null xSFunc is
- * a perfect match and any function with xSFunc NULL is
- * a non-match.
- */
-#define FUNC_PERFECT_MATCH 4	/* The score for a perfect match */
-static int
-matchQuality(FuncDef * p,	/* The function we are evaluating for match quality */
-	     int nArg		/* Desired number of arguments.  (-1)==any */
-    )
-{
-	int match;
-
-	/* nArg of -2 is a special case */
-	if (nArg == (-2))
-		return (p->xSFunc == 0) ? 0 : FUNC_PERFECT_MATCH;
-
-	/* Wrong number of arguments means "no match" */
-	if (p->nArg != nArg && p->nArg >= 0)
-		return 0;
-
-	/* Give a better score to a function with a specific number of arguments
-	 * than to function that accepts any number of arguments.
-	 */
-	if (p->nArg == nArg) {
-		match = 4;
-	} else {
-		match = 1;
-	}
-
-	return match;
-}
-
-/*
- * Search a FuncDefHash for a function with the given name.  Return
- * a pointer to the matching FuncDef if found, or 0 if there is no match.
- */
-static FuncDef *
-functionSearch(int h,		/* Hash of the name */
-	       const char *zFunc	/* Name of function */
-    )
-{
-	FuncDef *p;
-	for (p = sqlBuiltinFunctions.a[h]; p; p = p->u.pHash) {
-		if (sqlStrICmp(p->zName, zFunc) == 0) {
-			return p;
-		}
-	}
-	return 0;
-}
-
-/*
- * Insert a new FuncDef into a FuncDefHash hash table.
- */
-void
-sqlInsertBuiltinFuncs(FuncDef * aDef,	/* List of global functions to be inserted */
-			  int nDef	/* Length of the apDef[] list */
-    )
-{
-	int i;
-	for (i = 0; i < nDef; i++) {
-		FuncDef *pOther;
-		const char *zName = aDef[i].zName;
-		int nName = sqlStrlen30(zName);
-		int h =
-		    (sqlUpperToLower[(u8) zName[0]] +
-		     nName) % SQL_FUNC_HASH_SZ;
-		pOther = functionSearch(h, zName);
-		if (pOther) {
-			assert(pOther != &aDef[i] && pOther->pNext != &aDef[i]);
-			aDef[i].pNext = pOther->pNext;
-			pOther->pNext = &aDef[i];
-		} else {
-			aDef[i].pNext = 0;
-			aDef[i].u.pHash = sqlBuiltinFunctions.a[h];
-			sqlBuiltinFunctions.a[h] = &aDef[i];
-		}
-	}
-}
-
-/*
- * Locate a user function given a name, a number of arguments and a flag
- * indicating whether the function prefers UTF-16 over UTF-8.  Return a
- * pointer to the FuncDef structure that defines that function, or return
- * NULL if the function does not exist.
- *
- * If the createFlag argument is true, then a new (blank) FuncDef
- * structure is created and liked into the "db" structure if a
- * no matching function previously existed.
- *
- * If nArg is -2, then the first valid function found is returned.  A
- * function is valid if xSFunc is non-zero.  The nArg==(-2)
- * case is used to see if zName is a valid function name for some number
- * of arguments.  If nArg is -2, then createFlag must be 0.
- *
- * If createFlag is false, then a function with the required name and
- * number of arguments may be returned even if the eTextRep flag does not
- * match that requested.
- */
-FuncDef *
-sqlFindFunction(sql * db,	/* An open database */
-		    const char *zName,	/* Name of the function.  zero-terminated */
-		    int nArg,	/* Number of arguments.  -1 means any number */
-		    u8 createFlag	/* Create new entry if true and does not otherwise exist */
-    )
-{
-	FuncDef *p;		/* Iterator variable */
-	FuncDef *pBest = 0;	/* Best match found so far */
-	int bestScore = 0;	/* Score of best match */
-	int h;			/* Hash value */
-	int nName;		/* Length of the name */
-
-	assert(nArg >= (-2));
-	assert(nArg >= (-1) || createFlag == 0);
-	nName = sqlStrlen30(zName);
-
-	/* First search for a match amongst the application-defined functions.
-	 */
-	p = (FuncDef *) sqlHashFind(&db->aFunc, zName);
-	while (p) {
-		int score = matchQuality(p, nArg);
-		if (score > bestScore) {
-			pBest = p;
-			bestScore = score;
-		}
-		p = p->pNext;
-	}
-
-	/* If no match is found, search the built-in functions.
-	 *
-	 * If the SQL_PreferBuiltin flag is set, then search the built-in
-	 * functions even if a prior app-defined function was found.  And give
-	 * priority to built-in functions.
-	 *
-	 * Except, if createFlag is true, that means that we are trying to
-	 * install a new function.  Whatever FuncDef structure is returned it will
-	 * have fields overwritten with new information appropriate for the
-	 * new function.  But the FuncDefs for built-in functions are read-only.
-	 * So we must not search for built-ins when creating a new function.
-	 */
-	if (!createFlag && (pBest == NULL)) {
-		bestScore = 0;
-		h = (sqlUpperToLower[(u8) zName[0]] +
-		     nName) % SQL_FUNC_HASH_SZ;
-		p = functionSearch(h, zName);
-		while (p) {
-			int score = matchQuality(p, nArg);
-			if (score > bestScore) {
-				pBest = p;
-				bestScore = score;
-			}
-			p = p->pNext;
-		}
-	}
-
-	/* If the createFlag parameter is true and the search did not reveal an
-	 * exact match for the name, number of arguments and encoding, then add a
-	 * new entry to the hash table and return it.
-	 */
-	if (createFlag && bestScore < FUNC_PERFECT_MATCH &&
-	    (pBest =
-	     sqlDbMallocZero(db, sizeof(*pBest) + nName + 1)) != 0) {
-		FuncDef *pOther;
-		pBest->zName = (const char *)&pBest[1];
-		pBest->nArg = (u16) nArg;
-		pBest->funcFlags = 0;
-		memcpy((char *)&pBest[1], zName, nName + 1);
-		pOther =
-		    (FuncDef *) sqlHashInsert(&db->aFunc, pBest->zName,
-						  pBest);
-		if (pOther == pBest) {
-			sqlDbFree(db, pBest);
-			sqlOomFault(db);
-			return 0;
-		} else {
-			pBest->pNext = pOther;
-		}
-	}
-
-	if (pBest && (pBest->xSFunc || createFlag)) {
-		return pBest;
-	}
-	return 0;
-}
diff --git a/src/box/sql/date.c b/src/box/sql/date.c
index 2e2a71ad2..dffc23616 100644
--- a/src/box/sql/date.c
+++ b/src/box/sql/date.c
@@ -1290,31 +1290,3 @@ currentTimeFunc(sql_context * context, int argc, sql_value ** argv)
 	}
 }
 #endif
-
-/*
- * This function registered all of the above C functions as SQL
- * functions.  This should be the only routine in this file with
- * external linkage.
- */
-void
-sqlRegisterDateTimeFunctions(void)
-{
-	static FuncDef aDateTimeFuncs[] = {
-#if 0
-		DFUNCTION(julianday, -1, 0, 0, juliandayFunc, FIELD_TYPE_NUMBER),
-		DFUNCTION(date, -1, 0, 0, dateFunc, FIELD_TYPE_STRING),
-		DFUNCTION(time, -1, 0, 0, timeFunc, FIELD_TYPE_STRING),
-		DFUNCTION(datetime, -1, 0, 0, datetimeFunc, FIELD_TYPE_STRING),
-		DFUNCTION(strftime, -1, 0, 0, strftimeFunc, FIELD_TYPE_STRING),
-		DFUNCTION(current_time, 0, 0, 0, ctimeFunc, FIELD_TYPE_STRING),
-		DFUNCTION(current_timestamp, 0, 0, 0, ctimestampFunc,
-			  FIELD_TYPE_STRING),
-		DFUNCTION(current_date, 0, 0, 0, cdateFunc, FIELD_TYPE_STRING),
-		STR_FUNCTION(current_time, 0, "%H:%M:%S", 0, currentTimeFunc),
-		STR_FUNCTION(current_date, 0, "%Y-%m-%d", 0, currentTimeFunc),
-		STR_FUNCTION(current_timestamp, 0, "%Y-%m-%d %H:%M:%S", 0,
-			     currentTimeFunc),
-#endif
-	};
-	sqlInsertBuiltinFuncs(aDateTimeFuncs, ArraySize(aDateTimeFuncs));
-}
diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
index 7ee3e992e..003e49840 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -35,6 +35,8 @@
  */
 #include "box/coll_id_cache.h"
 #include "coll/coll.h"
+#include "box/func.h"
+#include "box/field_def.h"
 #include "sqlInt.h"
 #include "tarantoolInt.h"
 #include "box/schema.h"
@@ -275,12 +277,13 @@ sql_expr_coll(Parse *parse, Expr *p, bool *is_explicit_coll, uint32_t *coll_id,
 		if (op == TK_FUNCTION) {
 			uint32_t arg_count = p->x.pList == NULL ? 0 :
 					     p->x.pList->nExpr;
-			struct FuncDef *func = sqlFindFunction(parse->db,
-							       p->u.zToken,
-							       arg_count, 0);
+			struct func *func =
+				sql_func_by_signature(p->u.zToken,
+						strlen(p->u.zToken), arg_count);
 			if (func == NULL)
 				break;
-			if ((func->funcFlags & SQL_FUNC_DERIVEDCOLL) != 0) {
+			if ((func->def->sql_flags &
+			     SQL_FUNC_DERIVEDCOLL) != 0) {
 				/*
 				 * Now we use quite straightforward
 				 * approach assuming that resulting
@@ -289,7 +292,7 @@ sql_expr_coll(Parse *parse, Expr *p, bool *is_explicit_coll, uint32_t *coll_id,
 				 * built-in functions: trim, upper,
 				 * lower, replace, substr.
 				 */
-				assert(func->ret_type == FIELD_TYPE_STRING);
+				assert(func->def->returns == FIELD_TYPE_STRING);
 				p = p->x.pList->a->pExpr;
 				continue;
 			}
@@ -3923,11 +3926,9 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 	case TK_FUNCTION:{
 			ExprList *pFarg;	/* List of function arguments */
 			int nFarg;	/* Number of function arguments */
-			FuncDef *pDef;	/* The function definition object */
 			const char *zId;	/* The function name */
 			u32 constMask = 0;	/* Mask of function arguments that are constant */
 			int i;	/* Loop counter */
-			sql *db = pParse->db;	/* The database connection */
 			struct coll *coll = NULL;
 
 			assert(!ExprHasProperty(pExpr, EP_xIsSelect));
@@ -3939,8 +3940,10 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 			nFarg = pFarg ? pFarg->nExpr : 0;
 			assert(!ExprHasProperty(pExpr, EP_IntValue));
 			zId = pExpr->u.zToken;
-			pDef = sqlFindFunction(db, zId, nFarg, 0);
-			if (pDef == 0 || pDef->xFinalize != 0) {
+			struct func *func =
+				sql_func_by_signature(zId, strlen(zId), nFarg);
+			if (func == NULL ||
+			    func->def->aggregate == FUNC_AGGREGATE_GROUP) {
 				diag_set(ClientError, ER_NO_SUCH_FUNCTION,
 					 zId);
 				pParse->is_aborted = true;
@@ -3950,7 +3953,7 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 			 * IFNULL() functions.  This avoids unnecessary evaluation of
 			 * arguments past the first non-NULL argument.
 			 */
-			if (pDef->funcFlags & SQL_FUNC_COALESCE) {
+			if (func->def->sql_flags & SQL_FUNC_COALESCE) {
 				int endCoalesce = sqlVdbeMakeLabel(v);
 				assert(nFarg >= 2);
 				sqlExprCode(pParse, pFarg->a[0].pExpr,
@@ -3987,7 +3990,7 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 			 * is done using ANSI rules from
 			 * collations_check_compatibility().
 			 */
-			if ((pDef->funcFlags & SQL_FUNC_NEEDCOLL) != 0) {
+			if ((func->def->sql_flags & SQL_FUNC_NEEDCOLL) != 0) {
 				struct coll *unused = NULL;
 				uint32_t curr_id = COLL_NONE;
 				bool is_curr_forced = false;
@@ -4034,9 +4037,8 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 				 * or OPFLAG_TYPEOFARG respectively, to avoid unnecessary data
 				 * loading.
 				 */
-				if ((pDef->
-				     funcFlags & (SQL_FUNC_LENGTH |
-						  SQL_FUNC_TYPEOF)) != 0) {
+				if ((func->def->sql_flags & (SQL_FUNC_LENGTH |
+						SQL_FUNC_TYPEOF)) != 0) {
 					u8 exprOp;
 					assert(nFarg == 1);
 					assert(pFarg->a[0].pExpr != 0);
@@ -4051,8 +4053,7 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 							 funcFlags &
 							 OPFLAG_LENGTHARG);
 						pFarg->a[0].pExpr->op2 =
-						    pDef->
-						    funcFlags &
+						    func->def->sql_flags &
 						    (OPFLAG_LENGTHARG |
 						     OPFLAG_TYPEOFARG);
 					}
@@ -4066,12 +4067,12 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 			} else {
 				r1 = 0;
 			}
-			if (pDef->funcFlags & SQL_FUNC_NEEDCOLL) {
+			if (func->def->sql_flags & SQL_FUNC_NEEDCOLL) {
 				sqlVdbeAddOp4(v, OP_CollSeq, 0, 0, 0,
 						  (char *)coll, P4_COLLSEQ);
 			}
 			sqlVdbeAddOp4(v, OP_Function0, constMask, r1,
-					  target, (char *)pDef, P4_FUNCDEF);
+				      target, (char *)func, P4_FUNC);
 			sqlVdbeChangeP5(v, (u8) nFarg);
 			if (nFarg && constMask == 0) {
 				sqlReleaseTempRange(pParse, r1, nFarg);
@@ -5376,12 +5377,18 @@ analyzeAggregate(Walker * pWalker, Expr * pExpr)
 						pItem->iMem = ++pParse->nMem;
 						assert(!ExprHasProperty
 						       (pExpr, EP_IntValue));
-						pItem->pFunc = sqlFindFunction(
-							pParse->db,
+						pItem->func = sql_func_by_signature(
 							pExpr->u.zToken,
+							strlen(pExpr->u.zToken),
 							pExpr->x.pList ?
-							pExpr->x.pList->nExpr : 0,
-							0);
+							pExpr->x.pList->
+							nExpr : 0);
+						assert(pItem->func->def->
+							language ==
+							FUNC_LANGUAGE_SQL_BUILTIN &&
+							pItem->func->def->
+							aggregate ==
+							FUNC_AGGREGATE_GROUP);
 						if (pExpr->flags & EP_Distinct) {
 							pItem->iDistinct =
 								pParse->nTab++;
diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index f79939cb6..e35b5c2a2 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -39,14 +39,19 @@
 #include "version.h"
 #include "coll/coll.h"
 #include "box/func.h"
+#include "lua/utils.h"
+#include "box/tuple.h"
+#include "mpstream.h"
 #include "box/port.h"
 #include "tarantoolInt.h"
 #include "box/session.h"
+#include "box/schema.h"
 #include <unicode/ustring.h>
 #include <unicode/ucasemap.h>
 #include <unicode/ucnv.h>
 #include <unicode/uchar.h>
 #include <unicode/ucol.h>
+#include "small/rlist.h"
 
 /*
  * Return the collating function associated with a function.
@@ -101,17 +106,222 @@ port_vdbemem_get_context(struct port *base)
 	return port->ctx;
 }
 
+void
+port_vdbemem_dump_lua(struct port *base, struct lua_State *L, bool is_flat)
+{
+	(void) is_flat;
+	struct port_vdbemem *port = (struct port_vdbemem *) base;
+	assert(is_flat == true);
+	for (uint32_t i = 0; i < port->size; i++) {
+		sql_value *param =
+			(sql_value *)((struct Mem *)port->mem + i);
+		switch (sql_value_type(param)) {
+		case MP_INT:
+			luaL_pushint64(L, sql_value_int64(param));
+			break;
+		case MP_DOUBLE:
+			lua_pushnumber(L, sql_value_double(param));
+			break;
+		case MP_STR:
+			lua_pushstring(L, (const char *) sql_value_text(param));
+			break;
+		case MP_BIN:
+			lua_pushlstring(L, sql_value_blob(param),
+				(size_t) sql_value_bytes(param));
+			break;
+		case MP_NIL:
+			lua_pushnil(L);
+			break;
+		case MP_BOOL:
+			lua_pushboolean(L, sql_value_boolean(param));
+			break;
+		default:
+			unreachable();
+		}
+	}
+}
+
+const char *
+port_vdbemem_get_msgpack(struct port *base, uint32_t *size)
+{
+	struct port_vdbemem *port = (struct port_vdbemem *) base;
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	bool is_error = false;
+	struct mpstream stream;
+	mpstream_init(&stream, region, region_reserve_cb, region_alloc_cb,
+		      set_encode_error, &is_error);
+	mpstream_encode_array(&stream, port->size);
+
+	for (uint32_t i = 0; i < port->size && !is_error; i++) {
+		sql_value *param =
+			(sql_value *)((struct Mem *)port->mem + i);
+		switch (sql_value_type(param)) {
+		case MP_INT: {
+			sql_int64 val = sql_value_int64(param);
+			if (val < 0)
+				mpstream_encode_int(&stream, val);
+			else
+				mpstream_encode_uint(&stream, val);
+			break;
+		}
+		case MP_DOUBLE: {
+			mpstream_encode_double(&stream,
+					       sql_value_double(param));
+			break;
+		}
+		case MP_STR: {
+			mpstream_encode_str(&stream,
+					(const char *) sql_value_text(param));
+			break;
+		}
+		case MP_BIN: {
+			mpstream_encode_binl(&stream, sql_value_bytes(param));
+			mpstream_memcpy(&stream, sql_value_blob(param),
+					sql_value_bytes(param));
+			break;
+		}
+		case MP_NIL: {
+			mpstream_encode_nil(&stream);
+			break;
+		}
+		case MP_BOOL: {
+			mpstream_encode_bool(&stream, sql_value_boolean(param));
+			break;
+		}
+		default:
+			unreachable();
+		}
+	}
+	mpstream_flush(&stream);
+	*size = region_used(region) - region_svp;
+	if (is_error)
+		goto error;
+	const char *ret = (char *)region_join(region, *size);
+	if (ret == NULL)
+		goto error;
+	return ret;
+error:
+	diag_set(OutOfMemory, *size, "region", "ret");
+	return NULL;
+}
+
 static const struct port_vtab port_vdbemem_vtab = {
 	.dump_msgpack = NULL,
 	.dump_msgpack_16 = NULL,
-	.dump_lua = NULL,
+	.dump_lua = port_vdbemem_dump_lua,
 	.dump_plain = NULL,
-	.get_msgpack = NULL,
+	.get_msgpack = port_vdbemem_get_msgpack,
 	.get_vdbemem = port_vdbemem_get_vdbemem,
 	.get_context = port_vdbemem_get_context,
 	.destroy = NULL,
 };
 
+
+struct sql_value *
+port_lua_get_vdbemem(struct port *base, uint32_t *size)
+{
+	struct port_lua *port = (struct port_lua *) base;
+	struct lua_State *L = port->L;
+	int argc = lua_gettop(L);
+	*size = argc;
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	struct Mem *val = vdbemem_alloc_on_region(argc);
+	if (val == NULL)
+		return NULL;
+	for (int i = 0; i < argc; i++) {
+		switch(lua_type(L, -1 - i)) {
+		case LUA_TBOOLEAN:
+			mem_set_bool(&val[i], lua_toboolean(L, -1 - i));
+			break;
+		case LUA_TNUMBER:
+			sqlVdbeMemSetDouble(&val[i], lua_tonumber(L, -1 - i));
+			break;
+		case LUA_TSTRING:
+			if (sqlVdbeMemSetStr(&val[i], lua_tostring(L, -1 - i), -1,
+					     1, SQL_TRANSIENT) != 0)
+				goto error;
+			break;
+		case LUA_TNIL:
+			sqlVdbeMemSetNull(&val[i]);
+			break;
+		default:
+			diag_set(ClientError, ER_SQL_EXECUTE,
+				 "Unsupported type passed from Lua");
+			goto error;
+		}
+	}
+	return (struct sql_value *)val;
+error:
+	for (int i = 0; i < argc; i++)
+		sqlVdbeMemRelease(&val[i]);
+	region_truncate(region, region_svp);
+	return NULL;
+}
+
+struct sql_value *
+port_tuple_get_vdbemem(struct port *base, uint32_t *size)
+{
+	struct port_tuple *port = (struct port_tuple *)base;
+	*size = port->size;
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	struct Mem *val = vdbemem_alloc_on_region(port->size);
+	if (val == NULL)
+		return NULL;
+	int i = 0;
+	struct port_tuple_entry *pe;
+	for (pe = port->first; pe != NULL; pe = pe->next) {
+		const char *data = tuple_data(pe->tuple);
+		if (mp_typeof(*data) != MP_ARRAY ||
+		    mp_decode_array(&data) != 1) {
+			diag_set(ClientError, ER_SQL_EXECUTE,
+				 "Unsupported type passed from C");
+			goto error;
+		}
+		uint32_t len;
+		const char *str;
+		switch (mp_typeof(*data)) {
+		case MP_BOOL:
+			mem_set_bool(&val[i], mp_decode_bool(&data));
+			break;
+		case MP_FLOAT:
+			sqlVdbeMemSetDouble(&val[i], mp_decode_float(&data));
+			break;
+		case MP_DOUBLE:
+			sqlVdbeMemSetDouble(&val[i], mp_decode_double(&data));
+			break;
+		case MP_INT:
+			sqlVdbeMemSetInt64(val, mp_decode_int(&data));
+			break;
+		case MP_UINT:
+			sqlVdbeMemSetInt64(val, mp_decode_uint(&data));
+			break;
+		case MP_STR:
+			str = mp_decode_str(&data, &len);
+			if (sqlVdbeMemSetStr(&val[i], str, len,
+					     1, SQL_TRANSIENT) != 0)
+				goto error;
+			break;
+		case MP_NIL:
+			sqlVdbeMemSetNull(val);
+			break;
+		default:
+			diag_set(ClientError, ER_SQL_EXECUTE,
+				 "Unsupported type passed from C");
+			goto error;
+		}
+		i++;
+	}
+	return (struct sql_value *) val;
+error:
+	for (int i = 0; i < port->size; i++)
+		sqlVdbeMemRelease(&val[i]);
+	region_truncate(region, region_svp);
+	return NULL;
+}
+
 /*
  * Implementation of the non-aggregate min() and max() functions
  */
@@ -714,19 +924,6 @@ sql_builtin_ICU##case_type(struct func *func, struct port *args,               \
 ICU_CASE_CONVERT(Lower);
 ICU_CASE_CONVERT(Upper);
 
-
-/*
- * Some functions like COALESCE() and IFNULL() are implemented
- * as VDBE code so that unused argument values do not have to be
- * computed.
- * However, we still need some kind of function implementation for
- * this routines in the function table. The sql_builtin_noop macro
- * provides this. sql_builtin_noop will never be called so it
- * doesn't matter what the implementation is. We might as well
- * use the "version()" function as a substitute.
- */
-#define sql_builtin_noop sql_builtin_version
-
 /*
  * Implementation of random().  Return a random integer.
  */
@@ -2028,142 +2225,302 @@ sql_builtin_group_concat_finalize(struct func *func, struct port *args,
 }
 
 int
-sql_is_like_func(struct sql *db, struct Expr *expr, int *is_like_ci)
+sql_is_like_func(struct Expr *expr, int *is_like_ci)
 {
 	if (expr->op != TK_FUNCTION || !expr->x.pList ||
 	    expr->x.pList->nExpr != 2)
 		return 0;
 	assert(!ExprHasProperty(expr, EP_xIsSelect));
-	struct FuncDef *func = sqlFindFunction(db, expr->u.zToken, 2, 0);
+	struct func *func =
+		sql_func_by_signature(expr->u.zToken, strlen(expr->u.zToken), 2);
 	assert(func != NULL);
-	if ((func->funcFlags & SQL_FUNC_LIKE) == 0)
+	if ((func->def->sql_flags & SQL_FUNC_LIKE) == 0)
 		return 0;
 	*is_like_ci =
 		(current_session()->sql_flags & LIKE_CASE_SENS_FLAG) == 0;
 	return 1;
 }
 
-/*
- * All of the FuncDef structures in the aBuiltinFunc[] array above
- * to the global function hash table.  This occurs at start-time (as
- * a consequence of calling sql_initialize()).
- *
- * After this routine runs
- */
-void
-sqlRegisterBuiltinFunctions(void)
+static struct func_vtab func_sql_builtin_vtab;
+
+struct func *
+sql_func_by_signature(const char *name, uint32_t name_len, int argc)
 {
-	/*
-	 * The following array holds FuncDef structures for all of the functions
-	 * defined in this file.
-	 *
-	 * The array cannot be constant since changes are made to the
-	 * FuncDef.pHash elements at start-time.  The elements of this array
-	 * are read-only after initialization is complete.
-	 *
-	 * For peak efficiency, put the most frequently used function last.
-	 */
-	static FuncDef aBuiltinFunc[] = {
-		FUNCTION_COLL(trim, 1, 3, 0, sql_builtin_trim_one_arg),
-		FUNCTION_COLL(trim, 2, 3, 0, sql_builtin_trim_two_args),
-		FUNCTION_COLL(trim, 3, 3, 0, sql_builtin_trim_three_args),
-		FUNCTION(min, -1, 0, 1, sql_builtin_minmax, FIELD_TYPE_SCALAR),
-		FUNCTION(min, 0, 0, 1, 0, FIELD_TYPE_SCALAR),
-		AGGREGATE2(min, 1, 0, 1, sql_builtin_minmax_step,
-			   sql_builtin_minmax_finalize, SQL_FUNC_MINMAX,
-			   FIELD_TYPE_SCALAR),
-		FUNCTION(max, -1, 1, 1, sql_builtin_minmax, FIELD_TYPE_SCALAR),
-		FUNCTION(max, 0, 1, 1, 0, FIELD_TYPE_SCALAR),
-		AGGREGATE2(max, 1, 1, 1, sql_builtin_minmax_step,
-			   sql_builtin_minmax_finalize, SQL_FUNC_MINMAX,
-			   FIELD_TYPE_SCALAR),
-		FUNCTION2(typeof, 1, 0, 0, sql_builtin_typeof, SQL_FUNC_TYPEOF,
-			  FIELD_TYPE_STRING),
-		FUNCTION2(length, 1, 0, 0, sql_builtin_length, SQL_FUNC_LENGTH,
-			  FIELD_TYPE_INTEGER),
-		FUNCTION(position, 2, 0, 1, sql_builtin_position,
-			 FIELD_TYPE_INTEGER),
-		FUNCTION(printf, -1, 0, 0, sql_builtin_printf,
-			 FIELD_TYPE_STRING),
-		FUNCTION(unicode, 1, 0, 0, sql_builtin_unicode,
-			 FIELD_TYPE_STRING),
-		FUNCTION(char, -1, 0, 0, sql_builtin_char, FIELD_TYPE_STRING),
-		FUNCTION(abs, 1, 0, 0, sql_builtin_abs, FIELD_TYPE_NUMBER),
-		FUNCTION(round, 1, 0, 0, sql_builtin_round, FIELD_TYPE_INTEGER),
-		FUNCTION(round, 2, 0, 0, sql_builtin_round, FIELD_TYPE_INTEGER),
-		FUNCTION_COLL(upper, 1, 0, 1, sql_builtin_ICUUpper),
-		FUNCTION_COLL(lower, 1, 0, 1, sql_builtin_ICULower),
-		FUNCTION(hex, 1, 0, 0, sql_builtin_hex, FIELD_TYPE_STRING),
-		FUNCTION2(ifnull, 2, 0, 0, sql_builtin_noop, SQL_FUNC_COALESCE,
-			  FIELD_TYPE_INTEGER),
-		VFUNCTION(random, 0, 0, 0, sql_builtin_random,
-			  FIELD_TYPE_INTEGER),
-		VFUNCTION(randomblob, 1, 0, 0, sql_builtin_random_blob,
-			  FIELD_TYPE_SCALAR),
-		FUNCTION(nullif, 2, 0, 1, sql_builtin_nullif,
-			 FIELD_TYPE_SCALAR),
-		FUNCTION(version, 0, 0, 0, sql_builtin_version,
-			 FIELD_TYPE_STRING),
-		FUNCTION(quote, 1, 0, 0, sql_builtin_quote, FIELD_TYPE_STRING),
-		VFUNCTION(row_count, 0, 0, 0, sql_builtin_row_count,
-			  FIELD_TYPE_INTEGER),
-		FUNCTION_COLL(replace, 3, 0, 0, sql_builtin_replace),
-		FUNCTION(zeroblob, 1, 0, 0, sql_builtin_zeroblob,
-			 FIELD_TYPE_SCALAR),
-		FUNCTION_COLL(substr, 2, 0, 0, sql_builtin_substr),
-		FUNCTION_COLL(substr, 3, 0, 0, sql_builtin_substr),
-		AGGREGATE(sum, 1, 0, 0, sql_builtin_sum_step,
-			  sql_builtin_sum_finalize, FIELD_TYPE_NUMBER),
-		AGGREGATE(total, 1, 0, 0, sql_builtin_sum_step,
-			  sql_builtin_total_finalize, FIELD_TYPE_NUMBER),
-		AGGREGATE(avg, 1, 0, 0, sql_builtin_sum_step,
-			  sql_builtin_avg_finalize, FIELD_TYPE_NUMBER),
-		AGGREGATE2(count, 0, 0, 0, sql_builtin_count_step,
-			   sql_builtin_count_finalize, SQL_FUNC_COUNT,
-			   FIELD_TYPE_INTEGER),
-		AGGREGATE(count, 1, 0, 0, sql_builtin_count_step,
-			  sql_builtin_count_finalize, FIELD_TYPE_INTEGER),
-		AGGREGATE(group_concat, 1, 0, 0, sql_builtin_group_concat_step,
-			  sql_builtin_group_concat_finalize, FIELD_TYPE_STRING),
-		AGGREGATE(group_concat, 2, 0, 0, sql_builtin_group_concat_step,
-			  sql_builtin_group_concat_finalize, FIELD_TYPE_STRING),
-		FUNCTION2(like, 2, 1, 0, sql_builtin_like, SQL_FUNC_LIKE,
-			  FIELD_TYPE_INTEGER),
-		FUNCTION2(like, 3, 1, 0, sql_builtin_like, SQL_FUNC_LIKE,
-			  FIELD_TYPE_INTEGER),
-		FUNCTION(coalesce, 1, 0, 0, 0, FIELD_TYPE_SCALAR),
-		FUNCTION(coalesce, 0, 0, 0, 0, FIELD_TYPE_SCALAR),
-		FUNCTION2(coalesce, -1, 0, 0, sql_builtin_noop,
-			  SQL_FUNC_COALESCE, FIELD_TYPE_SCALAR),
-	};
-	sql_register_analyze_builtins();
-	sqlRegisterDateTimeFunctions();
-	sqlInsertBuiltinFuncs(aBuiltinFunc, ArraySize(aBuiltinFunc));
-
-#if 0				/* Enable to print out how the built-in functions are hashed */
-	{
-		int i;
-		FuncDef *p;
-		for (i = 0; i < SQL_FUNC_HASH_SZ; i++) {
-			printf("FUNC-HASH %02d:", i);
-			for (p = sqlBuiltinFunctions.a[i]; p;
-			     p = p->u.pHash) {
-				int n = sqlStrlen30(p->zName);
-				int h = p->zName[0] + n;
-				printf(" %s(%d)", p->zName, h);
-			}
-			printf("\n");
+	struct func *ret = NULL;
+	struct func *main_func = func_by_name(name, name_len);
+	if (main_func == NULL || argc == -2)
+		return main_func;
+	if (main_func->def->param_count == argc) {
+		ret = main_func;
+		goto end;
+	}
+	ret = main_func->def->param_count < 0 ? main_func : NULL;
+	struct func *func;
+	rlist_foreach_entry(func, &main_func->signature, signature) {
+		/** Exact match. */
+		if (func->def->param_count == argc) {
+			ret = func;
+			goto end;
 		}
+		/** Multiparam function. */
+		if (func->def->param_count < 0)
+			ret = func;
 	}
-#endif
+end:
+	return ret != NULL && ret->def->exports.sql ? ret : NULL;
 }
 
-struct func_sql_builtin {
-	/** Function object base class. */
-	struct func base;
+static int
+sql_builtin_stub(struct func *func, struct port *args, struct port *ret)
+{
+	(void) func; (void) args; (void) ret;
+	struct Mem *val = vdbemem_alloc_on_region(1);
+	if (val == NULL)
+		return -1;
+	diag_set(ClientError, ER_SQL_EXECUTE, "function is not implemented");
+	return -1;
+}
+
+struct sql_builtin_entry {
+	int param_count;
+	enum field_type returns;
+	uint16_t sql_flags;
+	int (*call)(struct func *func, struct port *args, struct port *ret);
+	int (*finalize)(struct func *func, struct port *args, struct port *ret);
+	void *user_data;
+	enum func_aggregate aggregate;
+	bool is_deterministic;
+	bool is_available;
 };
 
-static struct func_vtab func_sql_builtin_vtab;
+#define BUILTIN_FUNCTION_ENTRY(param_count, returns, flags, call,\
+			       user_data, is_deterministic,      \
+			       is_available)                     \
+	{param_count, returns, flags, call, NULL, user_data,     \
+	 FUNC_AGGREGATE_NONE, is_deterministic, is_available}
+
+#define BUILTIN_AGGREGATE_ENTRY(param_count, returns, flags, call,\
+				finalize, user_data, is_available)\
+	{param_count, returns, flags, call, finalize, user_data,  \
+	 FUNC_AGGREGATE_GROUP, false, is_available}
+
+#define BUILTIN_STUB_ENTRY() \
+	{-1, FIELD_TYPE_ANY, 0, sql_builtin_stub, NULL,		  \
+	 NULL, FUNC_AGGREGATE_NONE, false, false}
+
+/**
+ * A sequence of SQL builtins definitions in
+ * lexicographic order.
+ */
+static struct {
+	const char *name;
+	int entries;
+	struct sql_builtin_entry entry[5];
+} sql_builtins[] = {
+	{"ABS", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_NUMBER, 0,
+				sql_builtin_abs, NULL, true, true),
+	}},
+	{"AVG", 1, {
+	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_NUMBER, 0,
+				 sql_builtin_sum_step,
+				 sql_builtin_avg_finalize, NULL, true),
+	}},
+	{"CEIL", 1, {BUILTIN_STUB_ENTRY()}},
+	{"CEILING", 1, {BUILTIN_STUB_ENTRY()}},
+	{"CHAR", 1, {
+	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_STRING, 0,
+				sql_builtin_char, NULL, true, true),
+	}},
+	{"CHARACTER_LENGTH", 1, {BUILTIN_STUB_ENTRY()}},
+	{"CHAR_LENGTH", 1, {BUILTIN_STUB_ENTRY()}},
+	{"COALESCE", 3, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_SCALAR, 0,
+				sql_builtin_stub, NULL, true, false),
+	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_SCALAR, 0,
+				sql_builtin_stub, NULL, true, false),
+	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_SCALAR, SQL_FUNC_COALESCE,
+				sql_builtin_stub, NULL, true, true),
+	}},
+	{"COUNT", 2, {
+	 BUILTIN_AGGREGATE_ENTRY(0, FIELD_TYPE_INTEGER,
+				 SQL_FUNC_COUNT,
+				 sql_builtin_count_step,
+				 sql_builtin_count_finalize, NULL, true),
+	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_INTEGER, 0,
+				 sql_builtin_count_step,
+				 sql_builtin_count_finalize, NULL, true),
+	}},
+	{"CURRENT_DATE", 1, {BUILTIN_STUB_ENTRY()}},
+	{"CURRENT_TIME", 1, {BUILTIN_STUB_ENTRY()}},
+	{"CURRENT_TIMESTAMP", 1, {BUILTIN_STUB_ENTRY()}},
+	{"DATE", 1, {BUILTIN_STUB_ENTRY()}},
+	{"DATETIME", 1, {BUILTIN_STUB_ENTRY()}},
+	{"EVERY", 1, {BUILTIN_STUB_ENTRY()}},
+	{"EXISTS", 1, {BUILTIN_STUB_ENTRY()}},
+	{"EXP", 1, {BUILTIN_STUB_ENTRY()}},
+	{"EXTRACT", 1, {BUILTIN_STUB_ENTRY()}},
+	{"FLOOR", 1, {BUILTIN_STUB_ENTRY()}},
+	{"GREATER", 1, {BUILTIN_STUB_ENTRY()}},
+	{"GROUP_CONCAT", 2, {
+	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_STRING, 0,
+				 sql_builtin_group_concat_step,
+				 sql_builtin_group_concat_finalize, NULL, true),
+	 BUILTIN_AGGREGATE_ENTRY(2, FIELD_TYPE_STRING, 1,
+				 sql_builtin_group_concat_step,
+				 sql_builtin_group_concat_finalize, NULL, true),
+	}},
+	{"HEX", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, 0,
+				sql_builtin_hex, NULL, true, true),
+	}},
+	{"IFNULL", 1, {
+	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_INTEGER, SQL_FUNC_COALESCE,
+				sql_builtin_stub, NULL, true, true),
+	}},
+	{"JULIANDAY", 1, {BUILTIN_STUB_ENTRY()}},
+	{"LENGTH", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_INTEGER, SQL_FUNC_LENGTH,
+				sql_builtin_length, NULL, true, true),
+	}},
+	{"LESSER", 1, {BUILTIN_STUB_ENTRY()}},
+	{"LIKE", 2, {
+	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_INTEGER, SQL_FUNC_LIKE,
+				sql_builtin_like, SQL_INT_TO_PTR(1),
+				true, true),
+	 BUILTIN_FUNCTION_ENTRY(3, FIELD_TYPE_INTEGER, SQL_FUNC_LIKE,
+				sql_builtin_like, SQL_INT_TO_PTR(1),
+				true, true),
+	}},
+	{"LN", 1, {BUILTIN_STUB_ENTRY()}},
+	{"LOWER", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING,
+				SQL_FUNC_DERIVEDCOLL | SQL_FUNC_NEEDCOLL,
+				sql_builtin_ICULower, NULL, true, true),
+	}},
+	{"MAX", 3, {
+	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_SCALAR, SQL_FUNC_NEEDCOLL,
+				sql_builtin_minmax, SQL_INT_TO_PTR(1),
+				true, true),
+	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_SCALAR, 0,
+				sql_builtin_stub, SQL_INT_TO_PTR(1),
+				true, false),
+	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_SCALAR,
+				 SQL_FUNC_NEEDCOLL | SQL_FUNC_MINMAX,
+				 sql_builtin_minmax_step,
+				 sql_builtin_minmax_finalize,
+				 SQL_INT_TO_PTR(1), true),
+	}},
+	{"MIN", 3, {
+	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_SCALAR, SQL_FUNC_NEEDCOLL,
+				sql_builtin_minmax, SQL_INT_TO_PTR(0),
+				true, true),
+	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_SCALAR, 0,
+				sql_builtin_stub, SQL_INT_TO_PTR(0),
+				true, false),
+	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_SCALAR,
+				 SQL_FUNC_NEEDCOLL | SQL_FUNC_MINMAX,
+				 sql_builtin_minmax_step,
+				 sql_builtin_minmax_finalize,
+				 SQL_INT_TO_PTR(0), true),
+	}},
+	{"MOD", 1, {BUILTIN_STUB_ENTRY()}},
+	{"NULLIF", 1, {
+	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_SCALAR, SQL_FUNC_NEEDCOLL,
+				sql_builtin_nullif, NULL, true, true),
+	}},
+	{"OCTET_LENGTH", 1, {BUILTIN_STUB_ENTRY()}},
+	{"POSITION", 1, {
+	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_INTEGER, SQL_FUNC_NEEDCOLL,
+				sql_builtin_position, NULL, true, true),
+	}},
+	{"POWER", 1, {BUILTIN_STUB_ENTRY()}},
+	{"PRINTF", 1, {
+	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_STRING, 0,
+				sql_builtin_printf, NULL, true, true),
+	}},
+	{"QUOTE", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, 0,
+				sql_builtin_quote, NULL, true, true),
+	}},
+	{"RANDOM", 1, {
+	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_INTEGER, 0,
+				sql_builtin_random, NULL, false, true),
+	}},
+	{"RANDOMBLOB", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_SCALAR, 0,
+				sql_builtin_random_blob, NULL, false, true),
+	}},
+	{"REPLACE", 1, {
+	 BUILTIN_FUNCTION_ENTRY(3, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
+				sql_builtin_replace, NULL, true, true),
+	}},
+	{"ROUND", 2, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_INTEGER, 0,
+				sql_builtin_round, NULL, true, true),
+	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_INTEGER, 0,
+				sql_builtin_round, NULL, true, true),
+	}},
+	{"ROW_COUNT", 1, {
+	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_INTEGER, 0,
+				sql_builtin_row_count, NULL, true, true),
+	}},
+	{"SOME", 1, {BUILTIN_STUB_ENTRY()}},
+	{"SQRT", 1, {BUILTIN_STUB_ENTRY()}},
+	{"STRFTIME", 1, {BUILTIN_STUB_ENTRY()}},
+	{"SUBSTR", 2, {
+	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
+				sql_builtin_substr, NULL, true, true),
+	 BUILTIN_FUNCTION_ENTRY(3, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
+				sql_builtin_substr, NULL, true, true),
+	}},
+	{"SUM", 1, {
+	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_NUMBER, 0,
+				 sql_builtin_sum_step,
+				 sql_builtin_sum_finalize, NULL, true),
+	}},
+	{"TIME", 1, {BUILTIN_STUB_ENTRY()}},
+	{"TOTAL", 1, {
+	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_NUMBER, 0,
+				 sql_builtin_sum_step,
+				 sql_builtin_total_finalize, NULL, true),
+	}},
+	{"TRIM", 3, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
+				sql_builtin_trim_one_arg, NULL,
+				true, true),
+	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
+				sql_builtin_trim_two_args, NULL,
+				true, true),
+	 BUILTIN_FUNCTION_ENTRY(3, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
+				sql_builtin_trim_three_args, NULL,
+				true, true),
+	}},
+	{"TYPEOF", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, SQL_FUNC_TYPEOF,
+				sql_builtin_typeof, NULL, true, true),
+	}},
+	{"UNICODE", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, 0,
+				sql_builtin_unicode, NULL, true, true),
+	}},
+	{"UPPER", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING,
+				SQL_FUNC_DERIVEDCOLL | SQL_FUNC_NEEDCOLL,
+				sql_builtin_ICUUpper, NULL, true, true),
+	}},
+	{"VERSION", 1, {
+	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_STRING, 0,
+				sql_builtin_version, NULL, true, true),
+	}},
+	{"ZEROBLOB", 1, {
+	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_SCALAR, 0,
+				sql_builtin_zeroblob, NULL, true, true),
+	}},
+	{"_sql_stat_get", 1, {BUILTIN_STUB_ENTRY()}},
+	{"_sql_stat_init", 1, {BUILTIN_STUB_ENTRY()}},
+	{"_sql_stat_push", 1, {BUILTIN_STUB_ENTRY()}},
+};
 
 struct func *
 func_sql_builtin_new(struct func_def *def)
@@ -2175,26 +2532,88 @@ func_sql_builtin_new(struct func_def *def)
 			 "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");
+	/** Binary search for corresponding builtin entry. */
+	int idx = -1, left = 0, right = nelem(sql_builtins) - 1;
+	while (left <= right) {
+		uint32_t mid = (left + right) / 2;
+		int rc = strcmp(def->name, sql_builtins[mid].name);
+		if (rc == 0) {
+			idx = mid;
+			break;
+		}
+		if (rc < 0)
+			right = mid - 1;
+		else
+			left = mid + 1;
+	}
+	if (idx == -1) {
+		diag_set(ClientError, ER_CREATE_FUNCTION, def->name,
+			 "unknown sql builtin name");
+		return NULL;
+	}
+	int func_cnt = sql_builtins[idx].entries;
+	struct func_sql_builtin *funcs =
+		(struct func_sql_builtin *) malloc(sizeof(*funcs) * func_cnt);
+	if (funcs == NULL) {
+		diag_set(OutOfMemory, sizeof(*funcs) * func_cnt,
+			 "malloc", "func");
 		return NULL;
 	}
+	int func_idx = 0;
+	for (; func_idx < func_cnt; func_idx++) {
+		struct func_def *curr_def;
+		if (func_idx == 0) {
+			curr_def = def;
+			rlist_create(&funcs[func_idx].base.signature);
+		} else {
+			curr_def = func_def_dup(def);
+			if (curr_def == NULL)
+				goto error;
+			rlist_add(&funcs[0].base.signature,
+				  &funcs[func_idx].base.signature);
+		}
+		curr_def->is_deterministic =
+			sql_builtins[idx].entry[func_idx].is_deterministic;
+		curr_def->sql_flags =
+			sql_builtins[idx].entry[func_idx].sql_flags;
+		curr_def->param_count =
+			sql_builtins[idx].entry[func_idx].param_count;
+		curr_def->returns =
+			sql_builtins[idx].entry[func_idx].returns;
+		curr_def->aggregate =
+			sql_builtins[idx].entry[func_idx].aggregate;
+		curr_def->exports.sql =
+			sql_builtins[idx].entry[func_idx].is_available;
+		funcs[func_idx].base.def = curr_def;
+		funcs[func_idx].user_data =
+			sql_builtins[idx].entry[func_idx].user_data;
+		funcs[func_idx].finalize =
+			sql_builtins[idx].entry[func_idx].finalize;
+		funcs[func_idx].base.vtab = func_sql_builtin_vtab;
+		funcs[func_idx].base.vtab.call =
+			sql_builtins[idx].entry[func_idx].call;
+	}
 	/** Don't export SQL builtins in Lua for now. */
 	def->exports.lua = false;
-	func->base.vtab = func_sql_builtin_vtab;
-	return &func->base;
+	return &funcs[0].base;
+error:
+	for (int i = 1; i < func_idx; i++)
+		free(funcs[func_idx].base.def);
+	free(funcs);
+	return NULL;
 }
 
 static void
 func_sql_builtin_destroy(struct func *base)
 {
 	assert(base != NULL && base->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	struct func *func, *tmp;
+	rlist_foreach_entry_safe(func, &base->signature, signature, tmp)
+		free(func->def);
 	free(base);
 }
 
 static struct func_vtab func_sql_builtin_vtab = {
-	.call = NULL,
+	.call = sql_builtin_stub,
 	.destroy = func_sql_builtin_destroy,
 };
diff --git a/src/box/sql/global.c b/src/box/sql/global.c
index 6cadef809..c25b83de1 100644
--- a/src/box/sql/global.c
+++ b/src/box/sql/global.c
@@ -162,13 +162,6 @@ SQL_WSD struct sqlConfig sqlConfig = {
 	0x7ffffffe		/* iOnceResetThreshold */
 };
 
-/*
- * Hash table for global functions - functions common to all
- * database connections.  After initialization, this table is
- * read-only.
- */
-FuncDefHash sqlBuiltinFunctions;
-
 /*
  * The value of the "pending" byte must be 0x40000000 (1 byte past the
  * 1-gibabyte boundary) in a compatible database.  sql never uses
diff --git a/src/box/sql/main.c b/src/box/sql/main.c
index baaf7dcc9..16505ac7f 100644
--- a/src/box/sql/main.c
+++ b/src/box/sql/main.c
@@ -127,9 +127,6 @@ sql_initialize(void)
 	if (sqlGlobalConfig.isInit == 0
 	    && sqlGlobalConfig.inProgress == 0) {
 		sqlGlobalConfig.inProgress = 1;
-		memset(&sqlBuiltinFunctions, 0,
-		       sizeof(sqlBuiltinFunctions));
-		sqlRegisterBuiltinFunctions();
 		sql_os_init();
 		sqlGlobalConfig.isInit = 1;
 		sqlGlobalConfig.inProgress = 0;
@@ -177,25 +174,6 @@ sqlCloseSavepoints(Vdbe * pVdbe)
 	pVdbe->anonymous_savepoint = NULL;
 }
 
-/*
- * Invoke the destructor function associated with FuncDef p, if any. Except,
- * if this is not the last copy of the function, do not invoke it. Multiple
- * copies of a single function are created when create_function() is called
- * with SQL_ANY as the encoding.
- */
-static void
-functionDestroy(sql * db, FuncDef * p)
-{
-	FuncDestructor *pDestructor = p->u.pDestructor;
-	if (pDestructor) {
-		pDestructor->nRef--;
-		if (pDestructor->nRef == 0) {
-			pDestructor->xDestroy(pDestructor->pUserData);
-			sqlDbFree(db, pDestructor);
-		}
-	}
-}
-
 /*
  * Rollback all database files.  If tripCode is not 0, then
  * any write cursors are invalidated ("tripped" - as in "tripping a circuit
@@ -214,118 +192,6 @@ sqlRollbackAll(Vdbe * pVdbe)
 	}
 }
 
-/*
- * This function is exactly the same as sql_create_function(), except
- * that it is designed to be called by internal code. The difference is
- * that if a malloc() fails in sql_create_function(), an error code
- * is returned and the mallocFailed flag cleared.
- */
-int
-sqlCreateFunc(struct sql * db, const char *zFunctionName, enum field_type type,
-	      int nArg, int flags, void *pUserData,
-	      int (*xSFunc) (struct func *func, struct port *args,
-			     struct port *ret),
-	      int (*xStep) (struct func *func, struct port *args,
-			    struct port *ret),
-	      int (*xFinal) (struct func *func, struct port *args,
-			     struct port *ret),
-	      struct FuncDestructor * pDestructor)
-{
-	FuncDef *p;
-	int extraFlags;
-
-	if (zFunctionName == 0 ||
-	    (xSFunc && (xFinal || xStep)) ||
-	    (!xSFunc && (xFinal && !xStep)) ||
-	    (!xSFunc && (!xFinal && xStep)) ||
-	    (nArg < -1 || nArg > SQL_MAX_FUNCTION_ARG) ||
-	    (255 < (sqlStrlen30(zFunctionName)))) {
-		diag_set(ClientError, ER_CREATE_FUNCTION, zFunctionName,
-			 "wrong function definition");
-		return -1;
-	}
-
-	assert(SQL_FUNC_CONSTANT == SQL_DETERMINISTIC);
-	extraFlags = flags & SQL_DETERMINISTIC;
-
-
-	/* Check if an existing function is being overridden or deleted. If so,
-	 * and there are active VMs, then return an error. If a function
-	 * is being overridden/deleted but there are no active VMs, allow the
-	 * operation to continue but invalidate all precompiled statements.
-	 */
-	p = sqlFindFunction(db, zFunctionName, nArg, 0);
-	if (p && p->nArg == nArg) {
-		if (db->nVdbeActive) {
-			diag_set(ClientError, ER_CREATE_FUNCTION, zFunctionName,
-				 "unable to create function due to active "\
-				 "statements");
-			return -1;
-		} else {
-			sqlExpirePreparedStatements(db);
-		}
-	}
-
-	p = sqlFindFunction(db, zFunctionName, nArg, 1);
-	assert(p || db->mallocFailed);
-	if (p == NULL)
-		return -1;
-
-	/* If an older version of the function with a configured destructor is
-	 * being replaced invoke the destructor function here.
-	 */
-	functionDestroy(db, p);
-
-	if (pDestructor) {
-		pDestructor->nRef++;
-	}
-	p->u.pDestructor = pDestructor;
-	p->funcFlags = extraFlags;
-	testcase(p->funcFlags & SQL_DETERMINISTIC);
-	p->xSFunc = xSFunc ? xSFunc : xStep;
-	p->xFinalize = xFinal;
-	p->pUserData = pUserData;
-	p->nArg = (u16) nArg;
-	p->ret_type = type;
-	return 0;
-}
-
-
-int
-sql_create_function_v2(struct sql * db, const char *zFunc, enum field_type type,
-		       int nArg, int flags, void *p,
-		       int (*xSFunc) (struct func *func, struct port *args,
-				      struct port *ret),
-		       int (*xStep) (struct func *func, struct port *args,
-				     struct port *ret),
-		       int (*xFinal) (struct func *func, struct port *args,
-				      struct port *ret),
-		       void (*xDestroy) (void *ctx))
-{
-	FuncDestructor *pArg = 0;
-
-	if (xDestroy) {
-		pArg =
-		    (FuncDestructor *) sqlDbMallocZero(db,
-							   sizeof
-							   (FuncDestructor));
-		if (!pArg) {
-			xDestroy(p);
-			return -1;
-		}
-		pArg->xDestroy = xDestroy;
-		pArg->pUserData = p;
-	}
-	int rc = sqlCreateFunc(db, zFunc, type, nArg, flags, p, xSFunc, xStep,
-			       xFinal, pArg);
-	if (pArg && pArg->nRef == 0) {
-		assert(rc != 0);
-		xDestroy(p);
-		sqlDbFree(db, pArg);
-	}
-	return rc;
-}
-
 /*
  * This array defines hard upper bounds on limit values.  The
  * initializer must be kept in sync with the SQL_LIMIT_*
diff --git a/src/box/sql/resolve.c b/src/box/sql/resolve.c
index 7c81b1980..7c67d5d56 100644
--- a/src/box/sql/resolve.c
+++ b/src/box/sql/resolve.c
@@ -38,6 +38,8 @@
 #include "sqlInt.h"
 #include <stdlib.h>
 #include <string.h>
+#include "box/func.h"
+#include "box/field_def.h"
 
 /*
  * Walk the expression tree pExpr and increase the aggregate function
@@ -576,36 +578,31 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 			int no_such_func = 0;	/* True if no such function exists */
 			int wrong_num_args = 0;	/* True if wrong number of arguments */
 			int is_agg = 0;	/* True if is an aggregate function */
-			int nId;	/* Number of characters in function name */
-			const char *zId;	/* The function name. */
-			FuncDef *pDef;	/* Information about the function */
-
 			assert(!ExprHasProperty(pExpr, EP_xIsSelect));
-			zId = pExpr->u.zToken;
-			nId = sqlStrlen30(zId);
-			pDef = sqlFindFunction(pParse->db, zId, n, 0);
-			if (pDef == 0) {
-				pDef =
-				    sqlFindFunction(pParse->db, zId, -2,0);
-				if (pDef == 0) {
+			const char *name = pExpr->u.zToken;
+			uint32_t name_len = strlen(name);
+			struct func *func =
+				sql_func_by_signature(name, name_len, n);
+			if (func == 0) {
+				func = sql_func_by_signature(name, name_len, -2);
+				if (func == NULL) {
 					no_such_func = 1;
 				} else {
 					wrong_num_args = 1;
 				}
 			} else {
-				is_agg = pDef->xFinalize != 0;
-				pExpr->type = pDef->ret_type;
-				if (pDef->
-				    funcFlags & (SQL_FUNC_CONSTANT |
-						 SQL_FUNC_SLOCHNG)) {
+				is_agg = func->def->language ==
+					 FUNC_LANGUAGE_SQL_BUILTIN &&
+					 func->def->aggregate ==
+					 FUNC_AGGREGATE_GROUP;
+				pExpr->type = func->def->returns;
+				if (func->def->is_deterministic) {
 					/* For the purposes of the EP_ConstFunc flag, date and time
 					 * functions and other functions that change slowly are considered
 					 * constant because they are constant for the duration of one query
 					 */
 					ExprSetProperty(pExpr, EP_ConstFunc);
-				}
-				if ((pDef->funcFlags & SQL_FUNC_CONSTANT) ==
-				    0) {
+				} else {
 					/* Date/time functions that use 'now', and other functions
 					 * that might change over time cannot be used
 					 * in an index.
@@ -614,22 +611,21 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 				}
 			}
 			if (is_agg && (pNC->ncFlags & NC_AllowAgg) == 0) {
-				const char *err =
-					tt_sprintf("misuse of aggregate "\
-						   "function %.*s()", nId, zId);
-				diag_set(ClientError, ER_SQL_PARSER_GENERIC, err);
+				diag_set(ClientError, ER_SQL_PARSER_GENERIC,
+					 tt_sprintf("misuse of aggregate "
+						    "function %s()", name));
 				pParse->is_aborted = true;
 				pNC->nErr++;
 				is_agg = 0;
 			} else if (no_such_func && pParse->db->init.busy == 0) {
-				diag_set(ClientError, ER_NO_SUCH_FUNCTION, zId);
+				diag_set(ClientError, ER_NO_SUCH_FUNCTION,
+					 name);
 				pParse->is_aborted = true;
 				pNC->nErr++;
 			} else if (wrong_num_args) {
-				const char *err = "wrong number of arguments "\
-						  "to function %.*s()";
 				diag_set(ClientError, ER_SQL_PARSER_GENERIC,
-					 tt_sprintf(err, nId, zId));
+					 tt_sprintf("wrong number of arguments "
+						    "to function %s()", name));
 				pParse->is_aborted = true;
 				pNC->nErr++;
 			}
@@ -648,7 +644,7 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 					pExpr->op2++;
 					pNC2 = pNC2->pNext;
 				}
-				assert(pDef != 0);
+				assert(func != NULL);
 				if (pNC2) {
 					assert(SQL_FUNC_MINMAX ==
 					       NC_MinMaxAgg);
@@ -656,8 +652,7 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 						  funcFlags &
 						  SQL_FUNC_MINMAX) != 0);
 					pNC2->ncFlags |=
-					    NC_HasAgg | (pDef->
-							 funcFlags &
+					    NC_HasAgg | (func->def->sql_flags &
 							 SQL_FUNC_MINMAX);
 
 				}
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index 7c8da251e..70b39ff30 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -4374,7 +4374,7 @@ is_simple_count(struct Select *select, struct AggInfo *agg_info)
 		return NULL;
 	if (NEVER(agg_info->nFunc == 0))
 		return NULL;
-	if ((agg_info->aFunc[0].pFunc->funcFlags & SQL_FUNC_COUNT) == 0)
+	if ((agg_info->aFunc[0].func->def->sql_flags & SQL_FUNC_COUNT) == 0)
 		return NULL;
 	if (expr->flags & EP_Distinct)
 		return NULL;
@@ -5269,7 +5269,7 @@ finalizeAggFunctions(Parse * pParse, AggInfo * pAggInfo)
 		assert(!ExprHasProperty(pF->pExpr, EP_xIsSelect));
 		sqlVdbeAddOp2(v, OP_AggFinal, pF->iMem,
 				  pList ? pList->nExpr : 0);
-		sqlVdbeAppendP4(v, pF->pFunc, P4_FUNCDEF);
+		sqlVdbeAppendP4(v, pF->func, P4_FUNC);
 	}
 }
 
@@ -5310,11 +5310,11 @@ updateAccumulator(Parse * pParse, AggInfo * pAggInfo)
 			vdbe_insert_distinct(pParse, pF->iDistinct, pF->reg_eph,
 					     addrNext, 1, regAgg);
 		}
-		if (pF->pFunc->funcFlags & SQL_FUNC_NEEDCOLL) {
+		if ((pF->func->def->sql_flags & SQL_FUNC_NEEDCOLL) != 0) {
 			struct coll *coll = NULL;
 			struct ExprList_item *pItem;
 			int j;
-			assert(pList != 0);	/* pList!=0 if pF->pFunc has NEEDCOLL */
+			assert(pList != 0);
 			bool unused;
 			uint32_t id;
 			for (j = 0, pItem = pList->a; coll == NULL && j < nArg;
@@ -5329,7 +5329,7 @@ updateAccumulator(Parse * pParse, AggInfo * pAggInfo)
 					  (char *)coll, P4_COLLSEQ);
 		}
 		sqlVdbeAddOp3(v, OP_AggStep0, 0, regAgg, pF->iMem);
-		sqlVdbeAppendP4(v, pF->pFunc, P4_FUNCDEF);
+		sqlVdbeAppendP4(v, pF->func, P4_FUNC);
 		sqlVdbeChangeP5(v, (u8) nArg);
 		sql_expr_type_cache_change(pParse, regAgg, nArg);
 		sqlReleaseTempRange(pParse, regAgg, nArg);
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index ce15500e5..dc0a06f5a 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -520,19 +520,6 @@ sql_initialize(void);
 #define SQL_TRACE_ROW        0x04
 #define SQL_TRACE_CLOSE      0x08
 
-#define SQL_DETERMINISTIC    0x800
-
-int
-sql_create_function_v2(struct sql * db, const char *zFunc, enum field_type type,
-		       int nArg, int flags, void *p,
-		       int (*xSFunc) (struct func *func, struct port *args,
-				      struct port *ret),
-		       int (*xStep) (struct func *func, struct port *args,
-				     struct port *ret),
-		       int (*xFinal) (struct func *func, struct port *args,
-				      struct port *ret),
-		       void (*xDestroy) (void *ctx));
-
 #define SQL_OPEN_READONLY         0x00000001	/* Ok for sql_open_v2() */
 #define SQL_OPEN_READWRITE        0x00000002	/* Ok for sql_open_v2() */
 #define SQL_OPEN_CREATE           0x00000004	/* Ok for sql_open_v2() */
@@ -983,9 +970,6 @@ typedef struct Column Column;
 typedef struct Expr Expr;
 typedef struct ExprList ExprList;
 typedef struct ExprSpan ExprSpan;
-typedef struct FuncDestructor FuncDestructor;
-typedef struct FuncDef FuncDef;
-typedef struct FuncDefHash FuncDefHash;
 typedef struct IdList IdList;
 typedef struct KeyClass KeyClass;
 typedef struct NameContext NameContext;
@@ -1030,18 +1014,6 @@ typedef int VList;
  */
 #define SQL_N_LIMIT (SQL_LIMIT_TRIGGER_DEPTH+1)
 
-/*
- * A hash table for built-in function definitions.  (Application-defined
- * functions use a regular table table from hash.h.)
- *
- * Hash each FuncDef structure into one of the FuncDefHash.a[] slots.
- * Collisions are on the FuncDef.u.pHash chain.
- */
-#define SQL_FUNC_HASH_SZ 23
-struct FuncDefHash {
-	FuncDef *a[SQL_FUNC_HASH_SZ];	/* Hash table for functions */
-};
-
 /*
  * Each database connection is an instance of the following structure.
  */
@@ -1152,62 +1124,14 @@ struct type_def {
 };
 
 /*
- * Each SQL function is defined by an instance of the following
- * structure.  For global built-in functions (ex: substr(), max(), count())
- * a pointer to this structure is held in the sqlBuiltinFunctions object.
- * For per-connection application-defined functions, a pointer to this
- * structure is held in the db->aHash hash table.
- *
- * The u.pHash field is used by the global built-ins.  The u.pDestructor
- * field is used by per-connection app-def functions.
- */
-struct FuncDef {
-	i8 nArg;		/* Number of arguments.  -1 means unlimited */
-	u16 funcFlags;		/* Some combination of sql_FUNC_* */
-	void *pUserData;	/* User data parameter */
-	FuncDef *pNext;		/* Next function with same name */
-	int (*xSFunc) (struct func *func, struct port *args, struct port *ret);
-	int (*xFinalize) (struct func *func, struct port *args, struct port *ret);
-	const char *zName;	/* SQL name of the function. */
-	union {
-		FuncDef *pHash;	/* Next with a different name but the same hash */
-		FuncDestructor *pDestructor;	/* Reference counted destructor function */
-	} u;
-	/* Return type. */
-	enum field_type ret_type;
-};
-
-/*
- * This structure encapsulates a user-function destructor callback (as
- * configured using create_function_v2()) and a reference counter. When
- * create_function_v2() is called to create a function with a destructor,
- * a single object of this type is allocated. FuncDestructor.nRef is set to
- * the number of FuncDef objects created (either 1 or 3, depending on whether
- * or not the specified encoding is sql_ANY). The FuncDef.pDestructor
- * member of each of the new FuncDef objects is set to point to the allocated
- * FuncDestructor.
- *
- * Thereafter, when one of the FuncDef objects is deleted, the reference
- * count on this object is decremented. When it reaches 0, the destructor
- * is invoked and the FuncDestructor structure freed.
- */
-struct FuncDestructor {
-	int nRef;
-	void (*xDestroy) (void *);
-	void *pUserData;
-};
-
-/*
- * Possible values for FuncDef.flags.  Note that the _LENGTH and _TYPEOF
- * values must correspond to OPFLAG_LENGTHARG and OPFLAG_TYPEOFARG.  And
- * sql_FUNC_CONSTANT must be the same as sql_DETERMINISTIC.  There
- * are assert() statements in the code to verify this.
+ * Possible values for FuncDef.flags.  Note that the _LENGTH and
+ * _TYPEOF values must correspond to OPFLAG_LENGTHARG and
+ * OPFLAG_TYPEOFARG.
  *
  * Value constraints (enforced via assert()):
  *     SQL_FUNC_MINMAX    ==  NC_MinMaxAgg      == SF_MinMaxAgg
  *     SQL_FUNC_LENGTH    ==  OPFLAG_LENGTHARG
  *     SQL_FUNC_TYPEOF    ==  OPFLAG_TYPEOFARG
- *     SQL_FUNC_CONSTANT  ==  sql_DETERMINISTIC from the API
  */
 #define SQL_FUNC_LIKE     0x0004	/* Candidate for the LIKE optimization */
 #define SQL_FUNC_EPHEM    0x0010	/* Ephemeral.  Delete with VDBE */
@@ -1229,11 +1153,7 @@ struct FuncDestructor {
  * argument.
  */
 #define SQL_FUNC_DERIVEDCOLL 0x0400
-#define SQL_FUNC_CONSTANT 0x0800	/* Constant inputs give a constant output */
 #define SQL_FUNC_MINMAX   0x1000	/* True for min() and max() aggregates */
-#define SQL_FUNC_SLOCHNG  0x2000	/* "Slow Change". Value constant during a
-					 * single query - might change over time
-					 */
 
 /*
  * Trim side mask components. TRIM_LEADING means to trim left side
@@ -1246,72 +1166,6 @@ enum trim_side_mask {
 	TRIM_BOTH = TRIM_LEADING | TRIM_TRAILING
 };
 
-/*
- * The following three macros, FUNCTION(), LIKEFUNC() and AGGREGATE() are
- * used to create the initializers for the FuncDef structures.
- *
- *   FUNCTION(zName, nArg, iArg, bNC, xFunc)
- *     Used to create a scalar function definition of a function zName
- *     implemented by C function xFunc that accepts nArg arguments. The
- *     value passed as iArg is cast to a (void*) and made available
- *     as the user-data (sql_user_data()) for the function. If
- *     argument bNC is true, then the sql_FUNC_NEEDCOLL flag is set.
- *
- *   FUNCTION_COLL
- *     Like FUNCTION except it assumes that function returns
- *     STRING which collation should be derived from first
- *     argument (trim, substr etc).
- *
- *   VFUNCTION(zName, nArg, iArg, bNC, xFunc)
- *     Like FUNCTION except it omits the sql_FUNC_CONSTANT flag.
- *
- *   DFUNCTION(zName, nArg, iArg, bNC, xFunc)
- *     Like FUNCTION except it omits the sql_FUNC_CONSTANT flag and
- *     adds the sql_FUNC_SLOCHNG flag.  Used for date & time functions,
- *     but not during a single query.
- *
- *   AGGREGATE(zName, nArg, iArg, bNC, xStep, xFinal)
- *     Used to create an aggregate function definition implemented by
- *     the C functions xStep and xFinal. The first four parameters
- *     are interpreted in the same way as the first 4 parameters to
- *     FUNCTION().
- *
- *   LIKEFUNC(zName, nArg, pArg, flags)
- *     Used to create a scalar function definition of a function zName
- *     that accepts nArg arguments and is implemented by a call to C
- *     function likeFunc. Argument pArg is cast to a (void *) and made
- *     available as the function user-data (sql_user_data()). The
- *     FuncDef.flags variable is set to the value passed as the flags
- *     parameter.
- */
-#define FUNCTION(zName, nArg, iArg, bNC, xFunc, type) \
-  {nArg, SQL_FUNC_CONSTANT|(bNC*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
-#define FUNCTION_COLL(zName, nArg, iArg, bNC, xFunc) \
-  {nArg, SQL_FUNC_CONSTANT|SQL_FUNC_DERIVEDCOLL|(bNC*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, FIELD_TYPE_STRING}
-#define VFUNCTION(zName, nArg, iArg, bNC, xFunc, type) \
-  {nArg, (bNC*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
-#define DFUNCTION(zName, nArg, iArg, bNC, xFunc, type) \
-  {nArg, SQL_FUNC_SLOCHNG|(bNC*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
-#define FUNCTION2(zName, nArg, iArg, bNC, xFunc, extraFlags, type) \
-  {nArg,SQL_FUNC_CONSTANT|(bNC*SQL_FUNC_NEEDCOLL)|extraFlags,\
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
-#define STR_FUNCTION(zName, nArg, pArg, bNC, xFunc) \
-  {nArg, SQL_FUNC_SLOCHNG|(bNC*SQL_FUNC_NEEDCOLL), \
-   pArg, 0, xFunc, 0, #zName, {SQL_AFF_STRING, {0}}}
-#define LIKEFUNC(zName, nArg, arg, flags, type) \
-  {nArg, SQL_FUNC_CONSTANT|flags, \
-   (void *)(SQL_INT_TO_PTR(arg)), 0, likeFunc, 0, #zName, {0}, type}
-#define AGGREGATE(zName, nArg, arg, nc, xStep, xFinal, type) \
-  {nArg, (nc*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}, type}
-#define AGGREGATE2(zName, nArg, arg, nc, xStep, xFinal, extraFlags, type) \
-  {nArg, (nc*SQL_FUNC_NEEDCOLL)|extraFlags, \
-   SQL_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}, type}
-
 /*
  * All current savepoints are stored in a linked list starting at
  * sql.pSavepoint. The first element in the list is the most recently
@@ -1504,7 +1358,8 @@ struct AggInfo {
 				 */
 	struct AggInfo_func {	/* For each aggregate function */
 		Expr *pExpr;	/* Expression encoding the function */
-		FuncDef *pFunc;	/* The aggregate function implementation */
+		/** The aggregate function implementation. */
+		struct func *func;
 		int iMem;	/* Memory location that acts as accumulator */
 		int iDistinct;	/* Ephemeral table used to enforce DISTINCT */
 		/**
@@ -1654,7 +1509,8 @@ struct Expr {
 #define EP_Static    0x008000	/* Held in memory not obtained from malloc() */
 #define EP_MemToken  0x010000	/* Need to sqlDbFree() Expr.zToken */
 #define EP_NoReduce  0x020000	/* Cannot EXPRDUP_REDUCE this Expr */
-#define EP_ConstFunc 0x080000	/* A sql_FUNC_CONSTANT or _SLOCHNG function */
+/** A deterministic function. */
+#define EP_ConstFunc 0x080000
 #define EP_CanBeNull 0x100000	/* Can be null despite NOT NULL constraint */
 #define EP_Subquery  0x200000	/* Tree contains a TK_SELECT operator */
 #define EP_Alias     0x400000	/* Is an alias for a result set column */
@@ -3462,10 +3318,6 @@ void sqlSelectSetName(Select *, const char *);
 #else
 #define sqlSelectSetName(A,B)
 #endif
-void sqlInsertBuiltinFuncs(FuncDef *, int);
-FuncDef *sqlFindFunction(sql *, const char *, int, u8);
-void sqlRegisterBuiltinFunctions(void);
-void sqlRegisterDateTimeFunctions(void);
 
 /**
  * Evaluate a view and store its result in an ephemeral table.
@@ -4037,7 +3889,6 @@ extern const unsigned char sqlUpperToLower[];
 extern const unsigned char sqlCtypeMap[];
 extern const Token sqlIntTokens[];
 extern SQL_WSD struct sqlConfig sqlConfig;
-extern FuncDefHash sqlBuiltinFunctions;
 extern int sqlPendingByte;
 
 /**
@@ -4218,18 +4069,7 @@ sql_key_info_to_key_def(struct sql_key_info *key_info);
  * @retval 1 if LIKE optimization can be used, 0 otherwise.
  */
 int
-sql_is_like_func(struct sql *db, struct Expr *expr, int *is_like_ci);
-
-int
-sqlCreateFunc(struct sql * db, const char *zFunctionName, enum field_type type,
-	      int nArg, int flags, void *pUserData,
-	      int (*xSFunc) (struct func *func, struct port *args,
-			     struct port *ret),
-	      int (*xStep) (struct func *func, struct port *args,
-			    struct port *ret),
-	      int (*xFinal) (struct func *func, struct port *args,
-			     struct port *ret),
-	      struct FuncDestructor * pDestructor);
+sql_is_like_func(struct Expr *expr, int *is_like_ci);
 
 /** Set OOM error flag. */
 static inline void
@@ -4456,10 +4296,6 @@ Expr *sqlExprForVectorField(Parse *, Expr *, int);
  */
 extern int sqlSubProgramsRemaining;
 
-/** Register built-in functions to work with ANALYZE data. */
-void
-sql_register_analyze_builtins(void);
-
 /**
  * Generate VDBE code to halt execution with correct error if
  * the object with specified key is already present (or doesn't
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 92c99877a..eb18d4b7a 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -1722,14 +1722,14 @@ case OP_CollSeq: {
 case OP_Function0: {
 	sql_context *pCtx;
 
-	assert(pOp->p4type==P4_FUNCDEF);
+	assert(pOp->p4type == P4_FUNC);
 	assert(pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor));
 	assert(pOp->p5 == 0 ||
 	       (pOp->p2 > 0 && pOp->p2 + pOp->p5 <= (p->nMem+1 - p->nCursor)+1));
 	assert(pOp->p3 < pOp->p2 || pOp->p3>=pOp->p2 + pOp->p5);
 	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx));
 	if (pCtx==0) goto no_mem;
-	pCtx->pFunc = pOp->p4.pFunc;
+	pCtx->func = pOp->p4.func;
 	pCtx->iOp = (int)(pOp - aOp);
 	pCtx->pVdbe = p;
 	pOp->p4type = P4_FUNCCTX;
@@ -1764,7 +1764,7 @@ case OP_Function: {
 	}
 #endif
 	MemSetTypeFlag(pOut, MEM_Null);
-	rc = (*pCtx->pFunc->xSFunc)(NULL, &args, &ret);
+	rc = func_call(pCtx->func, &args, &ret);
 	if (rc != 0)
 		goto abort_due_to_error;
 	uint32_t size;
@@ -4969,15 +4969,18 @@ case OP_DecrJumpZero: {      /* jump, in1 */
 case OP_AggStep0: {
 	sql_context *pCtx;
 
-	assert(pOp->p4type == P4_FUNCDEF);
+	assert(pOp->p4type == P4_FUNC);
 	assert(pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor));
 	assert(pOp->p5 == 0 ||
 	       (pOp->p2 > 0 && pOp->p2 + pOp->p5 <= (p->nMem+1 - p->nCursor)+1));
-	assert(pOp->p3 < pOp->p2 || pOp->p3 >= pOp->p2 + pOp->p5);
+	assert(pOp->p3<pOp->p2 || pOp->p3>=pOp->p2 + pOp->p5);
+	assert(pOp->p4.func != NULL &&
+	       pOp->p4.func->def->language == FUNC_LANGUAGE_SQL_BUILTIN &&
+	       pOp->p4.func->def->aggregate == FUNC_AGGREGATE_GROUP);
 	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx));
 	if (pCtx==0) goto no_mem;
 	pCtx->pMem = 0;
-	pCtx->pFunc = pOp->p4.pFunc;
+	pCtx->func = pOp->p4.func;
 	pCtx->iOp = (int)(pOp - aOp);
 	pCtx->pVdbe = p;
 	pOp->p4type = P4_FUNCCTX;
@@ -5011,7 +5014,7 @@ case OP_AggStep: {
 	sqlVdbeMemInit(&t, db, MEM_Null);
 	pOut = &t;
 	pCtx->skipFlag = 0;
-	rc = (*pCtx->pFunc->xSFunc)(NULL, &args, &ret);
+	rc = func_call(pCtx->func, &args, &ret);
 	region_truncate(region, region_svp);
 	if (rc != 0)
 		goto abort_due_to_error;
@@ -5042,7 +5045,7 @@ case OP_AggFinal: {
 	assert(pOp->p1>0 && pOp->p1<=(p->nMem+1 - p->nCursor));
 	pMem = &aMem[pOp->p1];
 	assert((pMem->flags & ~(MEM_Null|MEM_Agg))==0);
-	if (sqlVdbeMemFinalize(pMem, pOp->p4.pFunc) != 0)
+	if (sql_vdbemem_finilize(pMem, pOp->p4.func) != 0)
 		goto abort_due_to_error;
 	UPDATE_MAX_BLOBSIZE(pMem);
 	if (sqlVdbeMemTooBig(pMem)) {
diff --git a/src/box/sql/vdbe.h b/src/box/sql/vdbe.h
index 4f94643cb..31a0a39d7 100644
--- a/src/box/sql/vdbe.h
+++ b/src/box/sql/vdbe.h
@@ -72,7 +72,8 @@ struct VdbeOp {
 		char *z;	/* Pointer to data for string (char array) types */
 		i64 *pI64;	/* Used when p4type is P4_INT64 */
 		double *pReal;	/* Used when p4type is P4_REAL */
-		FuncDef *pFunc;	/* Used when p4type is P4_FUNCDEF */
+		/* Used when p4type is P4_FUNC. */
+		struct func *func;
 		sql_context *pCtx;	/* Used when p4type is P4_FUNCCTX */
 		struct coll *pColl;	/* Used when p4type is P4_COLLSEQ */
 		Mem *pMem;	/* Used when p4type is P4_MEM */
@@ -122,7 +123,8 @@ struct SubProgram {
 #define P4_DYNAMIC  (-1)	/* Pointer to a string obtained from sqlMalloc() */
 #define P4_STATIC   (-2)	/* Pointer to a static string */
 #define P4_COLLSEQ  (-3)	/* P4 is a pointer to a CollSeq structure */
-#define P4_FUNCDEF  (-4)	/* P4 is a pointer to a FuncDef structure */
+/** P4 is a pointer to a function object. */
+#define P4_FUNC  (-4)
 #define P4_MEM      (-7)	/* P4 is a pointer to a Mem*    structure */
 #define P4_TRANSIENT  0		/* P4 is a pointer to a transient string */
 #define P4_REAL     (-9)	/* P4 is a 64-bit floating point value */
diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
index 4c8363a22..0d4acb0eb 100644
--- a/src/box/sql/vdbeInt.h
+++ b/src/box/sql/vdbeInt.h
@@ -46,6 +46,8 @@
  */
 typedef struct VdbeOp Op;
 
+struct func;
+
 /*
  * Boolean values
  */
@@ -167,7 +169,8 @@ struct Mem {
 		bool b;         /* Boolean value used when MEM_Bool is set in flags */
 		int nZero;	/* Used when bit MEM_Zero is set in flags */
 		void *p;	/* Generic pointer */
-		FuncDef *pDef;	/* Used only when flags==MEM_Agg */
+		/** Used only when flags==MEM_Agg. */
+		struct func *func;
 		VdbeFrame *pFrame;	/* Used when flags==MEM_Frame */
 	} u;
 	u32 flags;		/* Some combination of MEM_Null, MEM_Str, MEM_Dyn, etc. */
@@ -291,7 +294,8 @@ mem_apply_numeric_type(struct Mem *record);
  * (Mem) which are only defined there.
  */
 struct sql_context {
-	FuncDef *pFunc;		/* Pointer to function information */
+	/** Pointer to function object. */
+	struct func *func;
 	Mem *pMem;		/* Memory cell used to store aggregate context */
 	Vdbe *pVdbe;		/* The VM that owns this context */
 	int iOp;		/* Instruction number of OP_Function */
@@ -472,7 +476,17 @@ int sqlVdbeMemNumerify(Mem *);
 int sqlVdbeMemCast(Mem *, enum field_type type);
 int sqlVdbeMemFromBtree(BtCursor *, u32, u32, Mem *);
 void sqlVdbeMemRelease(Mem * p);
-int sqlVdbeMemFinalize(Mem *, FuncDef *);
+
+/**
+ * Memory cell pMem contains the context of an aggregate function.
+ * This routine calls the finalize method for that function.  The
+ * result of the aggregate is stored back into pMem.
+ *
+ * Returns -1 if the finalizer reports an error. 0 otherwise.
+ */
+int
+sql_vdbemem_finilize(struct Mem *mem, struct func *func);
+
 const char *sqlOpcodeName(int);
 int sqlVdbeMemGrow(Mem * pMem, int n, int preserve);
 int sqlVdbeMemClearAndResize(Mem * pMem, int n);
diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
index d014e8258..c59b740a7 100644
--- a/src/box/sql/vdbeapi.c
+++ b/src/box/sql/vdbeapi.c
@@ -370,8 +370,9 @@ sql_step(sql_stmt * pStmt)
 void *
 sql_user_data(sql_context * p)
 {
-	assert(p && p->pFunc);
-	return p->pFunc->pUserData;
+	assert(p != NULL && p->func != NULL &&
+	       p->func->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	return ((struct func_sql_builtin *)p->func)->user_data;
 }
 
 /*
@@ -416,7 +417,7 @@ createAggContext(sql_context * p, int nByte)
 	} else {
 		sqlVdbeMemClearAndResize(pMem, nByte);
 		pMem->flags = MEM_Agg;
-		pMem->u.pDef = p->pFunc;
+		pMem->u.func = p->func;
 		if (pMem->z) {
 			memset(pMem->z, 0, nByte);
 		}
@@ -432,7 +433,9 @@ createAggContext(sql_context * p, int nByte)
 void *
 sql_aggregate_context(sql_context * p, int nByte)
 {
-	assert(p && p->pFunc && p->pFunc->xFinalize);
+	assert(p != NULL && p->func != NULL &&
+	       p->func->def->language == FUNC_LANGUAGE_SQL_BUILTIN &&
+	       p->func->def->aggregate == FUNC_AGGREGATE_GROUP);
 	testcase(nByte < 0);
 	if ((p->pMem->flags & MEM_Agg) == 0) {
 		return createAggContext(p, nByte);
diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
index baeeb4624..2c6f5b279 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -39,6 +39,8 @@
 #include "box/schema.h"
 #include "box/tuple_format.h"
 #include "box/txn.h"
+#include "box/func.h"
+#include "box/func_def.h"
 #include "msgpuck/msgpuck.h"
 #include "sqlInt.h"
 #include "vdbeInt.h"
@@ -650,24 +652,11 @@ sqlVdbeJumpHere(Vdbe * p, int addr)
 	sqlVdbeChangeP2(p, addr, p->nOp);
 }
 
-/*
- * If the input FuncDef structure is ephemeral, then free it.  If
- * the FuncDef is not ephermal, then do nothing.
- */
-static void
-freeEphemeralFunction(sql * db, FuncDef * pDef)
-{
-	if ((pDef->funcFlags & SQL_FUNC_EPHEM) != 0) {
-		sqlDbFree(db, pDef);
-	}
-}
-
 static void vdbeFreeOpArray(sql *, Op *, int);
 
 static SQL_NOINLINE void
 freeP4FuncCtx(sql * db, sql_context * p)
 {
-	freeEphemeralFunction(db, p->pFunc);
 	sqlDbFree(db, p);
 }
 
@@ -690,10 +679,6 @@ freeP4(sql * db, int p4type, void *p4)
 	case P4_KEYINFO:
 		sql_key_info_unref(p4);
 		break;
-	case P4_FUNCDEF:{
-			freeEphemeralFunction(db, (FuncDef *) p4);
-			break;
-		}
 	case P4_MEM:
 		sqlValueFree((sql_value *) p4);
 		break;
@@ -1150,15 +1135,17 @@ displayP4(Op * pOp, char *zTemp, int nTemp)
 				sqlXPrintf(&x, "(binary)");
 			break;
 		}
-	case P4_FUNCDEF:{
-			FuncDef *pDef = pOp->p4.pFunc;
-			sqlXPrintf(&x, "%s(%d)", pDef->zName, pDef->nArg);
+	case P4_FUNC:{
+			struct func *func = pOp->p4.func;
+			sqlXPrintf(&x, "%s(%d)", func->def->name,
+				   func->def->param_count);
 			break;
 		}
 #if defined(SQL_DEBUG) || defined(VDBE_PROFILE)
 	case P4_FUNCCTX:{
-			FuncDef *pDef = pOp->p4.pCtx->pFunc;
-			sqlXPrintf(&x, "%s(%d)", pDef->zName, pDef->nArg);
+			struct func *func = pOp->p4.pCtx->func;
+			sqlXPrintf(&x, "%s(%d)", func->def->name,
+				   func->def->param_count);
 			break;
 		}
 #endif
diff --git a/src/box/sql/vdbemem.c b/src/box/sql/vdbemem.c
index 7bd5cdd71..b785739a6 100644
--- a/src/box/sql/vdbemem.c
+++ b/src/box/sql/vdbemem.c
@@ -39,6 +39,8 @@
 #include "sqlInt.h"
 #include "vdbeInt.h"
 #include "tarantoolInt.h"
+#include "box/func.h"
+#include "box/func_def.h"
 #include "box/schema.h"
 #include "box/tuple.h"
 #include "box/port.h"
@@ -306,40 +308,30 @@ sqlVdbeMemStringify(Mem * pMem, u8 bForce)
 	return 0;
 }
 
-/*
- * Memory cell pMem contains the context of an aggregate function.
- * This routine calls the finalize method for that function.  The
- * result of the aggregate is stored back into pMem.
- *
- * Return -1 if the finalizer reports an error. 0 otherwise.
- */
 int
-sqlVdbeMemFinalize(Mem * pMem, FuncDef * pFunc)
+sql_vdbemem_finilize(struct Mem *mem, struct func *func)
 {
 	int rc = 0;
-	if (ALWAYS(pFunc && pFunc->xFinalize)) {
-		assert((pMem->flags & MEM_Null) != 0 || pFunc == pMem->u.pDef);
-		struct sql_context ctx;
-		memset(&ctx, 0, sizeof(ctx));
-		ctx.pMem = pMem;
-		ctx.pFunc = pFunc;
-		struct region *region = &fiber()->gc;
-		size_t region_svp = region_used(region);
-		struct port args, ret;
-		port_vdbemem_create(&args, NULL, 0, &ctx);
-		rc = pFunc->xFinalize(NULL, &args, &ret);
-		assert((pMem->flags & MEM_Dyn) == 0);
-		if (pMem->szMalloc > 0)
-			sqlDbFree(pMem->db, pMem->zMalloc);
-		uint32_t size;
-		struct Mem *mem = (struct Mem *)port_get_vdbemem(&ret, &size);
-		if (mem != NULL) {
-			*pMem = mem[0];
-		} else {
-			rc = -1;
-		}
-		region_truncate(region, region_svp);
-	}
+	assert(func != NULL && func->def->aggregate == FUNC_AGGREGATE_GROUP);
+	struct sql_context ctx;
+	memset(&ctx, 0, sizeof(ctx));
+	ctx.pMem = mem;
+	ctx.func = func;
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+	struct port args, ret;
+	port_vdbemem_create(&args, NULL, 0, &ctx);
+	rc = ((struct func_sql_builtin *)func)->finalize(func, &args, &ret);
+	assert((mem->flags & MEM_Dyn) == 0);
+	if (mem->szMalloc > 0)
+		sqlDbFree(mem->db, mem->zMalloc);
+	uint32_t size;
+	struct Mem *val = (struct Mem *)port_get_vdbemem(&ret, &size);
+	if (val != NULL)
+		*mem = val[0];
+	else
+		rc = -1;
+	region_truncate(region, region_svp);
 	return rc;
 }
 
@@ -357,7 +349,7 @@ vdbeMemClearExternAndSetNull(Mem * p)
 {
 	assert(VdbeMemDynamic(p));
 	if (p->flags & MEM_Agg) {
-		sqlVdbeMemFinalize(p, p->u.pDef);
+		sql_vdbemem_finilize(p, p->u.func);
 		assert((p->flags & MEM_Agg) == 0);
 		testcase(p->flags & MEM_Dyn);
 	}
@@ -1222,8 +1214,8 @@ valueNew(sql * db, struct ValueNewStat4Ctx *p)
  * to be a scalar SQL function. If
  *
  *   * all function arguments are SQL literals,
- *   * one of the SQL_FUNC_CONSTANT or _SLOCHNG function flags is set, and
- *   * the SQL_FUNC_NEEDCOLL function flag is not set,
+ *   * the function is deterministic and the SQL_FUNC_NEEDCOLL
+ *     flag is not set,
  *
  * then this routine attempts to invoke the SQL function. Assuming no
  * error occurs, output parameter (*ppVal) is set to point to a value
@@ -1247,7 +1239,6 @@ valueFromFunction(sql * db,	/* The database connection */
 {
 	sql_value **apVal = 0;	/* Function arguments */
 	int nVal = 0;		/* Size of apVal[] array */
-	FuncDef *pFunc = 0;	/* Function definition */
 	sql_value *pVal = 0;	/* New value */
 	int rc = 0;	/* Return code */
 	ExprList *pList = 0;	/* Function arguments */
@@ -1258,13 +1249,11 @@ valueFromFunction(sql * db,	/* The database connection */
 	pList = p->x.pList;
 	if (pList)
 		nVal = pList->nExpr;
-	pFunc = sqlFindFunction(db, p->u.zToken, nVal, 0);
-	assert(pFunc);
-	if ((pFunc->funcFlags & (SQL_FUNC_CONSTANT | SQL_FUNC_SLOCHNG)) ==
-	    0 || (pFunc->funcFlags & SQL_FUNC_NEEDCOLL)
-	    ) {
+	struct func *func =
+		sql_func_by_signature(p->u.zToken, strlen(p->u.zToken), nVal);
+	assert(func != NULL);
+	if ((func->def->sql_flags & SQL_FUNC_NEEDCOLL) != 0)
 		return 0;
-	}
 	struct region *region = &fiber()->gc;
 	size_t region_svp = region_used(region);
 
@@ -1295,7 +1284,7 @@ valueFromFunction(sql * db,	/* The database connection */
 
 	struct port args, ret;
 	port_vdbemem_create(&args, (struct sql_value *)apVal, nVal, NULL);
-	if ((*pFunc->xSFunc)(NULL, &args, &ret) != 0) {
+	if (func_call(func, &args, &ret) != 0) {
 		rc = -1;
 		goto value_from_function_out;
 	}
diff --git a/src/box/sql/whereexpr.c b/src/box/sql/whereexpr.c
index befe4ce73..5736a4a0b 100644
--- a/src/box/sql/whereexpr.c
+++ b/src/box/sql/whereexpr.c
@@ -259,9 +259,9 @@ like_optimization_is_valid(Parse *pParse, Expr *pExpr, Expr **ppPrefix,
 	/* Result code to return. */
 	int rc;
 
-	if (!sql_is_like_func(db, pExpr, pnoCase)) {
+	if (!sql_is_like_func(pExpr, pnoCase))
 		return 0;
-	}
+
 	pList = pExpr->x.pList;
 	pLeft = pList->a[1].pExpr;
 	/* Value might be numeric */
diff --git a/src/lib/coll/coll.c b/src/lib/coll/coll.c
index fa27bf34e..275b1cfd7 100644
--- a/src/lib/coll/coll.c
+++ b/src/lib/coll/coll.c
@@ -36,6 +36,7 @@
 #include <unicode/ucol.h>
 #include <unicode/ucnv.h>
 #include <unicode/ucasemap.h>
+#include <unicode/ucnv.h>
 #include "tt_static.h"
 
 struct UCaseMap *icu_ucase_default_map = NULL;
diff --git a/test/box/function1.result b/test/box/function1.result
index c434b0067..7c3a93577 100644
--- a/test/box/function1.result
+++ b/test/box/function1.result
@@ -365,6 +365,125 @@ test_run:cmd("setopt delimiter ''");
 c:close()
 ---
 ...
+--
+-- gh-2233: Invoke Lua functions created outside SQL.
+--
+box.schema.func.create("function1.divide", {language = 'C', returns = 'number', is_deterministic = true, exports = {'LUA'}})
+---
+...
+box.execute('SELECT "function1.divide"()')
+---
+- error: wrong number of arguments to function function1.divide()
+...
+box.func["function1.divide"]:drop()
+---
+...
+box.schema.func.create("function1.divide", {language = 'C', returns = 'number', is_deterministic = true, exports = {'LUA', 'SQL'}})
+---
+...
+box.execute('SELECT "function1.divide"()')
+---
+- error: invalid argument
+...
+box.execute('SELECT "function1.divide"(6)')
+---
+- error: wrong number of arguments to function function1.divide()
+...
+box.execute('SELECT "function1.divide"(6, 3)')
+---
+- error: wrong number of arguments to function function1.divide()
+...
+box.func["function1.divide"]:drop()
+---
+...
+box.schema.func.create("function1.divide", {language = 'C', returns = 'number', param_list = {'number', 'number'}, is_deterministic = true, exports = {'LUA', 'SQL'}})
+---
+...
+box.execute('SELECT "function1.divide"()')
+---
+- error: wrong number of arguments to function function1.divide()
+...
+box.execute('SELECT "function1.divide"(6)')
+---
+- error: wrong number of arguments to function function1.divide()
+...
+box.execute('SELECT "function1.divide"(6, 3, 3)')
+---
+- error: wrong number of arguments to function function1.divide()
+...
+box.execute('SELECT "function1.divide"(6, 3)')
+---
+- metadata:
+  - name: '"function1.divide"(6, 3)'
+    type: number
+  rows:
+  - [2]
+...
+box.execute('SELECT "function1.divide"(5, 2)')
+---
+- metadata:
+  - name: '"function1.divide"(5, 2)'
+    type: number
+  rows:
+  - [2.5]
+...
+box.func["function1.divide"]:drop()
+---
+...
+function SUMMARIZE(a, b) return a + b end
+---
+...
+box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number', is_deterministic = true, param_list = {'number', 'number'}, exports = {'LUA', 'SQL'}})
+---
+...
+box.execute('SELECT summarize(1, 2)')
+---
+- metadata:
+  - name: summarize(1, 2)
+    type: number
+  rows:
+  - [3]
+...
+box.func.SUMMARIZE:drop()
+---
+...
+box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number', body = 'function (a, b) return a + b end', is_deterministic = true, param_list = {'number', 'number'}, exports = {'LUA', 'SQL'}})
+---
+...
+box.execute('SELECT summarize(1, 2)')
+---
+- metadata:
+  - name: summarize(1, 2)
+    type: number
+  rows:
+  - [3]
+...
+box.func.SUMMARIZE:drop()
+---
+...
+--
+-- gh-4113: Valid method to use Lua from SQL
+--
+box.execute('SELECT lua(\'return 1 + 1\')')
+---
+- metadata:
+  - name: lua('return 1 + 1')
+    type: any
+  rows:
+  - [2]
+...
+box.execute('SELECT lua(\'return box.cfg\')')
+---
+- error: 'Failed to execute SQL statement: Unsupported type passed from Lua'
+...
+box.execute('SELECT lua(\'return box.cfg.memtx_memory\')')
+---
+- metadata:
+  - name: lua('return box.cfg.memtx_memory')
+    type: any
+  rows:
+  - [107374182]
+...
 -- Test registered functions interface.
 function divide(a, b) return a / b end
 ---
diff --git a/test/box/function1.test.lua b/test/box/function1.test.lua
index dbbdcf8be..69c2de685 100644
--- a/test/box/function1.test.lua
+++ b/test/box/function1.test.lua
@@ -128,6 +128,41 @@ identifier.run_test(
 test_run:cmd("setopt delimiter ''");
 c:close()
 
+--
+-- gh-2233: Invoke Lua functions created outside SQL.
+--
+box.schema.func.create("function1.divide", {language = 'C', returns = 'number', is_deterministic = true, exports = {'LUA'}})
+box.execute('SELECT "function1.divide"()')
+box.func["function1.divide"]:drop()
+box.schema.func.create("function1.divide", {language = 'C', returns = 'number', is_deterministic = true, exports = {'LUA', 'SQL'}})
+box.execute('SELECT "function1.divide"()')
+box.execute('SELECT "function1.divide"(6)')
+box.execute('SELECT "function1.divide"(6, 3)')
+box.func["function1.divide"]:drop()
+box.schema.func.create("function1.divide", {language = 'C', returns = 'number', param_list = {'number', 'number'}, is_deterministic = true, exports = {'LUA', 'SQL'}})
+box.execute('SELECT "function1.divide"()')
+box.execute('SELECT "function1.divide"(6)')
+box.execute('SELECT "function1.divide"(6, 3, 3)')
+box.execute('SELECT "function1.divide"(6, 3)')
+box.execute('SELECT "function1.divide"(5, 2)')
+box.func["function1.divide"]:drop()
+
+function SUMMARIZE(a, b) return a + b end
+box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number', is_deterministic = true, param_list = {'number', 'number'}, exports = {'LUA', 'SQL'}})
+box.execute('SELECT summarize(1, 2)')
+box.func.SUMMARIZE:drop()
+
+box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number', body = 'function (a, b) return a + b end', is_deterministic = true, param_list = {'number', 'number'}, exports = {'LUA', 'SQL'}})
+box.execute('SELECT summarize(1, 2)')
+box.func.SUMMARIZE:drop()
+
+--
+-- gh-4113: Valid method to use Lua from SQL
+--
+box.execute('SELECT lua(\'return 1 + 1\')')
+box.execute('SELECT lua(\'return box.cfg\')')
+box.execute('SELECT lua(\'return box.cfg.memtx_memory\')')
+
 -- Test registered functions interface.
 function divide(a, b) return a / b end
 box.schema.func.create("divide", {comment = 'Divide two values'})
diff --git a/test/sql-tap/alias.test.lua b/test/sql-tap/alias.test.lua
index f9e6fc9fa..75391b305 100755
--- a/test/sql-tap/alias.test.lua
+++ b/test/sql-tap/alias.test.lua
@@ -25,14 +25,13 @@ test:plan(9)
 --
 
 counter = 0
-sequence = function()
-    counter = counter + 1
-    return counter
-end
 
 -- Function is declared as deterministic deliberately.
 -- Otherwise it would be called as much as it occurs in a query.
-box.internal.sql_create_function("sequence", "INT", sequence, 0, true)
+box.schema.func.create('SEQUENCE', {language = 'Lua', is_deterministic = true,
+                       returns = 'unsigned',
+                       body = 'function() counter = counter + 1 return counter end',
+                       exports = {'LUA', 'SQL'}})
 
 test:do_test(
     "alias-1.1",
@@ -220,6 +219,6 @@ test:do_test(
 --         -- </alias-3.1>
 --     })
 
-
+box.func.SEQUENCE:drop()
 
 test:finish_test()
diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua
index 98f906cf4..d83aafe2c 100755
--- a/test/sql-tap/check.test.lua
+++ b/test/sql-tap/check.test.lua
@@ -679,10 +679,12 @@ test:do_execsql_test(
 -- cannot be tested).
 --
 --reset_db()
-local function myfunc(x)
-    return x < 10
-end
-box.internal.sql_create_function("myfunc", "INT", myfunc)
+
+box.schema.func.create('MYFUNC', {language = 'Lua',
+                   is_deterministic = true,
+                   body = 'function(x) return x < 10 end',
+                   returns = 'boolean', param_list = {'number'},
+                   exports = {'LUA', 'SQL'}})
 
 test:do_execsql_test(
     7.1,
@@ -807,5 +809,8 @@ test:do_catchsql_test(
         -- </9.3>
     })
 
+
+box.func.MYFUNC:drop()
+
 test:finish_test()
 
diff --git a/test/sql-tap/func5.test.lua b/test/sql-tap/func5.test.lua
index f3706e631..4134d4036 100755
--- a/test/sql-tap/func5.test.lua
+++ b/test/sql-tap/func5.test.lua
@@ -71,13 +71,25 @@ test:do_execsql_test(
 
 global_counter = 0
 
-counter = function(str)
-    global_counter = global_counter + 1
-    return global_counter
-end
-
-box.internal.sql_create_function("counter1", "INT", counter, -1, false)
-box.internal.sql_create_function("counter2", "INT", counter, -1, true)
+box.schema.func.create('COUNTER1', {language = 'Lua', is_deterministic = false,
+            param_list = {'any'}, returns = 'integer',
+            exports = {'SQL', 'LUA'},
+            body = [[
+                function(str)
+                    global_counter = global_counter + 1
+                    return global_counter
+                end
+            ]]})
+
+box.schema.func.create('COUNTER2', {language = 'Lua', is_deterministic = true,
+                param_list = {'any'}, returns = 'integer',
+                exports = {'SQL', 'LUA'},
+                body = [[
+                    function(str)
+                            global_counter = global_counter + 1
+                            return global_counter
+                        end
+                ]]})
 
 test:do_execsql_test(
     "func5-2.2",
@@ -229,4 +241,7 @@ test:do_catchsql_test(
     }
 )
 
+box.func.COUNTER1:drop()
+box.func.COUNTER2:drop()
+
 test:finish_test()
diff --git a/test/sql-tap/lua_sql.test.lua b/test/sql-tap/lua_sql.test.lua
index b0913e7f4..6451a2cf5 100755
--- a/test/sql-tap/lua_sql.test.lua
+++ b/test/sql-tap/lua_sql.test.lua
@@ -1,19 +1,16 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
 NULL = require('msgpack').NULL
-test:plan(24)
-
-local function func1(a)
-    return a
-end
-local function allways_2(a)
-    return 2
-end
+test:plan(22)
 
 test:do_test(
     "lua_sql-1.0",
     function ()
-        box.internal.sql_create_function("func1", "INT", allways_2)
+        box.schema.func.create('FUNC1', {language = 'Lua',
+                        is_deterministic = true,
+                        body = 'function(a) return 2 end',
+                        param_list = {'any'}, returns = 'integer',
+                        exports = {'LUA', 'SQL'}})
         return test:execsql("select func1(1)")
     end,
     {2})
@@ -22,34 +19,16 @@ test:do_test(
 test:do_test(
     "lua_sql-1.1",
     function ()
-        box.internal.sql_create_function("func1", "INT", func1)
+        box.func.FUNC1:drop()
+        box.schema.func.create('FUNC1', {language = 'Lua',
+                        is_deterministic = true,
+                        body = 'function(a) return a end',
+                        param_list = {'scalar'}, returns = 'integer',
+                        exports = {'LUA', 'SQL'}})
         return test:execsql("select func1(1)")
     end,
     {1})
 
--- try to loose memory
-test:do_test(
-    "lua_sql-1.2",
-    function ()
-        for i = 1, 1000000, 1 do
-            box.internal.sql_create_function("func1", "INT", func1)
-        end
-        return test:execsql("select func1(1)")
-    end,
-    {1})
-
--- check sql polymorphism
-test:do_test(
-    "lua_sql-1.3",
-    function ()
-        box.internal.sql_create_function("allways_2", "INT", allways_2, 1) -- specify 1 arg
-        box.internal.sql_create_function("allways_2", "INT", func1)
-        box.internal.sql_create_function("allways_2", "INT", func1, 2)
-        box.internal.sql_create_function("allways_2", "INT", func1, 3)
-        return test:execsql("select allways_2(1)")
-    end,
-    {2})
-
 test:do_catchsql_test(
     "lua_sql-1.0",
     "select func3(1)",
@@ -72,7 +51,7 @@ for _, val in ipairs({
         {result})
 end
 
-local from_sql_to_lua = {
+from_sql_to_lua = {
     [1] = {1, 1},
     [2] = {"1", 1},
     [3] = {"1.5", 1.5},
@@ -81,14 +60,19 @@ local from_sql_to_lua = {
     [6] = {"x'0500'", "\u{0005}\u{0000}"},
     [7] = {"123123123123123", 123123123123123LL},
 }
-local json = require("json")
-local function check_from_sql_to_lua(i, arg)
-    if from_sql_to_lua[i][2] == arg then
-        return 1
-    end
-    return 0
-end
-box.internal.sql_create_function("check_from_sql_to_lua", "INT", check_from_sql_to_lua)
+
+box.schema.func.create('CHECK_FROM_SQL_TO_LUA', {language = 'Lua',
+                is_deterministic = true,
+                body = [[
+                    function(i, arg)
+                        if from_sql_to_lua[i][2] == arg then
+                            return 1
+                        end
+                        return 0
+                    end
+                ]],
+                param_list = {'integer', 'scalar'}, returns = 'integer',
+                exports = {'LUA', 'SQL'}})
 
 -- check for different types
 for i = 1, #from_sql_to_lua, 1 do
@@ -98,17 +82,23 @@ for i = 1, #from_sql_to_lua, 1 do
         {1})
 end
 
-local from_lua_to_sql = {
+from_lua_to_sql = {
     [1] = {1, 1},
     [2] = {"1.5", 1.5},
     [3] = {"'1'", "1"},
     [4] = {"true", true},
     [5] = {"false", false},
 }
-local function check_from_lua_to_sql(i)
-    return from_lua_to_sql[i][2]
-end
-box.internal.sql_create_function("check_from_lua_to_sql", "BLOB", check_from_lua_to_sql)
+
+box.schema.func.create('CHECK_FROM_LUA_TO_SQL', {language = 'Lua',
+                is_deterministic = true,
+                body = [[
+                    function(i)
+                        return from_lua_to_sql[i][2]
+                    end
+                ]],
+                param_list = {'integer'}, returns = 'scalar',
+                exports = {'LUA', 'SQL'}})
 
 -- check for different types
 for i = 1, #from_lua_to_sql, 1 do
@@ -118,14 +108,20 @@ for i = 1, #from_lua_to_sql, 1 do
         {1})
 end
 
-local from_lua_to_sql_bad = {
+from_lua_to_sql_bad = {
     [1] = NULL,
     [2] = 12LL, -- it is possible to support this type
 }
-local function check_from_lua_to_sql_bad(i)
-    return from_lua_to_sql_bad[i]
-end
-box.internal.sql_create_function("check_from_lua_to_sql_bad", "BLOB", check_from_lua_to_sql_bad)
+
+box.schema.func.create('CHECK_FROM_LUA_TO_SQL_BAD', {language = 'Lua',
+                is_deterministic = true,
+                body = [[
+                    function(i)
+                        return from_lua_to_sql_bad[i]
+                    end
+                ]],
+                param_list = {'integer'}, returns = 'scalar',
+                exports = {'LUA', 'SQL'}})
 
 for i = 1, #from_lua_to_sql_bad, 1 do
     test:do_catchsql_test(
@@ -134,16 +130,26 @@ for i = 1, #from_lua_to_sql_bad, 1 do
         {1, "/Unsupported/"})
 end
 
-local function allways_error()
-    error("my_error123")
-    return 1
-end
-box.internal.sql_create_function("allways_error", "INT", allways_error)
+box.schema.func.create('ALLWAYS_ERROR', {language = 'Lua',
+                is_deterministic = true,
+                body = [[
+                    function()
+                        error("my_error123")
+                        return 1
+                    end
+                ]],
+                param_list = {}, returns = 'integer',
+                exports = {'LUA', 'SQL'}})
 
 test:do_catchsql_test(
     "lua_sql-2.6",
     "select allways_error()",
     {1, "/my_error123/"})
 
+box.func.FUNC1:drop()
+box.func.CHECK_FROM_SQL_TO_LUA:drop()
+box.func.CHECK_FROM_LUA_TO_SQL:drop()
+box.func.CHECK_FROM_LUA_TO_SQL_BAD:drop()
+box.func.ALLWAYS_ERROR:drop()
 
 test:finish_test()
diff --git a/test/sql-tap/subquery.test.lua b/test/sql-tap/subquery.test.lua
index 7a3e270dc..743b7141e 100755
--- a/test/sql-tap/subquery.test.lua
+++ b/test/sql-tap/subquery.test.lua
@@ -710,17 +710,20 @@ test:do_execsql_test(
 -- for a matching column name did not cause an otherwise static subquery
 -- to become a dynamic (correlated) subquery.
 --
-local callcnt = 0
+callcnt = 0
 test:do_test(
     "subquery-5.1",
     function()
-        local function callcntproc(n)
-            callcnt = callcnt + 1
-            return n
-        end
-
-        callcnt = 0
-        box.internal.sql_create_function("callcnt", "INT", callcntproc)
+        box.schema.func.create('CALLCNT', {language = 'Lua',
+                   is_deterministic = true,
+                   param_list = {'integer'}, returns = 'integer',
+                   exports = {'LUA', 'SQL'},
+                   body = [[
+                       function(n)
+                            callcnt = callcnt + 1
+                            return n
+                       end
+                   ]]})
         return test:execsql [[
             CREATE TABLE t4(x TEXT,y  INT PRIMARY KEY);
             INSERT INTO t4 VALUES('one',1);
@@ -791,6 +794,8 @@ test:do_test(
         return callcnt
     end, 1)
 
+box.func.CALLCNT:drop()
+
 --############  was disable until we get #2652 fixed
 -- Ticket #2652.  Allow aggregate functions of outer queries inside
 -- a non-aggregate subquery.
diff --git a/test/sql-tap/trigger9.test.lua b/test/sql-tap/trigger9.test.lua
index e7e170b3d..64dcaff33 100755
--- a/test/sql-tap/trigger9.test.lua
+++ b/test/sql-tap/trigger9.test.lua
@@ -46,7 +46,10 @@ local function has_rowdata(sql)
 --     X(41, "X!cmd", [=[["expr","[lsearch [execsql \"explain $sql\"] RowData]>=0"]]=])
 end
 
-box.internal.sql_create_function('randstr', 'TEXT', test.randstr, 1)
+box.schema.func.create('RANDSTR', {language = 'Lua',
+                   body = 'function(n) return test.randstr(n) end',
+                   param_list = {'integer'}, returns = 'string',
+                   exports = {'LUA', 'SQL'}})
 
 -- MUST_WORK_TEST
 test:do_execsql_test(
@@ -450,5 +453,6 @@ test:do_execsql_test(
         -- </4.3>
     })
 
+box.func.RANDSTR:drop()
 
 test:finish_test()
diff --git a/test/sql-tap/where2.test.lua b/test/sql-tap/where2.test.lua
index 4116ca913..f267be8e6 100755
--- a/test/sql-tap/where2.test.lua
+++ b/test/sql-tap/where2.test.lua
@@ -231,7 +231,7 @@ test:do_execsql_test(
         EXPLAIN SELECT * FROM x1, x2 WHERE x=1 ORDER BY random();
     ]], {
         -- <where2-2.5>
-        "/random/"
+        "/RANDOM/"
         -- </where2-2.5>
     })
 
@@ -254,7 +254,7 @@ test:do_execsql_test(
         EXPLAIN SELECT * FROM x1, x2 WHERE x=1 ORDER BY abs(5);
     ]], {
         -- <where2-2.6>
-        "~/abs/"
+        "~/ABS/"
         -- </where2-2.6>
     })
 
diff --git a/test/sql/errinj.result b/test/sql/errinj.result
index 8846e5ee8..b4b44dda4 100644
--- a/test/sql/errinj.result
+++ b/test/sql/errinj.result
@@ -439,31 +439,6 @@ errinj.set("ERRINJ_WAL_DELAY", false)
 - ok
 ...
 --
--- gh-3931: Store regular identifiers in case-normal form
---
-errinj = box.error.injection
----
-...
-errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", true)
----
-- ok
-...
-box.execute("CREATE TABLE hello (id INT primary key,x INT,y INT);")
----
-- error: Failed to allocate 6 bytes in sqlDbMallocRawNN for res
-...
-dummy_f = function(int) return 1 end
----
-...
-box.internal.sql_create_function("counter1", "INT", dummy_f, -1, false)
----
-- error: Failed to allocate 9 bytes in region_alloc for res
-...
-errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false)
----
-- ok
-...
---
 -- Tests which are aimed at verifying work of commit/rollback
 -- triggers on _ck_constraint space.
 --
diff --git a/test/sql/errinj.test.lua b/test/sql/errinj.test.lua
index 48b80a443..1250bf34b 100644
--- a/test/sql/errinj.test.lua
+++ b/test/sql/errinj.test.lua
@@ -140,16 +140,6 @@ box.execute("UPDATE t SET id = 2;")
 -- Finish drop space.
 errinj.set("ERRINJ_WAL_DELAY", false)
 
---
--- gh-3931: Store regular identifiers in case-normal form
---
-errinj = box.error.injection
-errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", true)
-box.execute("CREATE TABLE hello (id INT primary key,x INT,y INT);")
-dummy_f = function(int) return 1 end
-box.internal.sql_create_function("counter1", "INT", dummy_f, -1, false)
-errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false)
-
 --
 -- Tests which are aimed at verifying work of commit/rollback
 -- triggers on _ck_constraint space.
diff --git a/test/sql/func-recreate.result b/test/sql/func-recreate.result
index 73fb03cc4..da6d6e770 100644
--- a/test/sql/func-recreate.result
+++ b/test/sql/func-recreate.result
@@ -12,7 +12,15 @@ box.execute('pragma sql_default_engine=\''..engine..'\'')
 fiber = require('fiber')
 ---
 ...
-box.internal.sql_create_function('WAITFOR', 'INT', function (n) fiber.sleep(n) return n end)
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+box.schema.func.create('WAITFOR', {language = 'Lua',
+                   body = 'function (n) fiber.sleep(n) return n end',
+                   param_list = {'integer'}, returns = 'integer',
+                   exports = {'LUA', 'SQL'}})
+test_run:cmd("setopt delimiter ''");
 ---
 ...
 ch = fiber.channel(1)
@@ -24,10 +32,19 @@ _ = fiber.create(function () ch:put(box.execute('select WAITFOR(0.2)')) end)
 fiber.sleep(0.1)
 ---
 ...
-box.internal.sql_create_function('WAITFOR', 'INT', function (n) require('fiber').sleep(n) return n end)
+box.func.WAITFOR:drop()
+---
+...
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+box.schema.func.create('WAITFOR', {language = 'Lua',
+                   body = 'function (n) fiber.sleep(n) return n end',
+                   param_list = {'integer'}, returns = 'integer',
+                   exports = {'LUA', 'SQL'}})
+test_run:cmd("setopt delimiter ''");
 ---
-- error: 'Failed to create function ''WAITFOR'': unable to create function due to
-    active statements'
 ...
 ch:get()
 ---
@@ -37,6 +54,20 @@ ch:get()
   rows:
   - [0.2]
 ...
-box.internal.sql_create_function('WAITFOR', 'INT', function (n) require('fiber').sleep(n) return n end)
+box.func.WAITFOR:drop()
+---
+...
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+box.schema.func.create('WAITFOR', {language = 'Lua',
+                   body = 'function (n) fiber.sleep(n) return n end',
+                   param_list = {'integer'}, returns = 'integer',
+                   exports = {'LUA', 'SQL'}})
+test_run:cmd("setopt delimiter ''");
+---
+...
+box.func.WAITFOR:drop()
 ---
 ...
diff --git a/test/sql/func-recreate.test.lua b/test/sql/func-recreate.test.lua
index 753e9ca4d..1b6e870f3 100644
--- a/test/sql/func-recreate.test.lua
+++ b/test/sql/func-recreate.test.lua
@@ -4,14 +4,36 @@ box.execute('pragma sql_default_engine=\''..engine..'\'')
 
 -- Check errors during function create process
 fiber = require('fiber')
-box.internal.sql_create_function('WAITFOR', 'INT', function (n) fiber.sleep(n) return n end)
+
+test_run:cmd("setopt delimiter ';'")
+box.schema.func.create('WAITFOR', {language = 'Lua',
+                   body = 'function (n) fiber.sleep(n) return n end',
+                   param_list = {'integer'}, returns = 'integer',
+                   exports = {'LUA', 'SQL'}})
+test_run:cmd("setopt delimiter ''");
 
 ch = fiber.channel(1)
 
 _ = fiber.create(function () ch:put(box.execute('select WAITFOR(0.2)')) end)
 fiber.sleep(0.1)
 
-box.internal.sql_create_function('WAITFOR', 'INT', function (n) require('fiber').sleep(n) return n end)
+box.func.WAITFOR:drop()
+
+test_run:cmd("setopt delimiter ';'")
+box.schema.func.create('WAITFOR', {language = 'Lua',
+                   body = 'function (n) fiber.sleep(n) return n end',
+                   param_list = {'integer'}, returns = 'integer',
+                   exports = {'LUA', 'SQL'}})
+test_run:cmd("setopt delimiter ''");
+
 ch:get()
-box.internal.sql_create_function('WAITFOR', 'INT', function (n) require('fiber').sleep(n) return n end)
+box.func.WAITFOR:drop()
+
+test_run:cmd("setopt delimiter ';'")
+box.schema.func.create('WAITFOR', {language = 'Lua',
+                   body = 'function (n) fiber.sleep(n) return n end',
+                   param_list = {'integer'}, returns = 'integer',
+                   exports = {'LUA', 'SQL'}})
+test_run:cmd("setopt delimiter ''");
 
+box.func.WAITFOR:drop()
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 02/12] sql: get rid of LIKELY, UNLIKELY and LIKEHOOD
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
                   ` (3 preceding siblings ...)
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 12/12] sql: use schema's func hash instead of FuncDef hash Kirill Shcherbatov
@ 2019-07-10 11:01 ` Kirill Shcherbatov
  2019-07-10 19:02   ` [tarantool-patches] " Konstantin Osipov
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 03/12] sql: put analyze helpers to FuncDef cache Kirill Shcherbatov
                   ` (6 subsequent siblings)
  11 siblings, 1 reply; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:01 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

Got rid of LIKELY, UNLIKELY and LIKEHOOD builtins because they
are not a part of the Standard and make source code more
complicated.

Needed for #4182
---
 src/box/sql/expr.c               |  26 +--
 src/box/sql/func.c               |  20 +--
 src/box/sql/resolve.c            |  52 ------
 src/box/sql/sqlInt.h             |   3 -
 src/box/sql/where.c              | 166 +++++++-----------
 src/box/sql/whereInt.h           |   1 -
 src/box/sql/whereexpr.c          |   6 -
 test/sql-tap/collation.test.lua  |   2 +-
 test/sql-tap/func3.test.lua      | 289 -------------------------------
 test/sql-tap/orderby5.test.lua   |   2 +-
 test/sql-tap/select4.test.lua    |  12 +-
 test/sql-tap/sql-errors.test.lua |  12 +-
 test/sql-tap/whereG.test.lua     |  87 +++-------
 13 files changed, 99 insertions(+), 579 deletions(-)
 delete mode 100755 test/sql-tap/func3.test.lua

diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
index d7104d8a0..20e490b96 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -176,23 +176,13 @@ sqlExprAddCollateString(Parse * pParse, Expr * pExpr, const char *zC)
 	return sqlExprAddCollateToken(pParse, pExpr, &s, 0);
 }
 
-/*
- * Skip over any TK_COLLATE operators and any unlikely()
- * or likelihood() function at the root of an expression.
- */
+/* Skip over any TK_COLLATE operators. */
 Expr *
 sqlExprSkipCollate(Expr * pExpr)
 {
 	while (pExpr && ExprHasProperty(pExpr, EP_Skip)) {
-		if (ExprHasProperty(pExpr, EP_Unlikely)) {
-			assert(!ExprHasProperty(pExpr, EP_xIsSelect));
-			assert(pExpr->x.pList->nExpr > 0);
-			assert(pExpr->op == TK_FUNCTION);
-			pExpr = pExpr->x.pList->a[0].pExpr;
-		} else {
-			assert(pExpr->op == TK_COLLATE);
-			pExpr = pExpr->pLeft;
-		}
+		assert(pExpr->op == TK_COLLATE);
+		pExpr = pExpr->pLeft;
 	}
 	return pExpr;
 }
@@ -3981,16 +3971,6 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 				break;
 			}
 
-			/* The UNLIKELY() function is a no-op.  The result is the value
-			 * of the first argument.
-			 */
-			if (pDef->funcFlags & SQL_FUNC_UNLIKELY) {
-				assert(nFarg >= 1);
-				return sqlExprCodeTarget(pParse,
-							     pFarg->a[0].pExpr,
-							     target);
-			}
-
 			for (i = 0; i < nFarg; i++) {
 				if (i < 32
 				    && sqlExprIsConstant(pFarg->a[i].
diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index f0e1b1f0c..29f2b5c6a 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -603,12 +603,14 @@ ICU_CASE_CONVERT(Upper);
 
 
 /*
- * Some functions like COALESCE() and IFNULL() and UNLIKELY() are implemented
- * as VDBE code so that unused argument values do not have to be computed.
- * However, we still need some kind of function implementation for this
- * routines in the function table.  The noopFunc macro provides this.
- * noopFunc will never be called so it doesn't matter what the implementation
- * is.  We might as well use the "version()" function as a substitute.
+ * Some functions like COALESCE() and IFNULL() are implemented
+ * as VDBE code so that unused argument values do not have to be
+ * computed.
+ * However, we still need some kind of function implementation for
+ * this routines in the function table. The noopFunc macro
+ * provides this. noopFunc will never be called so it doesn't
+ * matter what the implementation is. We might as well use the
+ * "version()" function as a substitute.
  */
 #define noopFunc sql_func_version /* Substitute function - never called */
 
@@ -1780,12 +1782,6 @@ sqlRegisterBuiltinFunctions(void)
 	 * For peak efficiency, put the most frequently used function last.
 	 */
 	static FuncDef aBuiltinFunc[] = {
-		FUNCTION2(unlikely, 1, 0, 0, noopFunc, SQL_FUNC_UNLIKELY,
-			  FIELD_TYPE_BOOLEAN),
-		FUNCTION2(likelihood, 2, 0, 0, noopFunc, SQL_FUNC_UNLIKELY,
-			  FIELD_TYPE_BOOLEAN),
-		FUNCTION2(likely, 1, 0, 0, noopFunc, SQL_FUNC_UNLIKELY,
-			  FIELD_TYPE_BOOLEAN),
 		FUNCTION_COLL(trim, 1, 3, 0, trim_func_one_arg),
 		FUNCTION_COLL(trim, 2, 3, 0, trim_func_two_args),
 		FUNCTION_COLL(trim, 3, 3, 0, trim_func_three_args),
diff --git a/src/box/sql/resolve.c b/src/box/sql/resolve.c
index 0b90edd06..7c81b1980 100644
--- a/src/box/sql/resolve.c
+++ b/src/box/sql/resolve.c
@@ -496,24 +496,6 @@ sql_expr_new_column(struct sql *db, struct SrcList *src_list, int src_idx,
 	return expr;
 }
 
-/*
- * Expression p should encode a floating point value between 1.0 and 0.0.
- * Return 1024 times this value.  Or return -1 if p is not a floating point
- * value between 1.0 and 0.0.
- */
-static int
-exprProbability(Expr * p)
-{
-	double r = -1.0;
-	if (p->op != TK_FLOAT)
-		return -1;
-	sqlAtoF(p->u.zToken, &r, sqlStrlen30(p->u.zToken));
-	assert(r >= 0.0);
-	if (r > 1.0)
-		return -1;
-	return (int)(r * 134217728.0);
-}
-
 /*
  * This routine is callback for sqlWalkExpr().
  *
@@ -613,40 +595,6 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 			} else {
 				is_agg = pDef->xFinalize != 0;
 				pExpr->type = pDef->ret_type;
-				const char *err =
-					"second argument to likelihood() must "\
-					"be a constant between 0.0 and 1.0";
-				if (pDef->funcFlags & SQL_FUNC_UNLIKELY) {
-					ExprSetProperty(pExpr,
-							EP_Unlikely | EP_Skip);
-					if (n == 2) {
-						pExpr->iTable =
-						    exprProbability(pList->a[1].
-								    pExpr);
-						if (pExpr->iTable < 0) {
-							diag_set(ClientError,
-								 ER_ILLEGAL_PARAMS,
-								 err);
-							pParse->is_aborted =
-								true;
-							pNC->nErr++;
-						}
-					} else {
-						/* EVIDENCE-OF: R-61304-29449 The unlikely(X) function is
-						 * equivalent to likelihood(X, 0.0625).
-						 * EVIDENCE-OF: R-01283-11636 The unlikely(X) function is
-						 * short-hand for likelihood(X,0.0625).
-						 * EVIDENCE-OF: R-36850-34127 The likely(X) function is short-hand
-						 * for likelihood(X,0.9375).
-						 * EVIDENCE-OF: R-53436-40973 The likely(X) function is equivalent
-						 * to likelihood(X,0.9375).
-						 */
-						/* TUNING: unlikely() probability is 0.0625.  likely() is 0.9375 */
-						pExpr->iTable =
-						    pDef->zName[0] ==
-						    'u' ? 8388608 : 125829120;
-					}
-				}
 				if (pDef->
 				    funcFlags & (SQL_FUNC_CONSTANT |
 						 SQL_FUNC_SLOCHNG)) {
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 9305f7d5c..8ab9804fe 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -1284,7 +1284,6 @@ struct FuncDestructor {
 #define SQL_FUNC_TYPEOF   0x0080	/* Built-in typeof() function */
 #define SQL_FUNC_COUNT    0x0100	/* Built-in count(*) aggregate */
 #define SQL_FUNC_COALESCE 0x0200	/* Built-in coalesce() or ifnull() */
-#define SQL_FUNC_UNLIKELY 0x0400	/* Built-in unlikely() function */
 #define SQL_FUNC_CONSTANT 0x0800	/* Constant inputs give a constant output */
 #define SQL_FUNC_MINMAX   0x1000	/* True for min() and max() aggregates */
 #define SQL_FUNC_SLOCHNG  0x2000	/* "Slow Change". Value constant during a
@@ -1673,7 +1672,6 @@ struct Expr {
 	int iTable;		/* TK_COLUMN: cursor number of table holding column
 				 * TK_REGISTER: register number
 				 * TK_TRIGGER: 1 -> new, 0 -> old
-				 * EP_Unlikely:  134217728 times likelihood
 				 * TK_SELECT: 1st register of result vector
 				 */
 	ynVar iColumn;		/* TK_COLUMN: column index.
@@ -1711,7 +1709,6 @@ struct Expr {
 #define EP_Static    0x008000	/* Held in memory not obtained from malloc() */
 #define EP_MemToken  0x010000	/* Need to sqlDbFree() Expr.zToken */
 #define EP_NoReduce  0x020000	/* Cannot EXPRDUP_REDUCE this Expr */
-#define EP_Unlikely  0x040000	/* unlikely() or likelihood() function */
 #define EP_ConstFunc 0x080000	/* A sql_FUNC_CONSTANT or _SLOCHNG function */
 #define EP_CanBeNull 0x100000	/* Can be null despite NOT NULL constraint */
 #define EP_Subquery  0x200000	/* Tree contains a TK_SELECT operator */
diff --git a/src/box/sql/where.c b/src/box/sql/where.c
index 5458c6a75..f93c7bda0 100644
--- a/src/box/sql/where.c
+++ b/src/box/sql/where.c
@@ -1111,20 +1111,13 @@ whereKeyStats(Parse * pParse,	/* Database connection */
  * bound on a range scan. Without considering pTerm, it is estimated
  * that the scan will visit nNew rows. This function returns the number
  * estimated to be visited after taking pTerm into account.
- *
- * If the user explicitly specified a likelihood() value for this term,
- * then the return value is the likelihood multiplied by the number of
- * input rows. Otherwise, this function assumes that an "IS NOT NULL" term
- * has a likelihood of 0.50, and any other term a likelihood of 0.25.
  */
 static LogEst
 whereRangeAdjust(WhereTerm * pTerm, LogEst nNew)
 {
 	LogEst nRet = nNew;
 	if (pTerm) {
-		if (pTerm->truthProb <= 0) {
-			nRet += pTerm->truthProb;
-		} else if ((pTerm->wtFlags & TERM_VNULL) == 0) {
+		if ((pTerm->wtFlags & TERM_VNULL) == 0) {
 			nRet -= 20;
 			assert(20 == sqlLogEst(4));
 		}
@@ -1469,17 +1462,6 @@ whereRangeScanEst(Parse * pParse,	/* Parsing & code generating context */
 	nNew = whereRangeAdjust(pLower, nOut);
 	nNew = whereRangeAdjust(pUpper, nNew);
 
-	/* TUNING: If there is both an upper and lower limit and neither limit
-	 * has an application-defined likelihood(), assume the range is
-	 * reduced by an additional 75%. This means that, by default, an open-ended
-	 * range query (e.g. col > ?) is assumed to match 1/4 of the rows in the
-	 * index. While a closed range (e.g. col BETWEEN ? AND ?) is estimated to
-	 * match 1/64 of the index.
-	 */
-	if (pLower && pLower->truthProb > 0 && pUpper && pUpper->truthProb > 0) {
-		nNew -= 20;
-	}
-
 	nOut -= (pLower != 0) + (pUpper != 0);
 	if (nNew < 10)
 		nNew = 10;
@@ -1624,8 +1606,8 @@ whereTermPrint(WhereTerm * pTerm, int iTerm)
 					 pTerm->leftCursor);
 		}
 		sqlDebugPrintf
-		    ("TERM-%-3d %p %s %-12s prob=%-3d op=0x%03x wtFlags=0x%04x",
-		     iTerm, pTerm, zType, zLeft, pTerm->truthProb,
+		    ("TERM-%-3d %p %s %-12s op=0x%03x wtFlags=0x%04x",
+		     iTerm, pTerm, zType, zLeft,
 		     pTerm->eOperator, pTerm->wtFlags);
 		if (pTerm->iField) {
 			sqlDebugPrintf(" iField=%d\n", pTerm->iField);
@@ -2117,11 +2099,6 @@ whereLoopInsert(WhereLoopBuilder * pBuilder, WhereLoop * pTemplate)
  * Adjust the WhereLoop.nOut value downward to account for terms of the
  * WHERE clause that reference the loop but which are not used by an
  * index.
-*
- * For every WHERE clause term that is not used by the index
- * and which has a truth probability assigned by one of the likelihood(),
- * likely(), or unlikely() SQL functions, reduce the estimated number
- * of output rows by the probability specified.
  *
  * TUNING:  For every WHERE clause term that is not used by the index
  * and which does not have an assigned truth probability, heuristics
@@ -2170,27 +2147,21 @@ whereLoopOutputAdjust(WhereClause * pWC,	/* The WHERE clause */
 				break;
 		}
 		if (j < 0) {
-			if (pTerm->truthProb <= 0) {
-				/* If a truth probability is specified using the likelihood() hints,
-				 * then use the probability provided by the application.
-				 */
-				pLoop->nOut += pTerm->truthProb;
-			} else {
-				/* In the absence of explicit truth probabilities, use heuristics to
-				 * guess a reasonable truth probability.
-				 */
-				pLoop->nOut--;
-				if ((pTerm->eOperator & WO_EQ) != 0) {
-					Expr *pRight = pTerm->pExpr->pRight;
-					if (sqlExprIsInteger(pRight, &k)
-					    && k >= (-1) && k <= 1) {
-						k = 10;
-					} else {
-						k = 20;
-					}
-					if (iReduce < k)
-						iReduce = k;
+			/* In the absence of explicit truth
+			 * probabilities, use heuristics to
+			 * guess a reasonable truth probability.
+			 */
+			pLoop->nOut--;
+			if ((pTerm->eOperator & WO_EQ) != 0) {
+				Expr *pRight = pTerm->pExpr->pRight;
+				if (sqlExprIsInteger(pRight, &k)
+					&& k >= (-1) && k <= 1) {
+					k = 10;
+				} else {
+					k = 20;
 				}
+				if (iReduce < k)
+					iReduce = k;
 			}
 		}
 	}
@@ -2504,56 +2475,53 @@ whereLoopAddBtreeIndex(WhereLoopBuilder * pBuilder,	/* The WhereLoop factory */
 			assert(eOp & (WO_ISNULL | WO_EQ | WO_IN));
 
 			assert(pNew->nOut == saved_nOut);
-			if (pTerm->truthProb <= 0 && probe->space_id != 0) {
-				assert((eOp & WO_IN) || nIn == 0);
-				testcase(eOp & WO_IN);
-				pNew->nOut += pTerm->truthProb;
-				pNew->nOut -= nIn;
-			} else {
-				tRowcnt nOut = 0;
-				if (nInMul == 0
-				    && stat->sample_count
-				    && pNew->nEq <= stat->sample_field_count
-				    && ((eOp & WO_IN) == 0
-					|| !ExprHasProperty(pTerm->pExpr,
-							    EP_xIsSelect))
-				    ) {
-					Expr *pExpr = pTerm->pExpr;
-					if ((eOp & (WO_EQ | WO_ISNULL))
-					    != 0) {
-						testcase(eOp & WO_EQ);
-						testcase(eOp & WO_ISNULL);
-						rc = whereEqualScanEst(pParse,
-								       pBuilder,
-								       pExpr->pRight,
-								       &nOut);
-					} else {
-						rc = whereInScanEst(pParse,
-								    pBuilder,
-								    pExpr->x.pList,
-								    &nOut);
-					}
-					if (rc != 0)
-						break;	/* Jump out of the pTerm loop */
-					if (nOut) {
-						pNew->nOut =
-						    sqlLogEst(nOut);
-						if (pNew->nOut > saved_nOut)
-							pNew->nOut = saved_nOut;
-						pNew->nOut -= nIn;
-					}
+			tRowcnt nOut = 0;
+			if (nInMul == 0
+				&& stat->sample_count
+				&& pNew->nEq <= stat->sample_field_count
+				&& ((eOp & WO_IN) == 0
+				|| !ExprHasProperty(pTerm->pExpr,
+							EP_xIsSelect))
+				) {
+				Expr *pExpr = pTerm->pExpr;
+				if ((eOp & (WO_EQ | WO_ISNULL))
+					!= 0) {
+					testcase(eOp & WO_EQ);
+					testcase(eOp & WO_ISNULL);
+					rc = whereEqualScanEst(pParse,
+								pBuilder,
+								pExpr->pRight,
+								&nOut);
+				} else {
+					rc = whereInScanEst(pParse,
+								pBuilder,
+								pExpr->x.pList,
+								&nOut);
 				}
-				if (nOut == 0) {
-					pNew->nOut +=
-						(index_field_tuple_est(probe, nEq) -
-						 index_field_tuple_est(probe, nEq -1));
-					if (eOp & WO_ISNULL) {
-						/* TUNING: If there is no likelihood() value, assume that a
-						 * "col IS NULL" expression matches twice as many rows
-						 * as (col=?).
-						 */
-						pNew->nOut += 10;
-					}
+				/* Jump out of the pTerm loop */
+				if (rc != 0)
+					break;
+				if (nOut) {
+					pNew->nOut =
+						sqlLogEst(nOut);
+					if (pNew->nOut > saved_nOut)
+						pNew->nOut = saved_nOut;
+					pNew->nOut -= nIn;
+				}
+			}
+			if (nOut == 0) {
+				pNew->nOut +=
+					(index_field_tuple_est(probe, nEq) -
+						index_field_tuple_est(probe, nEq -1));
+				if (eOp & WO_ISNULL) {
+					/*
+					 * Assume that a
+					 * "col IS NULL"
+					 * expression matches
+					 * twice as many rows
+					 *  as (col=?).
+					 */
+					pNew->nOut += 10;
 				}
 			}
 		}
@@ -3038,13 +3006,7 @@ whereLoopAddOr(WhereLoopBuilder * pBuilder, Bitmask mPrereq, Bitmask mUnusable)
 				 * errors, it may be that the cost of the OR-scan is equal to its
 				 * most expensive sub-scan. Add the smallest possible penalty
 				 * (equivalent to multiplying the cost by 1.07) to ensure that
-				 * this does not happen. Otherwise, for WHERE clauses such as the
-				 * following where there is an index on "y":
-				 *
-				 *     WHERE likelihood(x=?, 0.99) OR y=?
-				 *
-				 * the planner may elect to "OR" together a full-table scan and an
-				 * index lookup. And other similarly odd results.
+				 * this does not happen.
 				 */
 				pNew->rRun = sSum.a[i].rRun + 1;
 				pNew->nOut = sSum.a[i].nOut;
diff --git a/src/box/sql/whereInt.h b/src/box/sql/whereInt.h
index e39e00210..01258e289 100644
--- a/src/box/sql/whereInt.h
+++ b/src/box/sql/whereInt.h
@@ -248,7 +248,6 @@ struct WherePath {
 struct WhereTerm {
 	Expr *pExpr;		/* Pointer to the subexpression that is this term */
 	WhereClause *pWC;	/* The clause this term is part of */
-	LogEst truthProb;	/* Probability of truth for this expression */
 	u16 wtFlags;		/* TERM_xxx bit flags.  See below */
 	u16 eOperator;		/* A WO_xx value describing <op> */
 	u8 nChild;		/* Number of children that must disable us */
diff --git a/src/box/sql/whereexpr.c b/src/box/sql/whereexpr.c
index a88f96407..befe4ce73 100644
--- a/src/box/sql/whereexpr.c
+++ b/src/box/sql/whereexpr.c
@@ -110,11 +110,6 @@ whereClauseInsert(WhereClause * pWC, Expr * p, u16 wtFlags)
 		pWC->nSlot = sqlMallocSize(pWC->a) / sizeof(pWC->a[0]);
 	}
 	pTerm = &pWC->a[idx = pWC->nTerm++];
-	if (p && ExprHasProperty(p, EP_Unlikely)) {
-		pTerm->truthProb = sqlLogEst(p->iTable) - 270;
-	} else {
-		pTerm->truthProb = 1;
-	}
 	pTerm->pExpr = sqlExprSkipCollate(p);
 	pTerm->wtFlags = wtFlags;
 	pTerm->pWC = pWC;
@@ -360,7 +355,6 @@ static void
 markTermAsChild(WhereClause * pWC, int iChild, int iParent)
 {
 	pWC->a[iChild].iParent = iParent;
-	pWC->a[iChild].truthProb = pWC->a[iParent].truthProb;
 	pWC->a[iParent].nChild++;
 }
 
diff --git a/test/sql-tap/collation.test.lua b/test/sql-tap/collation.test.lua
index 79547361c..d41064d04 100755
--- a/test/sql-tap/collation.test.lua
+++ b/test/sql-tap/collation.test.lua
@@ -494,7 +494,7 @@ local like_testcases =
         {0, {"Aab", "aaa"}} },
     {"2.1.2",
         "EXPLAIN QUERY PLAN SELECT * FROM tx1 WHERE s1 LIKE 'A%';",
-        {0, {0, 0, 0, "SEARCH TABLE TX1 USING COVERING INDEX I1 (S1>? AND S1<?) (~16384 rows)"}}},
+        {0, {0, 0, 0, "SEARCH TABLE TX1 USING COVERING INDEX I1 (S1>? AND S1<?) (~65536 rows)"}}},
     {"2.2.0",
         "PRAGMA case_sensitive_like = true;",
         {0}},
diff --git a/test/sql-tap/func3.test.lua b/test/sql-tap/func3.test.lua
deleted file mode 100755
index 6d6411ca6..000000000
--- a/test/sql-tap/func3.test.lua
+++ /dev/null
@@ -1,289 +0,0 @@
-#!/usr/bin/env tarantool
-test = require("sqltester")
-test:plan(25)
-
---!./tcltestrunner.lua
--- 2010 August 27
---
--- The author disclaims copyright to this source code.  In place of
--- a legal notice, here is a blessing:
---
---    May you do good and not evil.
---    May you find forgiveness for yourself and forgive others.
---    May you share freely, never taking more than you give.
---
--------------------------------------------------------------------------
--- This file implements regression tests for sql library. The
--- focus of this file is testing that destructor functions associated
--- with functions created using sql_create_function_v2() is
--- correctly invoked.
---
--- ["set","testdir",[["file","dirname",["argv0"]]]]
--- ["source",[["testdir"],"\/tester.tcl"]]
--- MUST_WORK_TEST built-in functions check desrtoy api of sql_create_function_v2
-
--- EVIDENCE-OF: R-41921-05214 The likelihood(X,Y) function returns
--- argument X unchanged.
---
-test:do_execsql_test(
-    "func3-5.1",
-    [[
-        SELECT likelihood(9223372036854775807, 0.5);
-    ]], {
-        -- <func3-5.1>
-        9223372036854775807LL
-        -- </func3-5.1>
-    })
-
-test:do_execsql_test(
-    "func3-5.2",
-    [[
-        SELECT likelihood(-9223372036854775808, 0.5);
-    ]], {
-        -- <func3-5.2>
-        -9223372036854775808LL
-        -- </func3-5.2>
-    })
-
-test:do_execsql_test(
-    "func3-5.3",
-    [[
-        SELECT likelihood(14.125, 0.5);
-    ]], {
-        -- <func3-5.3>
-        14.125
-        -- </func3-5.3>
-    })
-
-test:do_execsql_test(
-    "func3-5.4",
-    [[
-        SELECT likelihood(NULL, 0.5);
-    ]], {
-        -- <func3-5.4>
-        ""
-        -- </func3-5.4>
-    })
-
-test:do_execsql_test(
-    "func3-5.5",
-    [[
-        SELECT likelihood('test-string', 0.5);
-    ]], {
-        -- <func3-5.5>
-        "test-string"
-        -- </func3-5.5>
-    })
-
-test:do_execsql_test(
-    "func3-5.6",
-    [[
-        SELECT quote(likelihood(x'010203000405', 0.5));
-    ]], {
-        -- <func3-5.6>
-        "X'010203000405'"
-        -- </func3-5.6>
-    })
-
--- EVIDENCE-OF: R-44133-61651 The value Y in likelihood(X,Y) must be a
--- floating point constant between 0.0 and 1.0, inclusive.
---
-test:do_execsql_test(
-    "func3-5.7",
-    [[
-        SELECT likelihood(123, 1.0), likelihood(456, 0.0);
-    ]], {
-        -- <func3-5.7>
-        123, 456
-        -- </func3-5.7>
-    })
-
-test:do_catchsql_test(
-    "func3-5.8",
-    [[
-        SELECT likelihood(123, 1.000001);
-    ]], {
-        -- <func3-5.8>
-        1, "Illegal parameters, second argument to likelihood() must be a constant between 0.0 and 1.0"
-        -- </func3-5.8>
-    })
-
-test:do_catchsql_test(
-    "func3-5.9",
-    [[
-        SELECT likelihood(123, -0.000001);
-    ]], {
-        -- <func3-5.9>
-        1, "Illegal parameters, second argument to likelihood() must be a constant between 0.0 and 1.0"
-        -- </func3-5.9>
-    })
-
-test:do_catchsql_test(
-    "func3-5.10",
-    [[
-        SELECT likelihood(123, 0.5+0.3);
-    ]], {
-        -- <func3-5.10>
-        1, "Illegal parameters, second argument to likelihood() must be a constant between 0.0 and 1.0"
-        -- </func3-5.10>
-    })
-
--- EVIDENCE-OF: R-28535-44631 The likelihood(X) function is a no-op that
--- the code generator optimizes away so that it consumes no CPU cycles
--- during run-time (that is, during calls to sql_step()).
---
-test:do_test(
-    "func3-5.20",
-    function()
-        return test:execsql "EXPLAIN SELECT likelihood(min(1.0+'2.0',4*11), 0.5)"
-    end, test:execsql("EXPLAIN SELECT min(1.0+'2.0',4*11)"))
-
--- EVIDENCE-OF: R-11152-23456 The unlikely(X) function returns the
--- argument X unchanged.
---
-test:do_execsql_test(
-    "func3-5.30",
-    [[
-        SELECT unlikely(9223372036854775807);
-    ]], {
-        -- <func3-5.30>
-        9223372036854775807LL
-        -- </func3-5.30>
-    })
-
-test:do_execsql_test(
-    "func3-5.31",
-    [[
-        SELECT unlikely(-9223372036854775808);
-    ]], {
-        -- <func3-5.31>
-        -9223372036854775808LL
-        -- </func3-5.31>
-    })
-
-test:do_execsql_test(
-    "func3-5.32",
-    [[
-        SELECT unlikely(14.125);
-    ]], {
-        -- <func3-5.32>
-        14.125
-        -- </func3-5.32>
-    })
-
-test:do_execsql_test(
-    "func3-5.33",
-    [[
-        SELECT unlikely(NULL);
-    ]], {
-        -- <func3-5.33>
-        ""
-        -- </func3-5.33>
-    })
-
-test:do_execsql_test(
-    "func3-5.34",
-    [[
-        SELECT unlikely('test-string');
-    ]], {
-        -- <func3-5.34>
-        "test-string"
-        -- </func3-5.34>
-    })
-
-test:do_execsql_test(
-    "func3-5.35",
-    [[
-        SELECT quote(unlikely(x'010203000405'));
-    ]], {
-        -- <func3-5.35>
-        "X'010203000405'"
-        -- </func3-5.35>
-    })
-
--- EVIDENCE-OF: R-22887-63324 The unlikely(X) function is a no-op that
--- the code generator optimizes away so that it consumes no CPU cycles at
--- run-time (that is, during calls to sql_step()).
---
-test:do_test(
-    "func3-5.39",
-    function()
-        return test:execsql "EXPLAIN SELECT unlikely(min(1.0+'2.0',4*11))"
-    end, test:execsql "EXPLAIN SELECT min(1.0+'2.0',4*11)")
-
--- EVIDENCE-OF: R-23735-03107 The likely(X) function returns the argument
--- X unchanged.
---
-test:do_execsql_test(
-    "func3-5.50",
-    [[
-        SELECT likely(9223372036854775807);
-    ]], {
-        -- <func3-5.50>
-        9223372036854775807LL
-        -- </func3-5.50>
-    })
-
-test:do_execsql_test(
-    "func3-5.51",
-    [[
-        SELECT likely(-9223372036854775808);
-    ]], {
-        -- <func3-5.51>
-        -9223372036854775808LL
-        -- </func3-5.51>
-    })
-
-test:do_execsql_test(
-    "func3-5.52",
-    [[
-        SELECT likely(14.125);
-    ]], {
-        -- <func3-5.52>
-        14.125
-        -- </func3-5.52>
-    })
-
-test:do_execsql_test(
-    "func3-5.53",
-    [[
-        SELECT likely(NULL);
-    ]], {
-        -- <func3-5.53>
-        ""
-        -- </func3-5.53>
-    })
-
-test:do_execsql_test(
-    "func3-5.54",
-    [[
-        SELECT likely('test-string');
-    ]], {
-        -- <func3-5.54>
-        "test-string"
-        -- </func3-5.54>
-    })
-
-test:do_execsql_test(
-    "func3-5.55",
-    [[
-        SELECT quote(likely(x'010203000405'));
-    ]], {
-        -- <func3-5.55>
-        "X'010203000405'"
-        -- </func3-5.55>
-    })
-
--- EVIDENCE-OF: R-43464-09689 The likely(X) function is a no-op that the
--- code generator optimizes away so that it consumes no CPU cycles at
--- run-time (that is, during calls to sql_step()).
---
-test:do_test(
-    "func3-5.59",
-    function()
-        return test:execsql "EXPLAIN SELECT likely(min(1.0+'2.0',4*11))"
-    end, test:execsql "EXPLAIN SELECT min(1.0+'2.0',4*11)")
-
-
-
-test:finish_test()
diff --git a/test/sql-tap/orderby5.test.lua b/test/sql-tap/orderby5.test.lua
index c7c09bc07..d5ce024f8 100755
--- a/test/sql-tap/orderby5.test.lua
+++ b/test/sql-tap/orderby5.test.lua
@@ -153,7 +153,7 @@ test:do_execsql_test(
 -- } {~/B-TREE/}
 -- do_execsql_test 2.1b {
 --   EXPLAIN QUERY PLAN
---   SELECT * FROM t1 WHERE likelihood(a=0, 0.05) ORDER BY a, b, c;
+--   SELECT * FROM t1 WHERE (a=0, 0.05) ORDER BY a, b, c;
 -- } {/B-TREE/}
 -- do_execsql_test 2.2 {
 --   EXPLAIN QUERY PLAN
diff --git a/test/sql-tap/select4.test.lua b/test/sql-tap/select4.test.lua
index 1c0804b8e..0355bddf5 100755
--- a/test/sql-tap/select4.test.lua
+++ b/test/sql-tap/select4.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(105)
+test:plan(104)
 
 --!./tcltestrunner.lua
 -- 2001 September 15
@@ -1446,16 +1446,6 @@ test:do_execsql_test(
         -- </select4-14.14>
     })
 
-test:do_execsql_test(
-    "select4-14.15",
-    [[
-        SELECT * FROM (SELECT 123), (SELECT 456) ON likely(false OR true) OR false;
-    ]], {
-        -- <select4-14.15>
-        123, 456
-        -- </select4-14.15>
-    })
-
 test:do_execsql_test(
     "select4-14.16",
     [[
diff --git a/test/sql-tap/sql-errors.test.lua b/test/sql-tap/sql-errors.test.lua
index 636242129..74b59bfc5 100755
--- a/test/sql-tap/sql-errors.test.lua
+++ b/test/sql-tap/sql-errors.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(63)
+test:plan(62)
 
 test:execsql([[
 	CREATE TABLE t0 (i INT PRIMARY KEY, a INT);
@@ -406,16 +406,6 @@ test:do_catchsql_test(
 		-- </sql-errors-1.35>
 	})
 
-test:do_catchsql_test(
-	"sql-errors-1.36",
-	[[
-		SELECT likelihood(1, 2);
-	]], {
-		-- <sql-errors-1.36>
-		1,"Illegal parameters, second argument to likelihood() must be a constant between 0.0 and 1.0"
-		-- </sql-errors-1.36>
-	})
-
 test:do_catchsql_test(
 	"sql-errors-1.37",
 	[[
diff --git a/test/sql-tap/whereG.test.lua b/test/sql-tap/whereG.test.lua
index 177d9d14e..8e3250569 100755
--- a/test/sql-tap/whereG.test.lua
+++ b/test/sql-tap/whereG.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(23)
+test:plan(20)
 
 --!./tcltestrunner.lua
 -- 2013-09-05
@@ -14,8 +14,6 @@ test:plan(23)
 --
 -------------------------------------------------------------------------
 -- 
--- Test cases for query planning decisions and the likely(), unlikely(), and
--- likelihood() functions.
 -- ["set","testdir",[["file","dirname",["argv0"]]]]
 -- ["source",[["testdir"],"\/tester.tcl"]]
 testprefix = "whereG"
@@ -75,7 +73,7 @@ test:do_eqp_test(
     [[
         SELECT DISTINCT aname
           FROM album, composer, track
-         WHERE unlikely(cname LIKE '%bach%')
+         WHERE (cname LIKE '%bach%')
            AND composer.cid=track.cid
            AND album.aid=track.aid;
     ]], {
@@ -89,7 +87,7 @@ test:do_execsql_test(
     [[
         SELECT DISTINCT aname
           FROM album, composer, track
-         WHERE unlikely(cname LIKE '%bach%')
+         WHERE (cname LIKE '%bach%')
            AND composer.cid=track.cid
            AND album.aid=track.aid;
     ]], {
@@ -104,7 +102,7 @@ test:do_eqp_test(
     [[
         SELECT DISTINCT aname
           FROM album, composer, track
-         WHERE likelihood(cname LIKE '%bach%', 0.5)
+         WHERE (cname LIKE '%bach%')
            AND composer.cid=track.cid
            AND album.aid=track.aid;
     ]], {
@@ -118,7 +116,7 @@ test:do_execsql_test(
     [[
         SELECT DISTINCT aname
           FROM album, composer, track
-         WHERE likelihood(cname LIKE '%bach%', 0.5)
+         WHERE (cname LIKE '%bach%')
            AND composer.cid=track.cid
            AND album.aid=track.aid;
     ]], {
@@ -161,8 +159,8 @@ test:do_eqp_test(
         SELECT DISTINCT aname
           FROM album, composer, track
          WHERE cname LIKE '%bach%'
-           AND unlikely(composer.cid=track.cid)
-           AND unlikely(album.aid=track.aid);
+           AND (composer.cid=track.cid)
+           AND (album.aid=track.aid);
     ]], {
         -- <whereG-1.7>
         "/.*track.*(composer.*album|album.*composer).*/"
@@ -175,56 +173,14 @@ test:do_execsql_test(
         SELECT DISTINCT aname
           FROM album, composer, track
          WHERE cname LIKE '%bach%'
-           AND unlikely(composer.cid=track.cid)
-           AND unlikely(album.aid=track.aid);
+           AND (composer.cid=track.cid)
+           AND (album.aid=track.aid);
     ]], {
         -- <whereG-1.8>
         "Mass in B Minor, BWV 232"
         -- </whereG-1.8>
     })
 
-test:do_catchsql_test(
-    "whereG-2.1",
-    [[
-        SELECT DISTINCT aname
-          FROM album, composer, track
-         WHERE likelihood(cname LIKE '%bach%', -0.01)
-           AND composer.cid=track.cid
-           AND album.aid=track.aid;
-    ]], {
-        -- <whereG-2.1>
-        1, "Illegal parameters, second argument to likelihood() must be a constant between 0.0 and 1.0"
-        -- </whereG-2.1>
-    })
-
-test:do_catchsql_test(
-    "whereG-2.2",
-    [[
-        SELECT DISTINCT aname
-          FROM album, composer, track
-         WHERE likelihood(cname LIKE '%bach%', 1.01)
-           AND composer.cid=track.cid
-           AND album.aid=track.aid;
-    ]], {
-        -- <whereG-2.2>
-        1, "Illegal parameters, second argument to likelihood() must be a constant between 0.0 and 1.0"
-        -- </whereG-2.2>
-    })
-
-test:do_catchsql_test(
-    "whereG-2.3",
-    [[
-        SELECT DISTINCT aname
-          FROM album, composer, track
-         WHERE likelihood(cname LIKE '%bach%', track.cid)
-           AND composer.cid=track.cid
-           AND album.aid=track.aid;
-    ]], {
-        -- <whereG-2.3>
-        1, "Illegal parameters, second argument to likelihood() must be a constant between 0.0 and 1.0"
-        -- </whereG-2.3>
-    })
-
 -- Commuting a term of the WHERE clause should not change the query plan
 --
 test:do_execsql_test(
@@ -270,9 +226,6 @@ test:do_execsql_test(
     })
 
 ---------------------------------------------------------------------------
--- Test that likelihood() specifications on indexed terms are taken into 
--- account by various forms of loops.
---
 --   5.1.*: open ended range scans
 --   5.2.*: skip-scans
 --
@@ -288,10 +241,10 @@ test:do_execsql_test(
 --   SELECT * FROM t1 WHERE a>?
 -- } {0 0 0 {SEARCH TABLE t1 USING INDEX i1 (a>?)}}
 -- do_eqp_test 5.1.3 {
---   SELECT * FROM t1 WHERE likelihood(a>?, 0.9)
+--   SELECT * FROM t1 WHERE (a>?)
 -- } {0 0 0 {SCAN TABLE t1}}
 -- do_eqp_test 5.1.4 {
---   SELECT * FROM t1 WHERE likely(a>?)
+--   SELECT * FROM t1 WHERE (a>?)
 -- } {0 0 0 {SCAN TABLE t1}}
 -- do_test 5.2 {
 --   for {set i 0} {$i < 100} {incr i} {
@@ -301,22 +254,22 @@ test:do_execsql_test(
 --   execsql { ANALYZE }
 -- } {}
 -- do_eqp_test 5.2.2 {
---   SELECT * FROM t1 WHERE likelihood(b>?, 0.01)
+--   SELECT * FROM t1 WHERE (b>?)
 -- } {0 0 0 {SEARCH TABLE t1 USING INDEX i1 (ANY(a) AND b>?)}}
 -- do_eqp_test 5.2.3 {
---   SELECT * FROM t1 WHERE likelihood(b>?, 0.9)
+--   SELECT * FROM t1 WHERE (b>?)
 -- } {0 0 0 {SCAN TABLE t1}}
 -- do_eqp_test 5.2.4 {
---   SELECT * FROM t1 WHERE likely(b>?)
+--   SELECT * FROM t1 WHERE (b>?)
 -- } {0 0 0 {SCAN TABLE t1}}
 -- do_eqp_test 5.3.1 {
 --   SELECT * FROM t1 WHERE a=?
 -- } {0 0 0 {SEARCH TABLE t1 USING INDEX i1 (a=?)}}
 -- do_eqp_test 5.3.2 {
---   SELECT * FROM t1 WHERE likelihood(a=?, 0.9)
+--   SELECT * FROM t1 WHERE (a=?)
 -- } {0 0 0 {SCAN TABLE t1}}
 -- do_eqp_test 5.3.3 {
---   SELECT * FROM t1 WHERE likely(a=?)
+--   SELECT * FROM t1 WHERE (a=?)
 -- } {0 0 0 {SCAN TABLE t1}}
 -- 2015-06-18
 -- Ticket [https://www.sql.org/see/tktview/472f0742a1868fb58862bc588ed70]
@@ -332,7 +285,7 @@ test:do_execsql_test(
         CREATE TABLE t2(i int PRIMARY KEY, b TEXT);
         INSERT INTO t2 VALUES(1,'T'), (2,'F');
         SELECT count(*) FROM t1 LEFT JOIN t2 ON t1.i=t2.i AND b='T' union all
-        SELECT count(*) FROM t1 LEFT JOIN t2 ON likely(t1.i=t2.i) AND b='T';
+        SELECT count(*) FROM t1 LEFT JOIN t2 ON (t1.i=t2.i) AND b='T';
     ]], {
         -- <6.0>
         4, 4
@@ -351,7 +304,7 @@ test:do_execsql_test(
         DROP TABLE IF EXISTS t2;
         CREATE TABLE t2(x INT , y INT , PRIMARY KEY(x,y));
         INSERT INTO t2 VALUES(3,3),(4,4);
-        SELECT likely(a), x FROM t1, t2 ORDER BY 1, 2;
+        SELECT (a), x FROM t1, t2 ORDER BY 1, 2;
     ]], {
         -- <7.0>
         1, 3, 1, 4, 9, 3, 9, 4
@@ -361,7 +314,7 @@ test:do_execsql_test(
 test:do_execsql_test(
     "7.1",
     [[
-        SELECT unlikely(a), x FROM t1, t2 ORDER BY 1, 2;
+        SELECT (a), x FROM t1, t2 ORDER BY 1, 2;
     ]], {
         -- <7.1>
         1, 3, 1, 4, 9, 3, 9, 4
@@ -371,7 +324,7 @@ test:do_execsql_test(
 test:do_execsql_test(
     "7.2",
     [[
-        SELECT likelihood(a,0.5), x FROM t1, t2 ORDER BY 1, 2;
+        SELECT a, x FROM t1, t2 ORDER BY 1, 2;
     ]], {
         -- <7.2>
         1, 3, 1, 4, 9, 3, 9, 4
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 03/12] sql: put analyze helpers to FuncDef cache
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
                   ` (4 preceding siblings ...)
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 02/12] sql: get rid of LIKELY, UNLIKELY and LIKEHOOD Kirill Shcherbatov
@ 2019-07-10 11:01 ` Kirill Shcherbatov
  2019-07-10 19:04   ` [tarantool-patches] " Konstantin Osipov
  2019-07-12  8:47   ` Kirill Yukhin
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 04/12] sql: rework LIKE case-insensitive mode Kirill Shcherbatov
                   ` (5 subsequent siblings)
  11 siblings, 2 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:01 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

Previously analyze functions refer to statically defined
service FuncDef context. We need to change this approach due we
going to rework the builtins functions machinery in following
patches.

Needed for #4113, #2200, #2233
---
 src/box/sql/analyze.c | 62 ++++++++++++++++---------------------------
 src/box/sql/func.c    |  2 +-
 src/box/sql/sqlInt.h  |  4 +++
 src/box/sql/vdbemem.c | 50 ----------------------------------
 4 files changed, 28 insertions(+), 90 deletions(-)

diff --git a/src/box/sql/analyze.c b/src/box/sql/analyze.c
index 9af23e985..512904cc1 100644
--- a/src/box/sql/analyze.c
+++ b/src/box/sql/analyze.c
@@ -350,18 +350,6 @@ statInit(sql_context * context, int argc, sql_value ** argv)
 	sql_result_blob(context, p, sizeof(*p), stat4Destructor);
 }
 
-static const FuncDef statInitFuncdef = {
-	3,			/* nArg */
-	0,			/* funcFlags */
-	0,			/* pUserData */
-	0,			/* pNext */
-	statInit,		/* xSFunc */
-	0,			/* xFinalize */
-	"stat_init",		/* zName */
-	{0},
-	0, false
-};
-
 /*
  * pNew and pOld are both candidate non-periodic samples selected for
  * the same column (pNew->iCol==pOld->iCol). Ignoring this column and
@@ -606,18 +594,6 @@ statPush(sql_context * context, int argc, sql_value ** argv)
 	}
 }
 
-static const FuncDef statPushFuncdef = {
-	3,			/* nArg */
-	0,			/* funcFlags */
-	0,			/* pUserData */
-	0,			/* pNext */
-	statPush,		/* xSFunc */
-	0,			/* xFinalize */
-	"stat_push",		/* zName */
-	{0},
-	0, false
-};
-
 #define STAT_GET_STAT1 0	/* "stat" column of stat1 table */
 #define STAT_GET_KEY   1	/* "key" column of stat4 entry */
 #define STAT_GET_NEQ   2	/* "neq" column of stat4 entry */
@@ -734,25 +710,16 @@ UNUSED_PARAMETER(argc);
 #endif
 }
 
-static const FuncDef statGetFuncdef = {
-	2,			/* nArg */
-	0,			/* funcFlags */
-	0,			/* pUserData */
-	0,			/* pNext */
-	statGet,		/* xSFunc */
-	0,			/* xFinalize */
-	"stat_get",		/* zName */
-	{0},
-	0, false
-};
-
 static void
 callStatGet(Vdbe * v, int regStat4, int iParam, int regOut)
 {
 	assert(regOut != regStat4 && regOut != regStat4 + 1);
 	sqlVdbeAddOp2(v, OP_Integer, iParam, regStat4 + 1);
+	struct FuncDef *func =
+		sqlFindFunction(sql_get(), "_sql_stat_get", 2, 0);
+	assert(func != NULL);
 	sqlVdbeAddOp4(v, OP_Function0, 0, regStat4, regOut,
-			  (char *)&statGetFuncdef, P4_FUNCDEF);
+		      (char *)func, P4_FUNCDEF);
 	sqlVdbeChangeP5(v, 2);
 }
 
@@ -888,8 +855,11 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
 		sqlVdbeAddOp2(v, OP_Count, idx_cursor, stat4_reg + 3);
 		sqlVdbeAddOp2(v, OP_Integer, part_count, stat4_reg + 1);
 		sqlVdbeAddOp2(v, OP_Integer, part_count, stat4_reg + 2);
+		struct FuncDef *init_func =
+			sqlFindFunction(sql_get(), "_sql_stat_init", 3, 0);
+		assert(init_func != NULL);
 		sqlVdbeAddOp4(v, OP_Function0, 0, stat4_reg + 1, stat4_reg,
-				  (char *)&statInitFuncdef, P4_FUNCDEF);
+			      (char *)init_func, P4_FUNCDEF);
 		sqlVdbeChangeP5(v, 3);
 		/*
 		 * Implementation of the following:
@@ -986,8 +956,11 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
 		sqlVdbeAddOp3(v, OP_MakeRecord, stat_key_reg,
 				  pk_part_count, key_reg);
 		assert(chng_reg == (stat4_reg + 1));
+		struct FuncDef *push_func =
+			sqlFindFunction(sql_get(), "_sql_stat_push", 3, 0);
+		assert(push_func != NULL);
 		sqlVdbeAddOp4(v, OP_Function0, 1, stat4_reg, tmp_reg,
-				  (char *)&statPushFuncdef, P4_FUNCDEF);
+			      (char *)push_func, P4_FUNCDEF);
 		sqlVdbeChangeP5(v, 3);
 		sqlVdbeAddOp2(v, OP_Next, idx_cursor, next_row_addr);
 		/* Add the entry to the stat1 table. */
@@ -1774,3 +1747,14 @@ fail:
 	box_txn_rollback();
 	return -1;
 }
+
+void
+sql_register_analyze_builtins(void)
+{
+	static FuncDef funcs[] = {
+		FUNCTION(_sql_stat_get, 2, 0, 0, statGet, FIELD_TYPE_ANY),
+		FUNCTION(_sql_stat_push, 3, 0, 0, statPush, FIELD_TYPE_ANY),
+		FUNCTION(_sql_stat_init, 3, 0, 0, statInit, FIELD_TYPE_ANY),
+	};
+	sqlInsertBuiltinFuncs(funcs, nelem(funcs));
+}
diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index 29f2b5c6a..580571709 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -1843,7 +1843,7 @@ sqlRegisterBuiltinFunctions(void)
 		FUNCTION2(coalesce, -1, 0, 0, noopFunc, SQL_FUNC_COALESCE,
 			  FIELD_TYPE_SCALAR),
 	};
-	sqlAnalyzeFunctions();
+	sql_register_analyze_builtins();
 	sqlRegisterDateTimeFunctions();
 	sqlInsertBuiltinFuncs(aBuiltinFunc, ArraySize(aBuiltinFunc));
 
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 8ab9804fe..9f439fb07 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -4509,6 +4509,10 @@ Expr *sqlExprForVectorField(Parse *, Expr *, int);
  */
 extern int sqlSubProgramsRemaining;
 
+/** Register built-in functions to work with ANALYZE data. */
+void
+sql_register_analyze_builtins(void);
+
 /**
  * Generate VDBE code to halt execution with correct error if
  * the object with specified key is already present (or doesn't
diff --git a/src/box/sql/vdbemem.c b/src/box/sql/vdbemem.c
index 4e4bd597d..f52035b0e 100644
--- a/src/box/sql/vdbemem.c
+++ b/src/box/sql/vdbemem.c
@@ -1466,56 +1466,6 @@ sqlValueFromExpr(sql * db,	/* The database connection */
 	return pExpr ? valueFromExpr(db, pExpr, type, ppVal, 0) : 0;
 }
 
-/*
- * The implementation of the sql_record() function. This function accepts
- * a single argument of any type. The return value is a formatted database
- * record (a blob) containing the argument value.
- *
- * This is used to convert the value stored in the 'sample' column of the
- * sql_stat4 table to the record format sql uses internally.
- */
-static void
-recordFunc(sql_context * context, int argc, sql_value ** argv)
-{
-	const int file_format = 1;
-	u32 iSerial;		/* Serial type */
-	int nSerial;		/* Bytes of space for iSerial as varint */
-	u32 nVal;		/* Bytes of space required for argv[0] */
-	int nRet;
-	sql *db;
-	u8 *aRet;
-
-	UNUSED_PARAMETER(argc);
-	iSerial = sqlVdbeSerialType(argv[0], file_format, &nVal);
-	nSerial = sqlVarintLen(iSerial);
-	db = sql_context_db_handle(context);
-
-	nRet = 1 + nSerial + nVal;
-	aRet = sqlDbMallocRawNN(db, nRet);
-	if (aRet == 0) {
-		context->is_aborted = true;
-	} else {
-		aRet[0] = nSerial + 1;
-		putVarint32(&aRet[1], iSerial);
-		sqlVdbeSerialPut(&aRet[1 + nSerial], argv[0], iSerial);
-		sql_result_blob(context, aRet, nRet, SQL_TRANSIENT);
-		sqlDbFree(db, aRet);
-	}
-}
-
-/*
- * Register built-in functions used to help read ANALYZE data.
- */
-void
-sqlAnalyzeFunctions(void)
-{
-	static FuncDef aAnalyzeTableFuncs[] = {
-		FUNCTION(sql_record, 1, 0, 0, recordFunc, 0),
-	};
-	sqlInsertBuiltinFuncs(aAnalyzeTableFuncs,
-				  ArraySize(aAnalyzeTableFuncs));
-}
-
 /*
  * Attempt to extract a value from pExpr and use it to construct *ppVal.
  *
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 04/12] sql: rework LIKE case-insensitive mode
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
                   ` (5 preceding siblings ...)
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 03/12] sql: put analyze helpers to FuncDef cache Kirill Shcherbatov
@ 2019-07-10 11:01 ` Kirill Shcherbatov
  2019-07-10 19:09   ` [tarantool-patches] " Konstantin Osipov
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 05/12] sql: replace bool is_derived_coll marker with flag Kirill Shcherbatov
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:01 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

The case_sensitive_like pragma used to process a strange way: on
each call a new instance of function having appropriate
properties was created. Reworked legacy code with user session
object to define this property there. The new approach allows
to rework sql builtins machinery with further patches.

Needed for #4113, #2200, #2233
---
 src/box/sql/func.c   | 41 +++++------------------------------------
 src/box/sql/pragma.c |  8 --------
 src/box/sql/sqlInt.h |  3 ---
 3 files changed, 5 insertions(+), 47 deletions(-)

diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index 580571709..ae75a1c3c 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -39,6 +39,7 @@
 #include "version.h"
 #include "coll/coll.h"
 #include "tarantoolInt.h"
+#include "box/session.h"
 #include <unicode/ustring.h>
 #include <unicode/ucasemap.h>
 #include <unicode/ucnv.h>
@@ -885,7 +886,8 @@ likeFunc(sql_context *context, int argc, sql_value **argv)
 	u32 escape = SQL_END_OF_STRING;
 	int nPat;
 	sql *db = sql_context_db_handle(context);
-	int is_like_ci = SQL_PTR_TO_INT(sql_user_data(context));
+	bool is_like_ci =
+		(current_session()->sql_flags & LIKE_CASE_SENS_FLAG) == 0;
 	int rhs_type = sql_value_type(argv[0]);
 	int lhs_type = sql_value_type(argv[1]);
 
@@ -1704,40 +1706,6 @@ groupConcatFinalize(sql_context * context)
 	}
 }
 
-/*
- * Set the LIKEOPT flag on the 2-argument function with the given name.
- */
-static void
-setLikeOptFlag(sql * db, const char *zName, u8 flagVal)
-{
-	FuncDef *pDef;
-	pDef = sqlFindFunction(db, zName, 2, 0);
-	if (ALWAYS(pDef)) {
-		pDef->funcFlags |= flagVal;
-	}
-}
-
-/**
- * Register the built-in LIKE function.
- */
-void
-sqlRegisterLikeFunctions(sql *db, int is_case_insensitive)
-{
-	/*
-	 * FIXME: after introducing type <BOOLEAN> LIKE must
-	 * return that type: TRUE if the string matches the
-	 * supplied pattern and FALSE otherwise.
-	 */
-	int *is_like_ci = SQL_INT_TO_PTR(is_case_insensitive);
-	sqlCreateFunc(db, "LIKE", FIELD_TYPE_BOOLEAN, 2, 0,
-			  is_like_ci, likeFunc, 0, 0, 0);
-	sqlCreateFunc(db, "LIKE", FIELD_TYPE_BOOLEAN, 3, 0,
-			  is_like_ci, likeFunc, 0, 0, 0);
-	setLikeOptFlag(db, "LIKE",
-		       !(is_case_insensitive) ? (SQL_FUNC_LIKE |
-		       SQL_FUNC_CASE) : SQL_FUNC_LIKE);
-}
-
 int
 sql_is_like_func(struct sql *db, struct Expr *expr, int *is_like_ci)
 {
@@ -1749,7 +1717,8 @@ sql_is_like_func(struct sql *db, struct Expr *expr, int *is_like_ci)
 	assert(func != NULL);
 	if ((func->funcFlags & SQL_FUNC_LIKE) == 0)
 		return 0;
-	*is_like_ci = (func->funcFlags & SQL_FUNC_CASE) == 0;
+	*is_like_ci =
+		(current_session()->sql_flags & LIKE_CASE_SENS_FLAG) == 0;
 	return 1;
 }
 
diff --git a/src/box/sql/pragma.c b/src/box/sql/pragma.c
index 53524b617..1fd74c0a3 100644
--- a/src/box/sql/pragma.c
+++ b/src/box/sql/pragma.c
@@ -484,17 +484,9 @@ sqlPragma(Parse * pParse, Token * pId,	/* First part of [schema.]id field */
 					sqlParserTrace(0, 0);
 			}
 #endif
-			/*
-			 * Reinstall the LIKE and functions. The
-			 * variant of LIKE * used will be case
-			 * sensitive or not depending on the RHS.
-			 */
-			if (mask == LIKE_CASE_SENS_FLAG)
-				sqlRegisterLikeFunctions(db, !is_pragma_set);
 		}
 		break;
 	}
-
 	case PragTyp_TABLE_INFO:
 		sql_pragma_table_info(pParse, zRight);
 		break;
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 9f439fb07..7bcbb4eb1 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -1271,7 +1271,6 @@ struct FuncDestructor {
  *     SQL_FUNC_CONSTANT  ==  sql_DETERMINISTIC from the API
  */
 #define SQL_FUNC_LIKE     0x0004	/* Candidate for the LIKE optimization */
-#define SQL_FUNC_CASE     0x0008	/* Case-sensitive LIKE-type function */
 #define SQL_FUNC_EPHEM    0x0010	/* Ephemeral.  Delete with VDBE */
 #define SQL_FUNC_NEEDCOLL 0x0020	/* sqlGetFuncCollSeq() might be called.
 					 * The flag is set when the collation
@@ -4262,8 +4261,6 @@ sql_key_info_unref(struct sql_key_info *key_info);
 struct key_def *
 sql_key_info_to_key_def(struct sql_key_info *key_info);
 
-void sqlRegisterLikeFunctions(sql *, int);
-
 /**
  * Check if the function implements LIKE-style comparison & if it
  * is appropriate to apply a LIKE query optimization.
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 05/12] sql: replace bool is_derived_coll marker with flag
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
                   ` (6 preceding siblings ...)
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 04/12] sql: rework LIKE case-insensitive mode Kirill Shcherbatov
@ 2019-07-10 11:01 ` Kirill Shcherbatov
  2019-07-10 19:10   ` [tarantool-patches] " Konstantin Osipov
  2019-07-12  8:48   ` Kirill Yukhin
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 06/12] sql: remove SQL_PreferBuiltin flag Kirill Shcherbatov
                   ` (3 subsequent siblings)
  11 siblings, 2 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:01 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

Introduce a new flag SQL_FUNC_DERIVEDCOLL for function that may
require collation to be applied on its result instead of separate
boolean variable. This is required to get rid of FuncDef in
further patches.

Needed for #4113, #2200, #2233
---
 src/box/sql/expr.c   |  2 +-
 src/box/sql/sqlInt.h | 34 +++++++++++++++++-----------------
 2 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
index 20e490b96..7ee3e992e 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -280,7 +280,7 @@ sql_expr_coll(Parse *parse, Expr *p, bool *is_explicit_coll, uint32_t *coll_id,
 							       arg_count, 0);
 			if (func == NULL)
 				break;
-			if (func->is_coll_derived) {
+			if ((func->funcFlags & SQL_FUNC_DERIVEDCOLL) != 0) {
 				/*
 				 * Now we use quite straightforward
 				 * approach assuming that resulting
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 7bcbb4eb1..d6a46e534 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -1229,13 +1229,6 @@ struct FuncDef {
 	} u;
 	/* Return type. */
 	enum field_type ret_type;
-	/**
-	 * If function returns string, it may require collation
-	 * to be applied on its result. For instance, result of
-	 * substr() built-in function must have the same collation
-	 * as its first argument.
-	 */
-	bool is_coll_derived;
 };
 
 /*
@@ -1283,6 +1276,13 @@ struct FuncDestructor {
 #define SQL_FUNC_TYPEOF   0x0080	/* Built-in typeof() function */
 #define SQL_FUNC_COUNT    0x0100	/* Built-in count(*) aggregate */
 #define SQL_FUNC_COALESCE 0x0200	/* Built-in coalesce() or ifnull() */
+/**
+ * If function returns string, it may require collation to be
+ * applied on its result. For instance, result of substr()
+ * built-in function must have the same collation as its first
+ * argument.
+ */
+#define SQL_FUNC_DERIVEDCOLL 0x0400
 #define SQL_FUNC_CONSTANT 0x0800	/* Constant inputs give a constant output */
 #define SQL_FUNC_MINMAX   0x1000	/* True for min() and max() aggregates */
 #define SQL_FUNC_SLOCHNG  0x2000	/* "Slow Change". Value constant during a
@@ -1340,31 +1340,31 @@ enum trim_side_mask {
  */
 #define FUNCTION(zName, nArg, iArg, bNC, xFunc, type) \
   {nArg, SQL_FUNC_CONSTANT|(bNC*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type, false}
+   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
 #define FUNCTION_COLL(zName, nArg, iArg, bNC, xFunc) \
-  {nArg, SQL_FUNC_CONSTANT|(bNC*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, FIELD_TYPE_STRING, true}
+  {nArg, SQL_FUNC_CONSTANT|SQL_FUNC_DERIVEDCOLL|(bNC*SQL_FUNC_NEEDCOLL), \
+   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, FIELD_TYPE_STRING}
 #define VFUNCTION(zName, nArg, iArg, bNC, xFunc, type) \
   {nArg, (bNC*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type, false }
+   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
 #define DFUNCTION(zName, nArg, iArg, bNC, xFunc, type) \
   {nArg, SQL_FUNC_SLOCHNG|(bNC*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type, false }
+   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
 #define FUNCTION2(zName, nArg, iArg, bNC, xFunc, extraFlags, type) \
   {nArg,SQL_FUNC_CONSTANT|(bNC*SQL_FUNC_NEEDCOLL)|extraFlags,\
-   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type, false }
+   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
 #define STR_FUNCTION(zName, nArg, pArg, bNC, xFunc) \
   {nArg, SQL_FUNC_SLOCHNG|(bNC*SQL_FUNC_NEEDCOLL), \
-   pArg, 0, xFunc, 0, #zName, {SQL_AFF_STRING, {0}}, false}
+   pArg, 0, xFunc, 0, #zName, {SQL_AFF_STRING, {0}}}
 #define LIKEFUNC(zName, nArg, arg, flags, type) \
   {nArg, SQL_FUNC_CONSTANT|flags, \
-   (void *)(SQL_INT_TO_PTR(arg)), 0, likeFunc, 0, #zName, {0}, type, false }
+   (void *)(SQL_INT_TO_PTR(arg)), 0, likeFunc, 0, #zName, {0}, type}
 #define AGGREGATE(zName, nArg, arg, nc, xStep, xFinal, type) \
   {nArg, (nc*SQL_FUNC_NEEDCOLL), \
-   SQL_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}, type, false}
+   SQL_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}, type}
 #define AGGREGATE2(zName, nArg, arg, nc, xStep, xFinal, extraFlags, type) \
   {nArg, (nc*SQL_FUNC_NEEDCOLL)|extraFlags, \
-   SQL_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}, type, false}
+   SQL_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}, type}
 
 /*
  * All current savepoints are stored in a linked list starting at
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 06/12] sql: remove SQL_PreferBuiltin flag
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
                   ` (7 preceding siblings ...)
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 05/12] sql: replace bool is_derived_coll marker with flag Kirill Shcherbatov
@ 2019-07-10 11:01 ` Kirill Shcherbatov
  2019-07-10 19:11   ` [tarantool-patches] " Konstantin Osipov
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 07/12] sql: move LIKE UConverter object to collation library Kirill Shcherbatov
                   ` (2 subsequent siblings)
  11 siblings, 1 reply; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:01 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

The SQL_PreferBuiltin flag is redundant (because builtin names
are forbidden for UDFs) so we may to remove it.

Needed for #4113, #2200, #2233
---
 src/box/sql/callback.c | 5 +----
 src/box/sql/sqlInt.h   | 1 -
 2 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/box/sql/callback.c b/src/box/sql/callback.c
index 49197532e..42fec2c6a 100644
--- a/src/box/sql/callback.c
+++ b/src/box/sql/callback.c
@@ -191,7 +191,6 @@ sqlFindFunction(sql * db,	/* An open database */
 	int bestScore = 0;	/* Score of best match */
 	int h;			/* Hash value */
 	int nName;		/* Length of the name */
-	struct session *user_session = current_session();
 
 	assert(nArg >= (-2));
 	assert(nArg >= (-1) || createFlag == 0);
@@ -221,9 +220,7 @@ sqlFindFunction(sql * db,	/* An open database */
 	 * new function.  But the FuncDefs for built-in functions are read-only.
 	 * So we must not search for built-ins when creating a new function.
 	 */
-	if (!createFlag &&
-	    (pBest == 0
-	     || (user_session->sql_flags & SQL_PreferBuiltin) != 0)) {
+	if (!createFlag && (pBest == NULL)) {
 		bestScore = 0;
 		h = (sqlUpperToLower[(u8) zName[0]] +
 		     nName) % SQL_FUNC_HASH_SZ;
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index d6a46e534..515fce3a9 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -1157,7 +1157,6 @@ struct sql {
 #define SQL_ReverseOrder   0x00020000	/* Reverse unordered SELECTs */
 #define SQL_RecTriggers    0x00040000	/* Enable recursive triggers */
 #define SQL_AutoIndex      0x00100000	/* Enable automatic indexes */
-#define SQL_PreferBuiltin  0x00200000	/* Preference to built-in funcs */
 #define SQL_EnableTrigger  0x01000000	/* True to enable triggers */
 #define SQL_DeferFKs       0x02000000	/* Defer all FK constraints */
 #define SQL_VdbeEQP        0x08000000	/* Debug EXPLAIN QUERY PLAN */
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 07/12] sql: move LIKE UConverter object to collation library
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
                   ` (8 preceding siblings ...)
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 06/12] sql: remove SQL_PreferBuiltin flag Kirill Shcherbatov
@ 2019-07-10 11:01 ` Kirill Shcherbatov
  2019-07-12  8:49   ` [tarantool-patches] " Kirill Yukhin
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 08/12] sql: rfc for SQL and Lua functions Kirill Shcherbatov
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 09/12] box: introduce Lua persistent functions Kirill Shcherbatov
  11 siblings, 1 reply; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:01 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

Moved UConverter object to collation library. This is required
to get rid of sqlRegisterBuiltinFunctions function in further
patches.

Needed for #4113, #2200, #2233
---
 src/box/sql/func.c  | 13 ++-----------
 src/lib/coll/coll.c |  7 ++++++-
 src/lib/coll/coll.h |  2 ++
 3 files changed, 10 insertions(+), 12 deletions(-)

diff --git a/src/box/sql/func.c b/src/box/sql/func.c
index ae75a1c3c..21ce78c24 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -46,8 +46,6 @@
 #include <unicode/uchar.h>
 #include <unicode/ucol.h>
 
-static UConverter* pUtf8conv;
-
 /*
  * Return the collating function associated with a function.
  */
@@ -659,7 +657,8 @@ randomBlob(sql_context * context, int argc, sql_value ** argv)
 	}
 }
 
-#define Utf8Read(s, e) ucnv_getNextUChar(pUtf8conv, &(s), (e), &status)
+#define Utf8Read(s, e) \
+	ucnv_getNextUChar(icu_utf8_conv, &(s), (e), &status)
 
 #define SQL_END_OF_STRING        0xffff
 #define SQL_INVALID_UTF8_SYMBOL  0xfffd
@@ -1732,14 +1731,6 @@ sql_is_like_func(struct sql *db, struct Expr *expr, int *is_like_ci)
 void
 sqlRegisterBuiltinFunctions(void)
 {
-	/*
-	 * Initialize default case map for UPPER/LOWER functions
-	 * This structure is not freed at db exit, but that is ok.
-	 */
-	UErrorCode status = U_ZERO_ERROR;
-
-	pUtf8conv = ucnv_open("utf8", &status);
-	assert(pUtf8conv);
 	/*
 	 * The following array holds FuncDef structures for all of the functions
 	 * defined in this file.
diff --git a/src/lib/coll/coll.c b/src/lib/coll/coll.c
index 98e4d0e64..fa27bf34e 100644
--- a/src/lib/coll/coll.c
+++ b/src/lib/coll/coll.c
@@ -34,10 +34,12 @@
 #include "diag.h"
 #include "assoc.h"
 #include <unicode/ucol.h>
+#include <unicode/ucnv.h>
 #include <unicode/ucasemap.h>
 #include "tt_static.h"
 
 struct UCaseMap *icu_ucase_default_map = NULL;
+struct UConverter *icu_utf8_conv = NULL;
 
 #define mh_name _coll
 struct mh_coll_key_t {
@@ -420,7 +422,9 @@ coll_init()
 	UErrorCode err = U_ZERO_ERROR;
 	coll_cache = mh_coll_new();
 	icu_ucase_default_map = ucasemap_open("", 0, &err);
-	if (coll_cache == NULL || icu_ucase_default_map == NULL)
+	icu_utf8_conv = ucnv_open("utf8", &err);
+	if (coll_cache == NULL || icu_ucase_default_map == NULL ||
+	    icu_utf8_conv == NULL)
 		panic("Can not create system collations cache");
 }
 
@@ -428,5 +432,6 @@ void
 coll_free()
 {
 	ucasemap_close(icu_ucase_default_map);
+	ucnv_close(icu_utf8_conv);
 	mh_coll_delete(coll_cache);
 }
diff --git a/src/lib/coll/coll.h b/src/lib/coll/coll.h
index 1f5b29a2e..dc343539e 100644
--- a/src/lib/coll/coll.h
+++ b/src/lib/coll/coll.h
@@ -55,6 +55,8 @@ struct UCollator;
 
 /** Default universal casemap for case transformations. */
 extern struct UCaseMap *icu_ucase_default_map;
+/** Default universal utf8 converter. */
+extern struct UConverter *icu_utf8_conv;
 
 /**
  * Collation. It has no unique features like name, id or owner.
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 08/12] sql: rfc for SQL and Lua functions
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
                   ` (9 preceding siblings ...)
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 07/12] sql: move LIKE UConverter object to collation library Kirill Shcherbatov
@ 2019-07-10 11:01 ` Kirill Shcherbatov
  2019-07-10 19:17   ` [tarantool-patches] " Konstantin Osipov
  2019-07-11 13:59   ` Kirill Yukhin
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 09/12] box: introduce Lua persistent functions Kirill Shcherbatov
  11 siblings, 2 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:01 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

Part of #4182
---
 doc/rfc/4182-persistent-lua-functions.md | 214 +++++++++++++++++++++++
 1 file changed, 214 insertions(+)
 create mode 100644 doc/rfc/4182-persistent-lua-functions.md

diff --git a/doc/rfc/4182-persistent-lua-functions.md b/doc/rfc/4182-persistent-lua-functions.md
new file mode 100644
index 000000000..80e857f56
--- /dev/null
+++ b/doc/rfc/4182-persistent-lua-functions.md
@@ -0,0 +1,214 @@
+# Persistent functions in Tarantool
+
+* **Status**: In progress
+* **Start date**: 10-05-2019
+* **Authors**: Kirill Shcherbatov @kshcherbatov kshcherbatov@tarantool.org, Vladimir Davydov @locker vdavydov.dev@gmail.com, Konstantin Osipov @kostja kostja@tarantool.org
+* **Issues**: [#4182](https://github.com/tarantool/tarantool/issues/4182), [#1260](https://github.com/tarantool/tarantool/issues/1260)
+
+## Summary
+Persistent Lua functions in Tarantool are such Lua functions that are the part of data schema and are also available after restart.
+
+## Background and motivation
+Now Lua functions defined in Tarantool are a part of the runtime environment and must be defined again after program restart.
+We need to introduce a machinery to persist them (make them a part of snapshot). This is a useful feature itself and moreover it is a dependency for functional indexes:
+entire index definition must be a part of database schema.
+
+## Detailed design
+Let's store function definition in _func and automatically load/define function on bootstrap (as well as replicate it).
+
+### Extend schema with some new fields
+We need to extend _func:format with some new fields.
+1. name: ``id``\
+   type: ``unsigned``\
+   Unique routine id
+2. name: ``owner``\
+   type: ``unsigned``\
+   The object owner
+3. name: ``name``\
+   type: ``string``\
+   collation: ``unicode_ci``\
+   Routine name
+4. name: ``stuid``\
+   type: ``unsigned``\
+   This makes Tarantool treat the function’s caller as the function’s creator.
+5. name: ``language``\
+   type: ``string``\
+   default: ``LUA``\
+   Procedure language -  `LUA`, `C`, `SQL`(reserved)
+6. name: ``body``\
+   type: ``string``\
+   default: ``''``\
+   Function language-dependent body
+7. name: ``routine_type``\
+   type: ``string``\
+   default: ``FUNCTION``\
+   Type of registered object: `FUNCTION` or `PROCEDURE`; The procedure is a function that never returns result.
+8. name: ``param_list``\
+   type: ``map``\
+   is_nullable: ``true``\
+   default: ``{}``\
+   An array of maps described one argument e.g. {name = `a`, type = `string`}. `{}`(undefined) by default.
+   This field would be usefull when static field types validation would be performed for function arguments.
+9. name: ``returns``\
+   type: ``string``\
+   default: ``any``\
+   A field_type-compatible string describing returned value type.
+10. name: ``aggregate``\
+    type: ``string``\
+    default: ``NONE``\
+    Whether this routine is SQL aggregate function: `NONE` or `GROUP`.
+11. name: ``sql_data_access``\
+    type: ``string``\
+    default: ``NONE``\
+    Returns one of the following values:\
+    `NONE` = Function does not contain SQL.\
+    `CONTAINS` = Function possibly contains SQL.\
+    `READS` = Function possibly reads SQL data.\
+    `MODIFIES` = Function possibly modifies SQL data.
+12. name: ``is_deterministic``\
+    type: ``boolean``\
+    default: ``false``\
+    Whether the routine is deterministic (can produce only one result for a given list of parameters) or not.
+13. name: ``is_sandboxed``\
+    type: ``boolean``\
+    default: ``false``\
+    Whether the Lua routine should be executed in isolated environment with limited number of available modules. This option is compatible only with Lua function.
+14. name: ``is_null_call``\
+    type: ``boolean``\
+    default: ``true``\
+    Indicates whether the routine will be called if any one of its arguments is NULL. This option is compatible only with SQL functions.
+14. name: ``exports``\
+    type: ``array``\
+    default: ``{'Lua'}``\
+    An array of `enum('LUA', 'SQL')` strings - the Tarantool's frontends where new function must be also available.
+15. name: ``opts``\
+    type: ``map``\
+    default: ``{}``\
+    Options map (reserved).
+16. name: ``comment``\
+    type: ``string``\
+    default: ``''``
+    Comment associated with the routine.
+17. name: ``created``\
+    type: ``string``\
+    Date and time the routine was created (format `0000-00-00 00:00:00`).
+18. name: ``last_altered``\
+    type: ``string``\
+    Date and time the routine was modified (format `0000-00-00 00:00:00`). Initially `last_altered == created`.
+
+The updated interface to create a functions in Tarantool is:
+```
+body_string = [[function(a, b) return a + b end]]
+
+box.schema.func.create('funcname',
+	<if_not_exists = boolean [false]>,
+        <setuid = boolean [false]>,
+        <language = enum('LUA', 'C') ['LUA']>,
+	<body = string [''],
+        <is_deterministic = boolean [false]>,
+	<is_sandboxed = boolean [false]>,
+        <returns = string ['any']>
+        <param_list = array [{}]>,
+        <comment = string ['']>'
+})
+```
+
+### Persistent functions in Lua
+Persistent Lua function object must be assembled just on insertion (in contrast with C functions are imported from .so that are loaded on demand) to export call pointer in 'persistent' table and to safely construct a sandbox, verify function body. Anyway the function internals may be invalid, indeterminate(important for functional indexes implementation); but there's nothing we can do about it.
+
+The function creation process is also could be unsafe. To assemble a new function object, we must evaluate an expression given by use, that could be `body = 'fiber.yield()'` instead of `body = 'function() return fiber.yield() end`. Therefore an assemble of a function object require own empty sandbox.
+
+#### Sandboxing
+Persistent Lua functions mustn't refer to runtime environment objects like global variables or Lua modules. To protect function from doing harmful things, user may specify `is_sandboxed = true` option to initialize the function object in **unique** sandbox via `setfenv`.
+The sandbox provides an access to this functions and modules:
+```
+assert, error, type, unpack, pairs, ipairs, pcall, xpcall,
+print, next, select, string, table, tonumber, math, utf8
+```
+
+All Lua sandboxed functions are stateless.
+
+#### Privileges
+The reworked persistent Lua functions use the same security model, that earlier. All access checks are performed on each function call, when ``stuid`` field is defined, the id if the function is a set-definer-uid one.
+
+### SQL Functions
+
+#### Background
+Currently Tarantool has a ``box.internal.sql_create_function`` mechanism  to make Lua functions callable from SQL statements.
+```
+sql_create_function("func_name", "type", func_lua_object,
+                    func_arg_num, is_deterministic);
+```
+That internally calls
+```
+int
+sql_create_function_v2(sql * db, const char *zFunc,
+			enum field_type returns, int nArg,
+			int flags,
+			void *p,
+			void (*xSFunc) (sql_context *, int,
+					sql_value **),
+			void (*xStep) (sql_context *, int,
+					  sql_value **),
+			void (*xFinal) (sql_context *),
+			void (*xDestroy) (void *));
+```
+With prepared context
+```
+struct lua_sql_func_info {
+	int func_ref;
+} func_info = {.func_ref = luaL_ref(L, LUA_REGISTRYINDEX);};
+
+sql_create_function_v2(db, normalized_name, type,
+                       func_arg_num,
+                       is_deterministic ? SQL_DETERMINISTIC : 0,
+                       func_info, lua_sql_call,
+                       NULL, NULL, lua_sql_destroy);
+```
+
+The persistent Lua function has everything what ``sql_create_function_v2`` needs:
+1. ``func_lua.base.def.name``,
+2. ``func_lua.lua_ref``,
+3. ``func_lua.base.def.is_deterministic``
+4. ``func_lua.base.def.param_count`` (it is the size of the ``param_list`` array)
+
+Therefore the internal ``box.internal.sql_create_function`` endpoint becomes redundant and **must be deleted**.
+
+**To control which Lua-functions are exported into SQL frontend, we would use `_func.exports` array.
+Only a functions that have `'SQL'` string in that array are exported in SQL.**
+
+The SQL subsystem has own function FuncDef representation and corresponding hash. It is inconsistent and
+it is not scallable, so it must be reworked. SQL subsystem must use Tarantools' function hash.
+
+#### SQL Built-ins
+
+SQL defines some names for builtins. They are:
+```
+   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, GREATER, LESSER
+```
+(the functions are currently not implemented in Tarantool but must be implemented in future)
+
+We must forbid such names for `box.schema.func.create` endpoint to prevent a mess in SQL code.
+To solve this problem, we may set `collation = 'unicode_ci'` for ``_func.name`` field and put all built-ins in bootstrap image.
+
+We also reserve service SQL method names
+```
+ _sql_stat_get, _sql_stat_push, _sql_stat_init
+```
+required to work with SQL analyze statistics.
+
+#### Functions access from SQL
+SQL may call all functions defined with 'SQL' export in exports array.
+
+A new function `LUA` allows to evaluate a Lua string in SQL request.
+```
+box.execute('SELECT lua(\'return box.cfg.memtx_memory\')')
+```
-- 
2.21.0

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

* [tarantool-patches] [PATCH v2 09/12] box: introduce Lua persistent functions
  2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
                   ` (10 preceding siblings ...)
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 08/12] sql: rfc for SQL and Lua functions Kirill Shcherbatov
@ 2019-07-10 11:01 ` Kirill Shcherbatov
  2019-07-10 19:26   ` [tarantool-patches] " Konstantin Osipov
  2019-07-12 21:49   ` Konstantin Osipov
  11 siblings, 2 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-10 11:01 UTC (permalink / raw)
  To: tarantool-patches, korablev; +Cc: kostja, Kirill Shcherbatov

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, GREATER, LESSER, _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 -> 5794 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        |  67 +++++++-
 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   |  76 ++++++++-
 test/box-py/bootstrap.test.py  |   2 +-
 test/box/access.result         |   2 +-
 test/box/access.test.lua       |   2 +-
 test/box/access_bin.result     |   2 +-
 test/box/access_bin.test.lua   |   2 +-
 test/box/access_misc.result    | 133 +++++++++++++++-
 test/box/access_sysview.result |   8 +-
 test/box/alter.result          |   2 +-
 test/box/function1.result      | 273 ++++++++++++++++++++++++++++++++-
 test/box/function1.test.lua    |  98 +++++++++++-
 test/wal_off/func_max.result   |   8 +-
 25 files changed, 1199 insertions(+), 59 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
GIT binary patch
literal 5528
zcmV;J6=&*GPC-x#FfK7O3RY!ub7^mGIv_GGGA=MJH83|VXE--CHf1$3HZlrHZgX^D
zZewLSAY(XVVlZMeI4xu~W@0TgGh;F>Vq!HhEjeOlHZ);5G&DA5WC~V8Y;R+0Iv{&}
z3JTS_3%bn}9RSW`n{|q%0000004TLD{Qyv<r~q2Qa4Sd<fLL|>|NsC0|AhYCGp|Cp
zDLnH$7i5tTovO&>kEORvNl_|Uo0F6>NlJh>D~+kHWn-XtYY$}WkyGVotR)mB|2b!G
zh=~NH0>lDx0$c5Gs1)PDmMqNE)(^~<n5|z2VYYt1BnM!&_-}&__>ccI;DG&QumSsX
z(t!Q3KVb&HUz5}Cmt+$DsD8;{Q~vTY<u5;&zvM6e^}Bz+m%QJ8yItMwlDfI8Zgm4&
z;mUI1`U47A;rfMr*Zq6f^~<}ytGd@UUDs8s>zYK<bv^aE`p`9*PpvWVX>De$FTg;i
zw7`rhE4Y9uuS2H1Dk-pFUA<JWu3act*CZD&ueznnt7_r$s;}DHB2&ZD5>vD56=yWF
zr)qS(G|jS0Qzc!R%w=JfIHi~}DaY<~sJ!81T3C{HdgZp$G+WDtWv572c5218)14-(
zAf;)|N}3*3Nz){)_P^25|1;YENh(INHxtYxpn~T9y~fJC-G8IGf7A5(O#~A_Ac58o
zM4<H)2wGkAK&y$Sw7S(&inMlsNb4w4v`#kQfX^#sQG8w*YryBxq0cLaWU@CxvDh01
zhIq3f-s}w&A(_?_24Y%Hp#0j?e9hXCsnM)o+xj$bSJyPPR+A?gUBF33gPLSCqd*G?
z6$lav695tl5m3M%kbw6FAmDug2jnveGa#SIp91npmCqy-m@LcZCziE@$&u|sT9y(Y
zPrwrI2T<aD4*{HK0L}&I;XD8y?Motn_U#0qecFd4iIdZj9VaIvNSvH>j(A_hi1$Qv
zydP4ecOgW24>F{8AVPpZ0D=I4KY##%Iq3rcS&ttmZ%^&3Vmy4LnQF+JX*QEPdI)>w
z+h@$-0o>g^?$fk-?Z|n#o`gN`-}OhXU*)=gj|_vRN3#vFM%#1q_lT!pJ2^Qo*Ao~t
zU34>?&B%x~Z#mI51>4Daxt{Rr#oO(>4j#+koR{k<4ql?$Y|21m6if%ZTt(=J5ka@5
z*ns53jEEgCmzexTYFGG)=r;A4U60hj%!y*iV04@A+jbQh#--VI{hgWhRq3x;jhd+L
zU8fo~Ss~v+lMTjrX$Hn4z)k>2lDw<aksTvQbdKm>+rKYjM7D;APHfGPB3sC}yG95R
znO2Y?F)biMl(8XM%%^Gr5Jbl22auQ#yu{1ZDNSCZsM}|<>G5NA;^AX;+R<ZmTAgxu
z$X<Afmobc&IYOZeFY!_qXKM<!gO^N9$vRwk$&{ocj!Zd69WkXGbY!MsW>Us6M;nAg
zjyCv49Br@-x&g-yw(-Uevf;)K?%*12^j8fw`kTfY{p~LrYP|0mX}qr)XuQcX&X8r1
zVMZ)#j54-fD)r40gN**e7^8nM#OS~OFT(iW3o!og;*0;2TzK*S7G3<G)Pf7DWENYj
z9}6wke?=DSU%wSt{9nZt|5IVb|H&z;$X^O7@{eMQ{PTxG3R#;cqKLI|0*atlgKA)J
zQ{7E7=-V{$g!fE1;r$X#_@oj{$g+}HB9?`O633r!X7^b~B+<PQNOWJs5#4vcG-Anb
z6|v-rST6Y`Kg5>zLJ;A75JPwmgb?2U5QO(W0O6B8{2+6~gAXw`aC$g5>aqH{QPbSs
z%?><luLGRzbJ$_~?QzgS`x|o5-Ub}B*S>}uvVUVk_HJs(K6|Y<ZExeI?QFE#-u5+Z
zSeFJitUJRR)@9u+8nP?H4B1tamEG*7k)gLrHN8!y>1}VZl)&~X32d#ffGx1Cx<XpD
zo*%2WJRf?c5y^{3&WT3OheaQ!BKmj?Lm&5XG*lmk82*0&&HpaI&|7$+cNSdeeT5d-
zGc2&I3M;UznKM7&lnPR2e!w5F%v3N)Mk*LpCK!Yw^Z%HrWC~C$RT70UoIN20&YFM%
zXE|HK2_%>hQ;tx=){h<jSfvOg$U+!l>z4#UgsorlgAimMY=IDa`XE3UuqH+?h(awu
z;tF^aT=9aDx*&1ERJKsjQq(Kr744#oFCv|bC`(Qh86u^j+z<|GC`srhgau0IN{}rG
zDagvJ>h%Ov1*irEbOfVC!Hj-pQ9o3CCh(EKCxX@IbqNGY9#AqxNl_0`kCYw<UN4>~
zI0_05=4pUOmL!&B2;&{09R^%cP(0{&Y4g_@M(h5GE*-p_g8e}0-w)CDBz`}XI!z^J
zpIou+X_Zm;<!#mZGq8BYb<b`U%I@_%^ZCyGd)}VrFEy1Ib&_3Tv1_hX({HRRsWh0^
zq!^`tKKa|fS%t4GKt@$}|0WV61--<m?_c&bk(dflaCf&z0add*xVu}`Gn>y{^-ul9
z*IsGeexAE}4DRllMNE`{RM-9unnpZ?!1~`T)wO@kA{IKJ%*o$0Xc}>l0bPGKo8M;^
z>u251qXq)o^Vqsg`pB~RG+FlLsBUU~D9_&X8|&QPZ@z(De~p@ctMU3gv&k;`G(W4l
zt7@+DyuV-fqu;Cv%ssnSrsn&<ZO0O#t&gbYSLK;SzF&JJt|?Cbre+ZX6)+Vvj~E8)
z*)8tjRFg*eHjVh_=S<MF!ahuXMolGN;={9M+t$aPLcN~(EdHLqFE(Yqfy1s<{rNt+
z`g`ITuHPl?%_^*9iJ72z#63!W2KF{Js+moTpTF&CqO9KO72hh?Ml+@n^ANzKchC#{
zF=$@lBm=k^IP~9biTP|<qbvMfbEdXPHO1TgGt0K8X@z%sc+jjbv+T=FzpwPm=3r6C
z^YBTie?yh;{=HF56c|_22Wnt31NC}(^N4luGWVW~nswh_<enX(nycFfOw`~sRC1KB
zE6F0Pp{pP(Sd|a5=ZKRSUpshNd=>EvOTmcnGESU0Mc^TiOZ%>ao4-h^9WNJ_f)T~g
zwfp)aW`gG2tgHJK&VfO*Z6}_a^}VhUzocM7FW@ELKr|OImVybn5P_PmuF$hHELF}X
zS-fZ_A?^#gSeSHh4cai2SO3J#>sb}1YyT3z*RNW{E-9E$3%4x0VHC>sPY@8ATlVKC
zNTclu<OB)`SooXo2Ub}9>nib-GheIb&)+9;kqT&UPu)#+$(vW0$V_fW#8RecGaZjh
z?T8(a;_Yl)YDOHTjIO4oR>TdE+0|~d91V$OQM05WjzQUyhInX9PFM4S#6l_9ZnqxJ
zh#5e^D0efm!cb1oG8^J2Cd_y_UJ*Mfp|>02CM4*x?1*@Q5tNEJNQ4^|1`2^oZHN^B
zVP`ZO;sidxV!IJB0v;6c0Z+JIj%IGj>8h{+Vt6$kEr*2*2$M@1Vi=fQ?pCFUe>#{U
z(XKEP4oqE*N1N$pNHjIrX9FuZ+s#&^9q|$kY&9AYE6E@!cZ8eijF<qY+pUMIVl(0a
zgj;gABJOFbDJk`en1|^I*URaOSb!kUw#z9YaR4t8&kC+~GvXwF$15excsm&|l5X;F
zHJz?U!@@_Fs_1w-BsSvKc(WrU)@hS$JR&Z_XGcU#1lVPEgqzuJy(?9C2+Nz%a5<W(
z)XV9tU|}IY-p;7EGvXj$XtSGAO?SjVHZRx9Qp5nPxSVaLt7$pnA1ap`(h&Pdbu%Oy
z5%<uwqa4kK>m4x<l!vqFl#F=CStVCf>cTVP9N!q6jd!#4jLch>-4#x<>3Br^%0;p%
zMX@PDLq9}9PoxMvDH;-r&432Xtalqi@c;*o=It2%rbsli-ff86ly4|q`xgq7rq)LT
z#z<UKzygO4sKb+InqmkI0RRAi008qE1Q5jKw3Dz369B-$f?<)8F*p<p1ThSj$|}l`
z0T=)XKmq|Ep#enoh9APD$~G`3BPRn_23ICmCTB*jFq0VY?Qh{WnK8^W0@y?0k#t>C
zm_!)8p{Z)-ZANC(49KPgvI<x`>y7yy5Y@8Js%5iT-Y;}1sSchtN?#MzfRT)8sVOZl
zd(6M&!<Tp4)~~V4Y1yJ4vs^j{{2&8&B3%?z3yu&cBL=v^C^~NeFQ7|;C1dDJ8d0_1
zo`ectu?k?Jm}U129GVd-%2BhDU=QwnckjzqJ6xv*ZE(J^4)wtknIhzGc@in#TU|JL
zk5)&al^5z_-nP6D5uSJ#4P#5CpA@E96(^-iyu9}()6iiq`G&h&Lj_n&UslKK^oq*|
zI+jw+gaf|e+U}wnoHMH)6>&UXFG=Qp(W{80MIo+D8_;337Tw!-ShDi!3{*dzn<Hlh
zflK4<yS`cb>asoycE>W+6KbN-DoO75+&5gi^(h8=VYLD5X%{4l>z9|;|KXd<7bx{~
z^A_QHy=Y*2893g@Q@YvP@Ug$aTdCb>q2aaBHGTlv(IlW_tHb?jBgI-4WC`8pN~sZe
zT(|iN4QPkTKUfi==7QZ7#}GH%zUQZVI@XdqfH^cbvrV4jdXQA*;fby?9m2c{`Qn!+
z8aP&xGwTTi1*HdmLiq_oeJ_d7%<DV{gUzjA`3DDzr(#g;NxTN24u7=^sRR}m6Zsp!
z@|+Axe{xzu4UrsVwE~|Ptv)~kkmFsM=qF8J@q}skl#}!q+`ng*qN_>hog@#IqJyg4
zmJG&2BHDPWUX@=6q9O-}TdW{>jXWGq_r&Lo3#=Q^JEU2}aF#*A;bi$Gkx4lO?BzWW
z_P}64lP1`omo<&49mkQNhh0f!!>?G~RnXm46lZtgx|5mD4f$Ig7?$;6$g#>=6cDRd
zmsfqPBe*o1yAl46Ql-oQD#>-enJ+5$S|WWC4aQFmQg-0%sJ`HwG~A7n(Km__7r~b!
z<g3#uo43{SBCz}wdu*2U;rL4`nI1fAW^CR4%A+dnuX*#-{b0f6Cq}Y{2H7FB)^wSE
zwR9!XnBPwt!?`Tei?A_ae7ky8FFkL;W<#5yFMm5PVS;oSQLGKq{gw5y=_!m<wv-^|
zxqbH3b+jWh%angew???#O0Ye@KDx9=M$Fg^1yRVBK&)*4tC$|Frcls=#OA*0j$!^l
z5(HzG5B6U0XI3o0biss~X64x2;(z5rxkDf3kUL4_4(_Y%L=W$|;tg|L+>I%9JUW>U
z)7Lt;#A^>$VLS8Ry0+OK(Y7nlx%+rF`k*Y=VK;x{x=OW?jvmQLFy&U7v`Y~0)Ut#P
zy?ANj7=2h~r9=}t=Tu>ci)g1q0qiW2;6CM1#O5sJ>|cWIB6o{%mJ{4S+_ks`)<0V(
zOhDn8U(EU~G&(sw$R#1>B4G#%1P@ZE_!^ePa@(7Ty)TYhTg*~Nf~sJM<6gQ~ZN;C4
z>VI%+$1@d}6{<n!=lIP8V49<A)>2D1wygxW5LcAIBPeg}Ts?~)0At%LXK@BH1Xc1r
z@X)+nRbo8xM$}MkEGOEC%#I2O6RAoBZ^QL&h+t8G6)yKiVU`3{f&YTrQBtE+;R#Ke
z1Z997p0BB?01S=PVmpEi(90$lJMXe<GzLMyhO)C>ga=sk)}`D0Q13QgUMh@JH)Acy
zlEP<lpbhc|J0RiW7FANlSw&*y6x|)ILwG_4fz%WhCK9QKqQPix{3=<t*iOuV5<Rc-
zaU*Uh)ckp?<`O7@u79Rd+=yAlIUg_XT7bEGrGxBH9#SyqOn8K1Ey^H&81}}-IY|-3
zP+k>;ue^{4Lx<vQYvEL#VP$I2!UP>GBj#=7yP2s%$$G`J<Z2CJqNy>2t`r-VT#C{V
zelrCgj2+5DL5C;xhLx#F`?J<qe?*;O^|Q7pQA2yuu2`B$0)P-Ri#U*t>)d^E5Sm(J
z#nhmE^A_A6lLyl7hUeO_&6F#}h<!Xm=|CPEu<9M5^*nsQ<6=J7X60p(7v1De7+nkc
zkRW><y1C;6o(O9*B_SOY(iuCIhBQ8kH3W!$?KH=Ts9RaUA^$LJ4-9)$LBOJ%C`ey<
zp$|q6;@Q^2sd{6@%AgHMq4XP8zArtmg#t4`BaG$l%^gUkeG=`<?fL#aI=ax4Sp72p
z$^u;YI@17i9`z7C2xrPhniCI&Oq&_RkJbW#u?r#=Tf|`n@ry6q!RSCZYdbt{K=vRV
zC>VK4d^9#KW)MGG3k1e4h*)e9hZV#xzHkSl1L3Ug@VEilgLI%^<SFsd*tD2I{Aeu@
z7`q^1u|*u3b@3`70Uv^dz<viHzlUm5vdM!XMtt((eWG5VR&*G-4<Sav_^-Q#9EutA
z%*53QJBe%|0A$f%AFz|i2!R0}8q|q<g^eHpa-KqrcADWA0qBd7=aN7Hh-0IoCK}8l
z<n!bVR8L6!)6e~&L@hjk*ryOXZMfH8LI=eRtqoNW57AH-F$iRR4E6So^Z7_|8^s(#
z1wOrgVnzTCdbo+!bkSwQ$n0pf?aBvPa_3zwjl`}>N=>qP|1rQy?*Ez)AOr{vIf<qf
zJze_Pl{D5bAFo9Qs455=ChSon^>%L+Gc?NMR1>T-_LF$@l8xIr+Vh0GPa*fd`6XRB
zr4ATcm*L+G9YE|tAvEsX95MEZ4X6-(W$a}7QY;w$^%P);9~ggG3lA5J9^~f^LI)w7
zcjxAau}^G3h3G3|C)1ZgmxPsx!3SK?rFxFFte`uF&aWpF0&OdfBYLzFSgOoRM80%b
zPR_&`pdOoJ^6`N(QRYt!cFfB4TfcR4O7(7-u_t^$h3G3|H|wXAQ1S}XY%=JqYAd=P
zYvzQd)&?9gTc~odATR5;JWz~X&g`B9v52`vF^?=Kc^YOFk6F;7RnElFy@ragbXzap
zjB1`3aD1scob!qzWzqbMWkz3k1_$E5j#|qeJB#;j$A;W7Vc1^;A=tL^dZPD;dRw{Y
zfF}2?B|P2UowOX?W~%{*S?JR&qzckjR^A4JNtRtF>S?$9-eVKQSZ6Z2POzP^5yVfS
z!^7I9FoCFamEwDDMfldMW6e@)GK6{lXmij#;7bZ5<q#xVW=4Qu#XO>3CJYEdghTT-
z5VeC8)%cs45ITzbOUs-OiL3>J>!9n|K+gS5qG*69qhd6EHDQ}!pUg`y9-HisOZ0h7
z>zTQz{(8Ef>g21W@`l(uopaEppCB^BTE0^LT|$YA!vxPANTJTF)HF=4Ge+my;|km5
z=D<wkDj#BgQ=Zp(_wD<3!9qtJCiq=KJzpg;+Rj)#6ZiQPNBQV91Hm8$;S{Osoviro
z+xM<Qm9&~}zd3&^Tc@kE+NMuQZ)JQh7sVU4iA@Z~c;be1ZN(MYip3BD=|+<Hf}gww
aY)=?beW-aH!0aL7On%F^6@AqZt?dfW@NL%s

literal 0
HcmV?d00001

diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index 56943ef7e7d0fe0e3dbb5d346544e2c78c3dc154..a9dfe8fb7d486e936bbff2a0f13aebeba9ff264c 100644
GIT binary patch
literal 5794
zcmb7`<v$$&!-gH*j&6=-98-tsI=Z{NyW2S05yNyFn{KA34a4;G#B^;s56|<w|G|5I
zy?@u2>&t!Xs7q^N^KkR>W9!>_`#5>Hi-7pJ`M7z&5CK8310RpAfPft@4-dAIyN|Dx
zyN#U)$c~?%hsU0e2h7K3F9;Uk;THxA+gjOz1q5uYtZnV>Z0v1#vGrwLJ#1V=Kq2q3
zu@}2=x~9n3kbYjP7jb_<`d`?;zC|Eu-ZUb0jn9CUfS$&FJd->_xcrkyq9{DVJ;Tdc
zRVTqr{}z7%lfJMwhH~m?XDI)wuUuhvk`cPS^_&K<I0tRTot@!?8Jdi+&B;4VWnSmO
zQOA%Mk@J!H9Z-ow@mBt%`pJL*XAzjav1%Bgdn!TdK-l;GY?KxVUg+1MG95e=i&g=b
z-dM-pg-|?l#VZJnAgR>VNE{Yde_Uuh4EgjQ40V}-6!|ojmWqG^zoIgBs64|f2jWUr
zahYrIrubnbl(qPxVH5F0yN9@4I~&}P#}W#NG3kKUdD(S((Gn%sc!>}7p3wE41n-+|
z>UAtZI_hi4>Roj3>RqarNGt273Y^`LG+hK-%wPZkHo)PeuoOT$NL+bukpl^E;%pD|
z12gq^36AFk#n}Alk>Ry9-{yQy{xo|5&z(I&e1uT1M)?NzTJosArHI}^%kSo?)xm)H
zNSrOxii!A$<dNr8&^~`$8xUT@!LY|D>^&_ZU&lYUY)RDN3A`f074*YG&u?3MHO+fz
zSb6bq-<FvmmcoO9ozFL2>L=F_?RifMS`Al@SRfoEW1HF1n@p}c`@y;6FPwGmUrAZ(
zYPg)#MiPEaap=K*E0rN%|6Cy{F-Vd6t6MsI$vkBr^qKv^S-ID^h+RqGExWe8xkf4O
z8p-=v6XNQ{daE^Lh=&2W+}CVkb|be-<{Cxy=Dfu+R(V!M`9B|-%CxYEi*4a|iW!07
z%u4F{e9XdbRJ7wx`s(@ewypJyWkv>sOe$^EZf4Mg=TyeuI1D@Dvp@;v=qnJk0EKc+
z>Jx~H0u_n^`4<Ny%}fEksiu>xtb~OIPM5{qiNbo82FmVeM1w~^mEBPb$3v;##Sc>n
z#SgE4wu%xgi~X^t8&a2xOEpmNWp~z6KA+c~B4nSs@;Oa<eZ%aWB$GJtf;g&<_qHc|
zskk$YVF?jppZ89Z`aJ&YYI%O2N)ijs;{$x)R|#Av)?j|SgS}}VtT5PUD=o>pOhuD;
z;r~>hs3LUFhHf&CA-R~WZdW>|j2>;pBO&GNtzTlvtsZBL%WKU80hm16`S3dJK6HGq
zj8kCPM1*{y;uL7C(pVU_Wj%)691@{yJDiKNK~8!l$HhZ*A>{7U7UllQ^i}S<6)QKY
zYLhfo<#_r+$GvXkeDnPRYxP3{EmB|vq()D7@!Ls>zSgg?0K%_PykWg*cxYJ2m$HXu
zRiUUi_kdklg0&a%6uMRsQJB4-@4sdF-VG69|Ct`o!<?XvvF!qmS$nBZ*?2Spq7e5<
z$1S$7dR?DOWx0-s%0fzr{u@dG{b~w+{p!qP1$1z1@xAcS{ez9b;6GwXr}w@86>-nO
zvWSZkTdb|d^Egvr5zXrp43N(fvFC6gw`i5)?r}^s@tq}GD<v~MwI(w;Wh65`-QhM*
zlMAowFYVJao!xxVwi<a-*BW_JmL6G4vc@(hC()klDvIAZnIh^nQ<s_zC)T*0jwVLj
z^+AbWIR}PcmAEd#iLqE-BpKna`SPH&MXsIr8)B>B$2fLt#0hz1{A(1&PB<v~zVAU6
z`0#`TeGz|74Sx~4m+EejC1?>9`KNj9tE+bErWrs2eiRix-4e$a2@IFT?{z}KZ>^_t
z&+(hJ9J)N9S$)5R3-Nu6g12#%`|0Vtm=F5!Rzgc6)*&P1k|#9iRR;HWH&WCcxMxco
zhdZ!O2B)_Q8Am`YL_cSHwoIl=Z+daQuJrIWP<y#W96Pv8k|E}-T?l}7rt(fYgh+M9
z{E1L`Oo9Mw<qsvp)9JU%Rs?=g+uDV5$lA%el^Nk_qdTUbVYXj4bc`F5!8Tjnz`sQl
zBl@?<(RG_}Mf<lsZ=$t2)~3uMIaheyFESnVlLzgSiL0`JH@RY+yEIk1&pD`<T+FHA
zo~YiSl=K_%`>j{}o#*KYjQ3Ia!Ar2H-~7w4m@>eGbt+(@H-ewp+RF3BUXg}q!;&=7
ztv*5DJz=C;@U^AM*<5qP9s;%;TEXVt;zA?y%xWRl1p&|iymQ>$)Vh?#M6XCIhn~GO
z=c8y-F-LCOY&6qx!%eO%M?Z+pu+_%dm9P>rnb}p9m0Ey1B~=LT(Q5JFYscTs+a9Tu
z6}J<#Uevpy%1bZFM0V9^39t)-WrAiV6ho34G{Y)Xo(m)iKyS`~x?RE87TBgO-s+L}
zAtzY>$1)K?8_E7?^%POYoVt?1m+OoS6=a>vYc#C5-RNDGa#9nYg}p<4zXj$Nqwx-k
z#oDAc5;RRzskE%cgRhEYjE3>141>^CsBKK8GM69}%|!2LKvpN#QHGy^B`NA-<mdj{
z>f1vh=PQF;`IDD49?<jfM@qS)P5MPaxv=maWi2-QPp(aFZ`f+iCrs+D9jv_*Pm0XI
znHw(H2UmqpcRgyeRzN@N8zuj=@%m1((l8%4v#9|IHYHAk&yUR)E%5UfCF)O}cPV^L
z#B+(Qk1uvQ8Sr)*(tv>6985@eVt2Q(c#H6cvE-ry>@4fo*+C~x_Q-~r06aU()bDJQ
zv3gip`Pb`yO+6x!)k~X5h$B2oP8Xh*>Wf%O3M)3MFZR>O&Qo-qw>wckVwu<{v0KTm
z39d$~77JPMsP_%;dtmiPd`);%8$A)2VyE6ac;;F#W1loGy1P5+z0Q||cRCp0S3kUX
ztaT<i<3O>mICpdHdM%#l8|k%Q&}W$?L?ZQ|*@mnqD{ALVzcTb6<TIDYFx3e&qY{&z
zYfIHlKgT;vwSiOBl7rQJ@}9-Riwk~)5~rBq+#G})9sf=9><BAsRrt!2#^{l7NJLEd
zkh{EAqMfs1d3n{Yrj@xU+A-3e>as_z3y+IcJTkkM@8c>pF;tAthbz2pkmb&2>2-bI
zgk(FX8yWU&NVd4<mwI~wXX&Pp`6qg5Zbaov`ZZi1yclo4V6n1hWJzhuWI+jupbC}@
zOxDu(xm+?hghsLbjKi6r9GKzbKmvhYaLM3M2CbvWq_^fE!x1`%AhZgAji+Jn$tX)K
zp*UUkmX(()LV6rW$y91?kdVxllcLJfRwJ#5kpn1gHHIal3VLG|9}Qwc8fS`T8#@`L
zrS}hB01!neV2XD6!cwE~KL`sI2<AaL99^eUnPfT9uN)|Kb|`v3g0~%tCEnj#?gCZ+
zyC(U4lQu~dxEAAC&0YQ$#l!Lvib5yHy)ofCj%{aSzAgPrdOORRTv(D1tkuHsi%hO6
z6!75IF_pLp$~=o7(f6b*zT>cuIP%KmON>~`K{uM~J>|wuUAJ#9LlD^M6#gtKJ0SA%
zfYxg5PC*$F8Eu~lIz;`qX?b}?W4HM-4l&1-Esx$7$HvDcKWi?^P%t$7h0d-_Y<1XR
z>pHl-fQa8}t7aq6JS*^Msy=R5ztI&^lrrfCuQ7cHe#a-oQoa)#K<uNk!{HX(ZtCV>
zW1XjWb`<ZR*HOWwZ{6}|W0J>f4z0uVyh6vCgUoYfuVZXL>kUvx)S8`SAym|YNHTY8
zNz|H+=-=RVYKIc4rcx5BG&c^txYWM?uqw@=R~7BWNA&(JZ^hc^2xj$W`Mb518D7yM
zGWpW)0aq*jruT|@8}oz{GpGKY+)A#kQd8q?G0E((OLqyAWCNe(*#SAMz}ao@j(j(G
zA_&&KlA1FH(CB70ORgA}t@Q3JuE$Y_$54!(dMl|4_(erNvRtI<rk}U}Vev{WC0JUM
z=H*RY8b6Q=nBZudA-L4kfLIWqK9`kAY$T69t#mY|RC0@1q`*Tuge>ad@Q78m0s?r@
zKVxE|zo4ut$dPl$`rkydUjvWMf*U_$Wf1;iZ%yBg&Mf`8JmKK|z~5$!fBfBKhkC93
zDMrbfy2d^3<ig!Lt0Ojba_T7F2{j19Molz5(_UVPSMdaH!d<Kwx+uwb7ne{OWuua(
z9dG~NH!g*Q#6*n>j9zR^Z^R}>f<?#-A@6BBGLv9hQ@qj9%V0q1S409Rf{=))T)}9g
zC2a4gb5K)=v9^Oi3}X17p_72lIf8Sr>Qccpmy5Bb=US!QJe>jif}rw1>vuHlugTgx
zbzTuNZe=8@E~X`#YwT$AROJ898eJ2OE9m;+Tr#cpL^~!Cbu7NG;@yBfNt$2`h?I#`
zn#_VyYdt&Hwep}UNe779P(%8(RR&vt(FWEY8v8!q44<>TVqSd_c$#aPAUE|Ky(>6s
zq^I*cLMulZAV9zLI+2wV`9s($Ol7pOB}&Rhk16(L_!hmy7MF+=Rd4>TpmgwD5J;Aw
z!45)UgEq<uHxny1VeBll-u3=us3%rQ4({FKcm&~}5?;yyqBF`=0q=sc9TH^fAgk6d
zcvC<^uUJYdd)a;wj+wgP0^ORRi=V3VQTW};+vjwQx=lGJQCJOSRk8Fl7pG3FNrHcu
zZqo+6=mvhT0OtS9R&oTKse>7v^5(x)0BQAYF*^};{hS0kC^<brK88M#Mw3Ig^f3)c
zf%P7oNd0_{`tM6u1Q^Z_mU=9@O%{^Pqop9X0TEauI(kD(0Y(2N%<^BZ4X^{><8B86
zDpM57_zKSp@0T`V1L^C_-n)C8kT9~=G6IfK{kW&aw)`=0JmYes<0?$Qo^x7uD3hVR
z)oR$@W<Y4nTJcEAqQvPi%EbsDmh0%Q6(nxQabL4n6>DaXK1iVcoTtWLFa+YaZR5an
zccIaZ^d=g5{N9CevnWg$1vaq!fnM>VriVP1)Ps@!A>BC^Q<lt2;F3T!ct?)Wk#bJ?
zdkd<0&B1w(=~mR{gvsx*+;DG!6RZv&n6}%D=Sy~B%o-oXx2ef*bxF{#gn|Q#=5nJ3
z1tvP?%rLw(0zD%}I@_^OzG2P@>W!L{`2bJY82(!p#Sez>Z$VeynDN);ulWENX1$uO
z;9Hj_3ib7Ztgyx}rCjD|ew7qrHHN3%(FZ5Ejcw1GD98?W(*CFF;6w2v{-dcjA5{GD
z7fYOql2$qU{<?$J{>cNA5IH%Kc;#D6eDkv%0Wu5EtX21v!G0k|CUUkSCYJ{FWQa%a
zWwS$EW>}Ln3(Q8Q9aSiBVtm=%8DLQpmeQNQHiqHDDEU@}by%HrXSlqjxB;DABxw&l
zz(#SpCg@45&1aQa9HBBT@yzVh;8;-XphoeLyDVhaGSXw(QLwsPVdBaO=r$&ya>}_H
zq&zVx`MFu)UR)$foaBq26O)E#(%QW~=u<?a6QUiw;adS?B%a#`Z$RiCQrZcVR3ri|
z2VR@38H(`d)1Bn!_ivv$?`kEkbA`PRI@gU_SIr;({r(^{3HK5e_39A`!sWy|hW)kZ
zhQzt`ZPI>QIhyg8s`9yb@w)cXv5TX9>`r>qk?Py^D@<C}9j&(q_1YBtuWn%Hr*dfh
z$h)@2oDa$#Q-CfCURn|7LNzplEaZe7ce4Unx~&os9p2hOVS3<{@w)uqVl7Hm;N_%8
z(et&MaKuV(CJ^joWoU(6Z8%FQbYL~E%El;JR6-MfUpy4Lh!PL)P~~#L+qiR1@XAki
z$V<~njlg;RBh)58OJk<?(YR~&xCib+>xhMk9uk3D<&dg6hIcrimS*cQms>>ev*C?j
zPw2E$6fIk1hNyEniynPkIiC)yy?lcct`3Q!U%^ZNdWA1tU!v6<0p2oaY!Y&=EokFq
z3cxcm^E)_MfQbq1`g&0PkN<>GIvvpQe#)l>KvsA-r)RNxZhEAm<$i2G$gEZVkLNpp
z&})*H0wW~^;^y!Pb=y1P_JjFC6{BJ9$hD!8^8P1BhlM^3A-GgSp`$hjRY-xCO($Vj
z@>v6&lMdB6d)|%<!=!^Yo%xI{;KzalAQG90jMFB&9J!=C6{i$>m_9Z(0OT+wtc{8W
zb|Ou9^4=pMArvRiS>tX?N<^XC7u;j6TxufCJfv@}lVCi)gA_7%MR=S@;^t}hB-CQK
zN7cose<ZP1&c4qyJJwBH!nbx~)lK3%o&)Ac3==NE1*tC~bYe;8)o(FNB;n&z!!uOZ
zSraP*)I@#GadDq4Zb+ql`z~;rP*Nfw)iy7`TVhtke*ZPuaou|tG|P4!t+SbMzKx6U
zET*M@kSlJ(Yi}*G`rU-9;z&JLgRZ9aW^$N`vEnL36xFb4qO6JL2Eg$0N)*w4`aH|7
z@z4)@W!ZJ-j3P51u+a48=WRtdJ7mDt7>->2HYHHm_`3S!BY;PoVqSa1jAODtZ-kVc
z;6SxJ*#DjxHy+0jJ!OCmb{K_&Y}6UWR#te4g8cXp8C`yQI(}$?Ktg&SDqX_qAI5jB
zB?*Aio2JfWm`P9~GU$<JG`KKrN4Pb<OB`ds4zF;KSDUZc%8LKTUqr&{mygDm|BvT}
z{;F-P78BHdN~A-&POqj-Qv*J?XHXkD6Qe@Jk2h1pQ0cJbu|LG`FFKAc9LrM!b>j{C
z?8Vp*QDRvhj!!1zBE@AoQX7gyOXUtT)BCT7SvhK_l-JXu->;9N<08I%zwM0M?q+jW
zf3D~*Ibu+x2QYcFWNkD310W%|;v0>4M}B--&QmN~qtM?FB{5)Rd6SS^aR1P0$&GxZ
zF*iiXT3*0J{S)eJz!51C_}Tx*-iLS-?{SQ@U1Oi&@{cUGdbSMu(fh1$6ddTl`&}T9
zJ}!NOoJ{i@PdjBO(kILT>%UG5T0xeIAfOiJKr_PW@Kc~5Gle(yoDWSl$wdaTwA?sl
zOwM`~D*7abPTMmM9tGi4T(r-J{-Ds`S?KiAN-B)aF>;t$W*V|7KWt=?0nv=OiHHg<
zg}n#{LTM-ibmBXIma_UnW4$BTt5=ZHPZy)G0VgZDk`gzFl7Jmr1vQudXv(Xa=R}1!
zJ(fh6jB%`)D-ty*$d0ARE-?k3z|Y2g2}#31rT@-Wqnj4<E0EVAJ@V&ROR1KVeV#|q
z>{#4@U{JH58!<drdu>^^HO66|+}3U5gYDjoaB@bJno%o;UiNTK+ZB9E&~#chJx1*9
ze*Gw-dW=&!P446ujPmZQWH&Gj_e;*T;m$*Bb`sT(pwXAu!kA?WR^i<6g@4BWap4Uf
zcnWzNHL?cB^0Z@~uqp|eLR~ixJC^D40ar_ZZ3`Lo6ugOa3US_C3{>n%@(=D0cxzA@
znJV~?%hL}IiNN&`VLi#?9xB3O$#bY2-J8V0jyUv4e*va!E*)|CE^gmAoT&)RY&Pd<
zelwKh6Iq>%%T1ZX_d!J$KvRIlGP?DZkWmO>8>Kcb+HcHtY(Xu<B*|OPG8Ty|6uFo|
zI{VfIyU<!#kyu9B>rZSKN1f#O%u($qv!V@FqLj}4Vuw;`H3?Iv94Yl%jt9D^Rl3Jr
zx$&DUtsHvlxeurB>~0GgTNI|I3R}v5LL#^GIdxTSXCCcmS#1;o$U1Y$b)C7Nlu}N|
zcSYKD$;@tT%sa+3$yhrn1KDI(Yx_#BY(aX~wOSyVoBcc%10PJXt$d5*N$16I2*&4X
ZK?CwIx&xG3pi<!`--r_ZBCzfZ`#-Y-+;so|

literal 4475
zcmV->5rpnjPC-x#FfK7O3RY!ub7^mGIv_GGGA=MJGdMUcXE<dzH)LaGGzv*>b97;D
zV`VxZHDzKqVmUQrEipD@GA%S>HexMdGdVdeV=*-`WMN@9Ha9b53RXjGZ)0mZAbWiZ
z3e~y`y3G*+0M73H2F|4b00000D77#B08q_`0LqncAx038*f{_U01N<p8IUZ>L;9HZ
zjmE{UXYp(U&<y~GzLY7c$&_SdOG(Nka_XNR@m~x-ALp&GF}crdu6N0bs@!iW`QNky
z7Xk_b^#L7xvmFN>L|KfBvUs;a2N;{-0OK+nV4P}T<{UF9bB-8jaE|9lCIi|WCk$qD
zE+CU@y7F~#&KM_A6mx+nh8ZKK7KqV1Fkxo93M|lB#O=O-p?P_!Ad6kTWKmA`;^HNX
zTe@VC0xnt1Vyb0}C}yER6tQR##q>&+|E*y8zv;^VDU%i2*91*fm_pmXzj5s5`RCB~
z|9$>`QJFx3D)&Y}<z5IR;CCqkew85Lm){4L?sY)v-YE~<n>OG;&F^SaYJN9spyoqd
z&F_+Ev@fLD?2CaUr=}#QrY|W-G_NQO%)AzmLUQmszbsh38+MVL*Y4)n{J!f2C5jCd
zSipuyP;6*JBL!>;9NBCL7};zGB;XA|!21FY@cxMb7EEOYSTHSmV!<m5rcPwEE~27Y
zR}&{$y4$XF4H0<)ns`6JkoQ6a?4SYm7C>n40D^7}NPt`W3E<XljS0~0w&dq_Q^MqS
z^I7735+&Xv@$vphlHQFV>AlF2-ia6~O8|<L<q#reX;UEp(1HvJHwUlZngJ0a+N&i0
ziS|o>1hI9yb4}L+q|DvS%HRhl_M;~To3eGAf4^S~|8d{`y%w4`WUg?*)f;j!?=!1f
z$$olzVz8+*Z_12m0>j0MuHMZz7R*Za(-VVD)mUuv?E6Kt*wmgFY${Lfh53`RrH)GG
zQ(>?se9}CzQsPLMDpoWx@goLH6aRDJ#sBFRUD&S$7xrhdh5hzpp#{y(6<N&eSb+uV
z=Y+THT)h4JP;aM-E4-(|3h$?=!lzPDLF<ZQidh#FQe=Jm<?Fudi72{v0*daNc%u96
zdrm-kUK3EBi5`@P@=Nf%SAq%elUTxgB$V*}h$Or>0tufMaRluS9wTOVh$sTgp^wwS
z9DZN+cZR@|y%0oXAH)#ZXAguB*Z&a2^*#V`{q;TkVEva5*5B#Ddh5Nuc(-$h_c~s@
z^G?T(HhHL{%^m1yQ=45pSW|}`tU0I4n%0z~L+86Wou_g-?>x?~<XmaV;lwF<$+x()
z;BddQws?Ny{7$E}b6I<1>GsCuYWoVSwsXMMc5T18`Z(n9{|#^cw*iM<!wtRDU_<XS
z)S%taptTG$Xx+vQSz5_Z)rKs|l8qILuBk$)*r2GIHUGyzmBOG#U!^a^u(b;?Y~8{O
zTW`&R3oEoR+o`|;kJLC)?^0NShJp$_GCd&$9+{ee0<8qz39~5^c$~!T(v-lXp_$^5
zXLxCNBnfDEB$<wIhPO!(NZ8~iLl{AmAc&yJTL_xeqzwd;l)8~*8f7D|$P|^bPiK^U
z`lx-fPyY0|f1jbe&pw-d%p7XW+Z^+lOK!t@+b~($unj-y6KC?o`02za9y7)w#`umH
zr=f^3crmVpIJMxtAJp#s?7bd*K-&d7f4W*!<OcoVR3azehTpC>A=>t8&TAU9T?|?<
z_q+W*CuMgoas7_>@0?}c@3GwMdqQ3RZPlSBZO|{P3JrZ*b}s5htHs>kCkMY!-z;+N
zJnpM&^XY030Z})j-sZE=kE;R=VA>Mrzsa#I$hFS2{eE{8&s|w<p1;+agCCswBZTMb
zn$`N;pU+(P)E3Rzw#7}^E{Pj=?%(UuP@REE{qmMMTV4$MFRS|0p6%V{t~Sq~+g{_s
zyT|PJsdY`;#|razL2G)gTiT74W=#$AXP&%|T4-Qbb4-F(*S7Az`i5V4d%xlhKW*C?
z{p!?Odu?Pv0{jHP1n9)KB|l1-_$;|3@82g;LZgGkXGZ5pl5E@Aq+<jL&8x_fnHLcw
zyIB%#RvWzlC_<|<gvd-t?bRN>$+39Km}_61AVcsCh!A}H5d`1xtp^A#R(rKKtF_l-
zv%zYw_Go!{R<fVki;7D7fYn}9%10jBZjU}@J3aWYQ8*j5%VQ6Y;-Lpe@5qCr9JK?F
zjMi~SM(MC4qZyr}j(*BPM?d43qo4hRLyq%&BaZWQ1CBFo;|*FD8*a?H)@XxwUX5p$
z8f^5R#v1*jp+^7xpOMD@W}xwZ8E5>Tl3~XGW0diKDh3%>X&7VR4~7`{e-Q@$_<I4y
z|6P0$PR{z7dVH))UxG?9kGO0Tl|s4#k+GlJ%f+7RK1Hoqt&%C#UY<O8ny5Gkc74BS
z-lx|1BL=HgG9|HJ^Yppu<bqk7w{z8=@M_+%z^rShsjIOn8B*=-T!uDRYmLe%V$<ez
zlNT)@6ftIQlnu;BGQC7y6r(;QS?r^j7K2-8`t>K)_xBug6;>rfvQ1>Z;3^k)meQ|3
zrAU$DH6CiMk{NxVzZ!Z#6?ULiIdIybi<!Em%oYtQQwB^JERk7qTDpKr7gHJoQxeld
zwMHpSk_Aa%k}5#@g5(9M%Mq8OEozP^Fw$a_MU4>_*%d08m7*r9A||2<h^i1#!QSE~
z5)4H!1i{dfkdn{?LJq`YK~pM~8X{^?13@&P8BkKKKS)0ez^K&VxhGWKrw+}%{bRv=
zYR^hmBqih^*7rDjj@xRjD)8G?9feDC5RMJa^V@uD^DPX1Tl&nkZ@1=Fs51uL{GDz8
zesl06#cx-`1+0k`-f#Qecl>;g!&N1xIzh=%-EYBt_Wy0wE()v}$K3y?Rks|SSXAGi
z8vL{>lM$M`xs<?obw+bHx2(3H*KK_tyiYaGbua(h_Av|1-TZP@34yA9{h4$C{->?#
z*PmamA{oF2+^2|sx_VT=`LAaK?o<47)#!;8`ejv_tXngeaaK|{42EW?1Ei-KLL(yp
z000mO08<-C5LavFk_;07fI-56v9d8d7K;Qi44CUN%hUi6fPe)FiU6g6f<Kxq`pgr3
zh&)6eq7RXW$V1$GBq@>ZfRuF2g7V}|o1*<23+CtwWu!{5^Rx=BZ30}>glkX<G$guG
z!;i~zql&S?&@z-d_)Zt$AY~fuvKXbL3r`5)jP=nsxDi;`K)_LE+zr7S+y)Uh2n=N_
z7K)g)>-z%=23-#H(K5|OtV&h{EVErp?uMwM$W!qgE_FRJ^m|v_FE2XdtwM$6W+9DP
zM%jbY``wKG-K9JW@C$_0v~`iew{hDnI!8aN%GnPhpHSuK2hmR`EbvO1Sk|Y7@TN-n
zr&2S|iaS(O6tXPr|4_E<%5BkuFtV)92vJL+N-&6H(Fm2#F>oKDs&X`kQT&8gm7|^2
zaQ^Uy0_)`S4ycLr!^kJvF&baVWDtk~vEF|SJzxNss)?=VA5=tsaJJR&qdXPGwm@R)
zRLFf)idygDn*?U=ImxqCP!!gr-i1_BH>DM4^y2+6j@MS1(<q41uCci7;&3y2MNTkY
zk2~g~Rj8PQCE7W4F)o0Pf}L7GVxgmaHgI$V??mb%IWOepn$iIL<YFaomws6&x8Y+}
zYVxp(?9s#6WF(_lMGe_Egm!&G%qp;)f^FY3M2vkTL}N%7A^(DMj97ZBTX#%r)q_(7
zbnu|#r|HGo^@@MG(FnF<ihr$S<5PtVcGQWbxr=Pt<fp^aVvk(1w29Bdtwpx;-OAi4
zH^vp>oa5CCKTP{i7n1GT2^HAR?0alBgJiU|is~FZ>}P#cbH-tZ-VVEpH<6B6ZmHly
zwPL+mV)~$#lh}|Cr<3?jk6>XAxaKXjQy*~`L4Ab+Tw<5O_JD&TrW2l%b^;ud)nzeU
z;4IvTLg=?W{j(XuyhEJ%6W5QHS0{|cp(Vu6ONhn41gDx)1~eMRBD<c@Vo(-}9%CsS
zYg4;XJk&CxsXgK|RMQ%79Khv|=!%a)$0K-y7@6kKAqJ^M#}xawyBsDeft`8eMF@Ga
z@07PFAx?H26{U(5KNPMLR(@$uIXb{s*bpasj-OK1GiB+M2o1~kyoxuY$kTY-qc-nK
z74_bQ7jLKrHRBB#CN|07Nj#r{u||x9s;eQ?8Eo$67aPF7YcH{C0Kwb^Pp<$AO>`;d
z&n>K|s+T&=sT-uU3R;O?e@YJd<hQx$IY$+&t*1n!oT7cBci<A@n7vP>PsWdjLSv!}
z=z#=yshvQ6NfbFQ<VGqqltIs{4E>7qbK=xBFOuXLGUEjm7NFr^Fe40W?K5K&?U7w6
z0EDFHa5Wnx!QstW-2$Q(7~a_^2o6-vYT`bWHlo%B=&s{LUI3Ym#W6`%17sN&Mueg3
z!4UrhHhh1|^dW@okEllPKtM5EFXy4Cp*6c}HGsqkfYh$q!c5lgNvFip)sgr|m~1a;
zMo(biwy0L@L)lYn*Suvg2pLA0dpJ$TPGI1+h%4izRu4KOJJo?x;~j>($Dlr-!{P!Y
zFMn8^a7$izS8asp)4Jk==X~2eN+n*x3uA#15C^cRhVdim9BvX3t1dnx!UCyPV$8BX
zvsYpR2zoqzXQLoEP&uoK`%v15+OSKM07xw>YHcj^xK11ac=T_-M3?pgvmb}y9(;7M
z{F9*BGXKH?&;i>_BV+8*jy(NQp^VsQA*>wcI0QQwOK*e*#$ogLV-VcnczPo+Fo2rJ
z9E0EvN7EaDfdSM!<`@KbIGWxF3=E*=F~=ad!_o9cU|;|>k2wax9ge0q0s{l6dCV~g
z?r=1{5f~Ui&0{i`236vWZHv5Y1_q!Aq-sZHlb>s(bJ1yQWO|Jb@gV5X@6ru};6UZ9
zA~70GGeeTc(ZKs7z)jYB$RHOs_O3>rtJv)xI3vJySqI;^p9=_i*fz*zLNuB^Ln6SV
z9eG-B#Pe)G2K%fAbxWaO1RIaj(I^WH4aRFS>)R_>yCig5I|6_+W6HfNvJoj!Nin!J
zpU<O?Z%7(&4?Fu$aiKMA{KQo=wH_8}I|JY!+5avXI+CK}hleXIx<4T)wGo=L5<wO=
ze0}UD4B$hKw{8-u3TG!1C4s(2{Mt7IGYC+aAkVMPob#_3?)b3&gd|QG)Ry&<qBy1R
z$$i~;@<V(HWGuG9FwZsPlMm2OBtx^$j`^n;xBPH^0uh@fb<82fc=reMCkWA5DCe9j
zhPytfKS7MnLOJJ*Su<%5I1pEa-4A^9WaY0-`MSS#GtzKj0g3g<tfbUH*vO?#^0gVj
zTm54Q-!XQsO!0o;p(oBy#jCjvnX1)54nPVM<oVT^^YKF~gE0nTX6NLrV*PD1?JsUH
zL`Jfm^I`Dl$>^zhHMV$NeU@#J*jB_7P=*FG2D5Tmz-?Et5d;1lGeYF-yZ~wTJTl~3
zl4I6-1xjS`gTZCP0>#rt{NqC^{7A}rhtrwKtEOV#hfv!P<G}@vLOtx}I_PA5Yv?sv
zZ$31nHbi#90Z#G}Hm3@rB`arxOF38f=4aPeE%|G`8OQ-hFp(TzJv-*!IkUolaG|Im
zkAI(dMRN0E^yg}p8PCYS>JcpuVMsxs13~_9egyn>?m;TEAwYjej`?U$oF}AJV`1h(
zfSLTiL;24+#4`k*V8^Nf)cj4V=Sc;_L(R=jJmN#3R4EtCKkgWYFf3$Q<L9_$aDo}&
zEJ@f6xpYo^2B-BTJi=aiuRL8*#a3S`&nU7`g*i8kea;b3#`idOyAPj#M!8Tlz<*hZ
z`M?I1^%%vXvk!a}MDNR(YyhXf!ia5jmdp~43}|VCQHT-dGX@gsAg%$d;)HoX0f~KB
zG6+Rmgu*1*Ntpg(k<M_-Y{oWhAxP<NY_y8sVn+k>?Z^;u@fTbGf+p0egF}x~JbOq`
N%5QO4t`F4^t?kB;gJb{z

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..a27240815 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,72 @@ 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 _priv = box.space[box.schema.PRIV_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", "GREATER", "LESSER",
+        "_sql_stat_get", "_sql_stat_push", "_sql_stat_init",
+    }
+    for _, v in pairs(sql_builtin_list) do
+        local t = _func:auto_increment({ADMIN, v, 1, 'SQL_BUILTIN', '',
+                                       'function', {}, 'any', 'none', 'none',
+                                        false, false, true, {}, setmap({}), '',
+                                        datetime, datetime})
+        _priv:replace{ADMIN, PUBLIC, 'function', t.id, box.priv.X}
+    end
+    local t = _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})
+    _priv:replace{ADMIN, PUBLIC, 'function', t.id, box.priv.X}
+    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 21ce78c24..d59aba9ee 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 "box/session.h"
 #include <unicode/ustring.h>
@@ -1824,3 +1825,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..d9b9c6382 160000
--- a/test-run
+++ b/test-run
@@ -1 +1 @@
-Subproject commit 37d15bd781ddfb41dfd75d9b761c180395b4b53f
+Subproject commit d9b9c6382453dfb4d4663909ae4dc3a535826889
diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result
index b20dc41e5..2ef2200bf 100644
--- a/test/box-py/bootstrap.result
+++ b/test/box-py/bootstrap.result
@@ -53,7 +53,14 @@ 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': 'param_list',
+        'type': 'array'}, {'name': 'returns', 'type': 'string'}, {'name': 'aggregate',
+        'type': 'string'}, {'name': 'sql_data_access', 'type': 'string'}, {'name': 'is_deterministic',
+        'type': 'boolean'}, {'name': 'is_sandboxed', 'type': 'boolean'}, {'name': 'is_null_call',
+        'type': 'boolean'}, {'name': 'exports', 'type': 'array'}, {'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'}]]
@@ -113,7 +120,7 @@ box.space._index:select{}
   - [289, 2, 'name', 'tree', {'unique': true}, [[0, 'unsigned'], [2, 'string']]]
   - [296, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
   - [296, 1, 'owner', 'tree', {'unique': false}, [[1, 'unsigned']]]
-  - [296, 2, 'name', 'tree', {'unique': true}, [[2, 'string']]]
+  - [296, 2, 'name', 'tree', {'unique': true}, [{'field': 2, 'collation': 2, 'type': 'string'}]]
   - [297, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
   - [297, 1, 'owner', 'tree', {'unique': false}, [[1, 'unsigned']]]
   - [297, 2, 'name', 'tree', {'unique': true}, [[2, 'string']]]
@@ -150,9 +157,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({{"=", 18, ""}, {"=", 19, ""}})) return r end
 ---
-- - [1, 1, 'box.schema.user.info', 1, 'LUA']
+- - [1, 1, 'box.schema.user.info', 1, 'LUA', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, ['LUA'], {}, '', '', '']
 ...
 box.space._priv:select{}
 ---
@@ -160,6 +168,66 @@ box.space._priv:select{}
   - [1, 0, 'universe', 0, 24]
   - [1, 1, 'universe', 0, 4294967295]
   - [1, 2, 'function', 1, 4]
+  - [1, 2, 'function', 2, 4]
+  - [1, 2, 'function', 3, 4]
+  - [1, 2, 'function', 4, 4]
+  - [1, 2, 'function', 5, 4]
+  - [1, 2, 'function', 6, 4]
+  - [1, 2, 'function', 7, 4]
+  - [1, 2, 'function', 8, 4]
+  - [1, 2, 'function', 9, 4]
+  - [1, 2, 'function', 10, 4]
+  - [1, 2, 'function', 11, 4]
+  - [1, 2, 'function', 12, 4]
+  - [1, 2, 'function', 13, 4]
+  - [1, 2, 'function', 14, 4]
+  - [1, 2, 'function', 15, 4]
+  - [1, 2, 'function', 16, 4]
+  - [1, 2, 'function', 17, 4]
+  - [1, 2, 'function', 18, 4]
+  - [1, 2, 'function', 19, 4]
+  - [1, 2, 'function', 20, 4]
+  - [1, 2, 'function', 21, 4]
+  - [1, 2, 'function', 22, 4]
+  - [1, 2, 'function', 23, 4]
+  - [1, 2, 'function', 24, 4]
+  - [1, 2, 'function', 25, 4]
+  - [1, 2, 'function', 26, 4]
+  - [1, 2, 'function', 27, 4]
+  - [1, 2, 'function', 28, 4]
+  - [1, 2, 'function', 29, 4]
+  - [1, 2, 'function', 30, 4]
+  - [1, 2, 'function', 31, 4]
+  - [1, 2, 'function', 32, 4]
+  - [1, 2, 'function', 33, 4]
+  - [1, 2, 'function', 34, 4]
+  - [1, 2, 'function', 35, 4]
+  - [1, 2, 'function', 36, 4]
+  - [1, 2, 'function', 37, 4]
+  - [1, 2, 'function', 38, 4]
+  - [1, 2, 'function', 39, 4]
+  - [1, 2, 'function', 40, 4]
+  - [1, 2, 'function', 41, 4]
+  - [1, 2, 'function', 42, 4]
+  - [1, 2, 'function', 43, 4]
+  - [1, 2, 'function', 44, 4]
+  - [1, 2, 'function', 45, 4]
+  - [1, 2, 'function', 46, 4]
+  - [1, 2, 'function', 47, 4]
+  - [1, 2, 'function', 48, 4]
+  - [1, 2, 'function', 49, 4]
+  - [1, 2, 'function', 50, 4]
+  - [1, 2, 'function', 51, 4]
+  - [1, 2, 'function', 52, 4]
+  - [1, 2, 'function', 53, 4]
+  - [1, 2, 'function', 54, 4]
+  - [1, 2, 'function', 55, 4]
+  - [1, 2, 'function', 56, 4]
+  - [1, 2, 'function', 57, 4]
+  - [1, 2, 'function', 58, 4]
+  - [1, 2, 'function', 59, 4]
+  - [1, 2, 'function', 60, 4]
+  - [1, 2, 'function', 61, 4]
   - [1, 2, 'space', 276, 2]
   - [1, 2, 'space', 277, 1]
   - [1, 2, 'space', 281, 1]
diff --git a/test/box-py/bootstrap.test.py b/test/box-py/bootstrap.test.py
index 4f2f55a7c..63c13e8a4 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({{"=", 18, ""}, {"=", 19, ""}})) return r end')
 server.admin('box.space._priv:select{}')
 
 # Cleanup
diff --git a/test/box/access.result b/test/box/access.result
index ca2531f0e..ecb85a563 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -691,7 +691,7 @@ box.schema.func.exists(1)
 ---
 - true
 ...
-box.schema.func.exists(2)
+box.schema.func.exists(62)
 ---
 - false
 ...
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index c1ba00211..1341cf67b 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -276,7 +276,7 @@ box.schema.user.exists{}
 box.schema.func.exists('nosuchfunc')
 box.schema.func.exists('guest')
 box.schema.func.exists(1)
-box.schema.func.exists(2)
+box.schema.func.exists(62)
 box.schema.func.exists('box.schema.user.info')
 box.schema.func.exists()
 box.schema.func.exists(nil)
diff --git a/test/box/access_bin.result b/test/box/access_bin.result
index df8ef8dee..395afbcf3 100644
--- a/test/box/access_bin.result
+++ b/test/box/access_bin.result
@@ -299,7 +299,7 @@ box.schema.user.grant('guest', 'execute', 'universe')
 function f1() return box.space._func:get(1)[4] end
 ---
 ...
-function f2() return box.space._func:get(2)[4] end
+function f2() return box.space._func:get(62)[4] end
 ---
 ...
 box.schema.func.create('f1')
diff --git a/test/box/access_bin.test.lua b/test/box/access_bin.test.lua
index e77d8c0a8..394cc49be 100644
--- a/test/box/access_bin.test.lua
+++ b/test/box/access_bin.test.lua
@@ -113,7 +113,7 @@ test:drop()
 -- notice that guest can execute stuff, but can't read space _func
 box.schema.user.grant('guest', 'execute', 'universe')
 function f1() return box.space._func:get(1)[4] end
-function f2() return box.space._func:get(2)[4] end
+function f2() return box.space._func:get(62)[4] end
 box.schema.func.create('f1')
 box.schema.func.create('f2',{setuid=true})
 c = net.connect(box.cfg.listen)
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 53d366106..be793708b 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -793,7 +793,14 @@ 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': 'param_list',
+        'type': 'array'}, {'name': 'returns', 'type': 'string'}, {'name': 'aggregate',
+        'type': 'string'}, {'name': 'sql_data_access', 'type': 'string'}, {'name': 'is_deterministic',
+        'type': 'boolean'}, {'name': 'is_sandboxed', 'type': 'boolean'}, {'name': 'is_null_call',
+        'type': 'boolean'}, {'name': 'exports', 'type': 'array'}, {'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 +836,129 @@ 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', [], 'any', 'none', 'none',
+    false, false, true, ['LUA'], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [2, 1, 'TRIM', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [3, 1, 'TYPEOF', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [4, 1, 'PRINTF', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [5, 1, 'UNICODE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [6, 1, 'CHAR', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [7, 1, 'HEX', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [8, 1, 'VERSION', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [9, 1, 'QUOTE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [10, 1, 'REPLACE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [11, 1, 'SUBSTR', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [12, 1, 'GROUP_CONCAT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [13, 1, 'JULIANDAY', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [14, 1, 'DATE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [15, 1, 'TIME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [16, 1, 'DATETIME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [17, 1, 'STRFTIME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [18, 1, 'CURRENT_TIME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [19, 1, 'CURRENT_TIMESTAMP', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
+    'none', false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [20, 1, 'CURRENT_DATE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [21, 1, 'LENGTH', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [22, 1, 'POSITION', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [23, 1, 'ROUND', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [24, 1, 'UPPER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [25, 1, 'LOWER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [26, 1, 'IFNULL', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [27, 1, 'RANDOM', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [28, 1, 'CEIL', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [29, 1, 'CEILING', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [30, 1, 'CHARACTER_LENGTH', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
+    'none', false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [31, 1, 'CHAR_LENGTH', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [32, 1, 'FLOOR', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [33, 1, 'MOD', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [34, 1, 'OCTET_LENGTH', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [35, 1, 'ROW_COUNT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [36, 1, 'COUNT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [37, 1, 'LIKE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [38, 1, 'ABS', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [39, 1, 'EXP', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [40, 1, 'LN', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [41, 1, 'POWER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [42, 1, 'SQRT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [43, 1, 'SUM', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [44, 1, 'TOTAL', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [45, 1, 'AVG', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [46, 1, 'RANDOMBLOB', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [47, 1, 'NULLIF', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [48, 1, 'ZEROBLOB', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [49, 1, 'MIN', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [50, 1, 'MAX', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [51, 1, 'COALESCE', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [52, 1, 'EVERY', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [53, 1, 'EXISTS', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [54, 1, 'EXTRACT', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [55, 1, 'SOME', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none', false,
+    false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [56, 1, 'GREATER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [57, 1, 'LESSER', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none', 'none',
+    false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [58, 1, '_sql_stat_get', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
+    'none', false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [59, 1, '_sql_stat_push', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
+    'none', false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [60, 1, '_sql_stat_init', 1, 'SQL_BUILTIN', '', 'function', [], 'any', 'none',
+    'none', false, false, true, [], {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
+  - [61, 1, 'LUA', 1, 'LUA', 'function(code) return assert(loadstring(code))() end',
+    'function', ['string'], 'any', 'none', 'none', false, false, true, ['LUA', 'SQL'],
+    {}, '', '2019-07-10 13:38:07', '2019-07-10 13:38:07']
 ...
 session = nil
 ---
diff --git a/test/box/access_sysview.result b/test/box/access_sysview.result
index 69eb6d191..752f0102a 100644
--- a/test/box/access_sysview.result
+++ b/test/box/access_sysview.result
@@ -258,11 +258,11 @@ box.session.su('guest')
 ...
 #box.space._vpriv:select{}
 ---
-- 16
+- 76
 ...
 #box.space._vfunc:select{}
 ---
-- 1
+- 61
 ...
 #box.space._vcollation:select{}
 ---
@@ -290,11 +290,11 @@ box.session.su('guest')
 ...
 #box.space._vpriv:select{}
 ---
-- 16
+- 76
 ...
 #box.space._vfunc:select{}
 ---
-- 1
+- 61
 ...
 #box.space._vsequence:select{}
 ---
diff --git a/test/box/alter.result b/test/box/alter.result
index eb59f05fb..a6db011ff 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -190,7 +190,7 @@ _index:select{}
   - [289, 2, 'name', 'tree', {'unique': true}, [[0, 'unsigned'], [2, 'string']]]
   - [296, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
   - [296, 1, 'owner', 'tree', {'unique': false}, [[1, 'unsigned']]]
-  - [296, 2, 'name', 'tree', {'unique': true}, [[2, 'string']]]
+  - [296, 2, 'name', 'tree', {'unique': true}, [{'field': 2, 'collation': 2, 'type': 'string'}]]
   - [297, 0, 'primary', 'tree', {'unique': true}, [[0, 'unsigned']]]
   - [297, 1, 'owner', 'tree', {'unique': false}, [[1, 'unsigned']]]
   - [297, 2, 'name', 'tree', {'unique': true}, [[2, 'string']]]
diff --git a/test/box/function1.result b/test/box/function1.result
index ec1ab5e6b..c434b0067 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: 62
   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: 62
   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: 62
   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", "GREATER", "LESSER", "_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..dbbdcf8be 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", "GREATER", "LESSER", "_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..07efc6ab1 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 ''func31940'': function id is too big'
 ...
 drop_limit_func()
 ---
-- error: Function 'func32000' does not exist
+- error: Function 'func31940' 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 ''func31940'': function id is too big'
 ...
 drop_limit_func()
 ---
-- error: Function 'func32000' does not exist
+- error: Function 'func31940' does not exist
 ...
 session.su('admin')
 ---
-- 
2.21.0

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

* [tarantool-patches] Re: [PATCH v2 01/12] sql: get rid of SOUNDEX, MATCH
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 01/12] sql: get rid of SOUNDEX, MATCH Kirill Shcherbatov
@ 2019-07-10 18:45   ` Konstantin Osipov
  2019-07-12  8:44   ` Kirill Yukhin
  1 sibling, 0 replies; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 18:45 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> In relation with FuncDef cache rework we need to clean-up
> builtins list. The SOUNDEX function is not in use while MATCH
> fucntion is a stub that raises an error, so they could be
> dropped.
> 
> Needed for #4182

I wonder why is SOUNDEX function disabled in Tarantool?

sqlite makes it optional because it uses a static integer table,
and sqlite is very concerned with memory footprint.

We can simply always enable it, rather than remove.

The rest of the patch is LGTM.


-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 10/12] sql: refactor builtins signatures with port
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 10/12] sql: refactor builtins signatures with port Kirill Shcherbatov
@ 2019-07-10 18:47   ` Konstantin Osipov
  2019-07-11  7:33     ` Kirill Shcherbatov
  0 siblings, 1 reply; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 18:47 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> Reworked SQL builtins signatures to use args and ret abstract
> ports to pass arguments and return the execution result.
> The new approach allows to support calling sql builtins functions
> from Lua in future and to use uniform func_call API in
> OP_Function opcode.

I don't know why you ignored my comment on telegram: I think this
is a non-goal and SQL functions don't have to use _func api for
calls. It is a performance issue as well, since obstructs possible JITting
of expressions. Did you discuss this with Nikita as I asked you on
telegram?

> 
> Needed for #4113, #2200, #2233
> ---
>  src/box/call.c        |    1 +
>  src/box/execute.c     |    2 +
>  src/box/lua/call.c    |    2 +
>  src/box/lua/lua_sql.c |   41 +-
>  src/box/port.c        |    2 +
>  src/box/port.h        |   19 +
>  src/box/sql/analyze.c |  130 ++--
>  src/box/sql/func.c    | 1400 +++++++++++++++++++++++++----------------
>  src/box/sql/main.c    |   58 +-
>  src/box/sql/printf.c  |    7 +-
>  src/box/sql/sqlInt.h  |  101 +--
>  src/box/sql/vdbe.c    |  101 ++-
>  src/box/sql/vdbeInt.h |   18 +-
>  src/box/sql/vdbeapi.c |  163 +----
>  src/box/sql/vdbemem.c |   51 +-
>  src/lib/core/port.h   |   31 +
>  16 files changed, 1197 insertions(+), 930 deletions(-)
> 
> diff --git a/src/box/call.c b/src/box/call.c
> index ac2bf3004..b459a9839 100644
> --- a/src/box/call.c
> +++ b/src/box/call.c
> @@ -73,6 +73,7 @@ static const struct port_vtab port_msgpack_vtab = {
>  	.dump_lua = port_msgpack_dump_lua,
>  	.dump_plain = NULL,
>  	.get_msgpack = port_msgpack_get_msgpack,
> +	.get_vdbemem = NULL,
>  	.destroy = NULL,
>  };
>  
> diff --git a/src/box/execute.c b/src/box/execute.c
> index a9ca2e67a..0dc1bc46d 100644
> --- a/src/box/execute.c
> +++ b/src/box/execute.c
> @@ -109,6 +109,8 @@ const struct port_vtab port_sql_vtab = {
>  	/* .dump_lua = */ port_sql_dump_lua,
>  	/* .dump_plain = */ NULL,
>  	/* .get_msgpack = */ NULL,
> +	/* .get_vdbemem = */ NULL,
> +	/* .get_context = */ NULL,
>  	/* .destroy = */ port_sql_destroy,
>  };
>  
> diff --git a/src/box/lua/call.c b/src/box/lua/call.c
> index 95fac4834..4297218cd 100644
> --- a/src/box/lua/call.c
> +++ b/src/box/lua/call.c
> @@ -507,6 +507,8 @@ static const struct port_vtab port_lua_vtab = {
>  	.dump_lua = port_lua_dump_lua,
>  	.dump_plain = port_lua_dump_plain,
>  	.get_msgpack = port_lua_get_msgpack,
> +	.get_vdbemem = NULL,
> +	.get_context = NULL,
>  	.destroy = port_lua_destroy,
>  };
>  
> diff --git a/src/box/lua/lua_sql.c b/src/box/lua/lua_sql.c
> index 4d90a53de..0c5797fa2 100644
> --- a/src/box/lua/lua_sql.c
> +++ b/src/box/lua/lua_sql.c
> @@ -34,6 +34,7 @@
>  
>  #include "box/lua/call.h"
>  #include "box/sql/sqlInt.h"
> +#include "box/port.h"
>  #include "box/sql/vdbeInt.h"
>  
>  struct lua_sql_func_info {
> @@ -47,15 +48,28 @@ struct lua_sql_func_info {
>   * Lua func should be previously registered in sql
>   * (see lbox_sql_create_function).
>   */
> -static void
> -lua_sql_call(sql_context *pCtx, int nVal, sql_value **apVal) {
> +static int
> +lua_sql_call(struct func *func, struct port *args, struct port *ret)
> +{
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	lua_State *L = lua_newthread(tarantool_L);
>  	int coro_ref = luaL_ref(tarantool_L, LUA_REGISTRYINDEX);
> -	struct lua_sql_func_info *func_info = sql_user_data(pCtx);
> +	struct lua_sql_func_info *func_info = sql_user_data(ctx);
>  
>  	lua_rawgeti(L, LUA_REGISTRYINDEX, func_info->func_ref);
> -	for (int i = 0; i < nVal; i++) {
> -		sql_value *param = apVal[i];
> +	for (uint32_t i = 0; i < argc; i++) {
> +		sql_value *param = (sql_value *)&argv[i];
>  		switch (sql_value_type(param)) {
>  		case MP_INT:
>  			luaL_pushint64(L, sql_value_int64(param));
> @@ -79,38 +93,37 @@ lua_sql_call(sql_context *pCtx, int nVal, sql_value **apVal) {
>  		default:
>  			diag_set(ClientError, ER_SQL_EXECUTE, "Unsupported "\
>  				 "type passed to Lua");
> -			pCtx->is_aborted = true;
>  			goto error;
>  		}
>  	}
>  	if (lua_pcall(L, lua_gettop(L) - 1, 1, 0) != 0){
>  		diag_set(ClientError, ER_SQL_EXECUTE, lua_tostring(L, -1));
> -		pCtx->is_aborted = true;
>  		goto error;
>  	}
>  	switch(lua_type(L, -1)) {
>  	case LUA_TBOOLEAN:
> -		sql_result_bool(pCtx, lua_toboolean(L, -1));
> +		mem_set_bool(val, lua_toboolean(L, -1));
>  		break;
>  	case LUA_TNUMBER:
> -		sql_result_double(pCtx, lua_tonumber(L, -1));
> +		sqlVdbeMemSetDouble(val, lua_tonumber(L, -1));
>  		break;
>  	case LUA_TSTRING:
> -		sql_result_text(pCtx, lua_tostring(L, -1), -1,
> -				    SQL_TRANSIENT);
> +		 if (sqlVdbeMemSetStr(val, lua_tostring(L, -1), -1,
> +					1, SQL_TRANSIENT) != 0)
> +			return -1;
>  		break;
>  	case LUA_TNIL:
> -		sql_result_null(pCtx);
> +		sqlVdbeMemSetNull(val);
>  		break;
>  	default:
>  		diag_set(ClientError, ER_SQL_EXECUTE, "Unsupported type "\
>  			 "passed from Lua");
> -		pCtx->is_aborted = true;
>  		goto error;
>  	}
> +	return 0;
>  error:
>  	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, coro_ref);
> -	return;
> +	return -1;
>  }
>  
>  static void
> diff --git a/src/box/port.c b/src/box/port.c
> index 7f552bcfe..9e4ab9453 100644
> --- a/src/box/port.c
> +++ b/src/box/port.c
> @@ -146,5 +146,7 @@ const struct port_vtab port_tuple_vtab = {
>  	.dump_lua = port_tuple_dump_lua,
>  	.dump_plain = NULL,
>  	.get_msgpack = NULL,
> +	.get_vdbemem = NULL,
> +	.get_context = NULL,
>  	.destroy = port_tuple_destroy,
>  };
> diff --git a/src/box/port.h b/src/box/port.h
> index a7f5d81bd..f14d61189 100644
> --- a/src/box/port.h
> +++ b/src/box/port.h
> @@ -95,6 +95,25 @@ static_assert(sizeof(struct port_msgpack) <= sizeof(struct port),
>  void
>  port_msgpack_create(struct port *port, const char *data, uint32_t data_sz);
>  
> +struct sql_value;
> +struct sql_context;
> +
> +/** Port implementation used with vdbe memory variables. */
> +struct port_vdbemem {
> +	const struct port_vtab *vtab;
> +	struct sql_context *ctx;
> +	struct sql_value *mem;
> +	uint32_t size;
> +};
> +
> +static_assert(sizeof(struct port_vdbemem) <= sizeof(struct port),
> +	      "sizeof(struct port_vdbemem) must be <= sizeof(struct port)");
> +
> +/** Initialize a port to dump data in sql vdbe memory. */
> +void
> +port_vdbemem_create(struct port *base, struct sql_value *mem, uint32_t size,
> +		    struct sql_context *ctx);
> +
>  /** Port for storing the result of a Lua CALL/EVAL. */
>  struct port_lua {
>  	const struct port_vtab *vtab;
> diff --git a/src/box/sql/analyze.c b/src/box/sql/analyze.c
> index 512904cc1..7764b48c5 100644
> --- a/src/box/sql/analyze.c
> +++ b/src/box/sql/analyze.c
> @@ -109,6 +109,7 @@
>  #include "box/index.h"
>  #include "box/key_def.h"
>  #include "box/schema.h"
> +#include "box/port.h"
>  #include "third_party/qsort_arg.h"
>  
>  #include "sqlInt.h"
> @@ -263,9 +264,19 @@ stat4Destructor(void *pOld)
>   * return value is BLOB, but it is really just a pointer to the Stat4Accum
>   * object.
>   */
> -static void
> -statInit(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_stat_init(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	Stat4Accum *p;
>  	int nCol;		/* Number of columns in index being sampled */
>  	int nKeyCol;		/* Number of key columns */
> @@ -275,8 +286,7 @@ statInit(sql_context * context, int argc, sql_value ** argv)
>  	int mxSample = SQL_STAT4_SAMPLES;
>  
>  	/* Decode the three function arguments */
> -	UNUSED_PARAMETER(argc);
> -	nCol = sql_value_int(argv[0]);
> +	nCol = sql_value_int(&argv[0]);
>  	assert(nCol > 0);
>  	/* Tarantool: we use an additional artificial column for the reason
>  	 * that Tarantool's indexes don't contain PK columns after key columns.
> @@ -284,7 +294,7 @@ statInit(sql_context * context, int argc, sql_value ** argv)
>  	 * identical rows, we have to use this artificial column.
>  	 */
>  	nColUp = sizeof(tRowcnt) < 8 ? (nCol + 2) & ~1 : nCol + 1;
> -	nKeyCol = sql_value_int(argv[1]);
> +	nKeyCol = sql_value_int(&argv[1]);
>  	assert(nKeyCol <= nCol);
>  	assert(nKeyCol > 0);
>  
> @@ -295,12 +305,10 @@ statInit(sql_context * context, int argc, sql_value ** argv)
>  	    + sizeof(tRowcnt) * nColUp	/* Stat4Accum.anLt */
>  	    + sizeof(Stat4Sample) * (nCol + 1 + mxSample)	/* Stat4Accum.aBest[], a[] */
>  	    + sizeof(tRowcnt) * 3 * nColUp * (nCol + 1 + mxSample);
> -	db = sql_context_db_handle(context);
> +	db = sql_get();
>  	p = sqlDbMallocZero(db, n);
> -	if (p == 0) {
> -		context->is_aborted = true;
> -		return;
> -	}
> +	if (p == NULL)
> +		return -1;
>  
>  	p->db = db;
>  	p->nRow = 0;
> @@ -316,12 +324,12 @@ statInit(sql_context * context, int argc, sql_value ** argv)
>  		p->iGet = -1;
>  		p->mxSample = mxSample;
>  		p->nPSample =
> -		    (tRowcnt) (sql_value_int64(argv[2]) /
> +		    (tRowcnt) (sql_value_int64(&argv[2]) /
>  			       (mxSample / 3 + 1) + 1);
>  		p->current.anLt = &p->current.anEq[nColUp];
>  		p->iPrn =
>  		    0x689e962d * (u32) nCol ^ 0xd0944565 *
> -		    (u32) sql_value_int(argv[2]);
> +		    (u32) sql_value_int(&argv[2]);
>  
>  		/* Set up the Stat4Accum.a[] and aBest[] arrays */
>  		p->a = (struct Stat4Sample *)&p->current.anLt[nColUp];
> @@ -347,7 +355,8 @@ statInit(sql_context * context, int argc, sql_value ** argv)
>  	 * (given by the 3rd parameter) is never used and can be any positive
>  	 * value.
>  	 */
> -	sql_result_blob(context, p, sizeof(*p), stat4Destructor);
> +	return sqlVdbeMemSetStr(val, (const char *)p, sizeof(*p), 0,
> +				stat4Destructor) != 0 ? -1 : 0;
>  }
>  
>  /*
> @@ -535,16 +544,21 @@ samplePushPrevious(Stat4Accum * p, int iChng)
>   *
>   * The R parameter is only used for STAT4
>   */
> -static void
> -statPush(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_stat_push(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	(void) ret;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +
>  	int i;
>  	/* The three function arguments */
> -	Stat4Accum *p = (Stat4Accum *) sql_value_blob(argv[0]);
> -	int iChng = sql_value_int(argv[1]);
> +	Stat4Accum *p = (Stat4Accum *) sql_value_blob(&argv[0]);
> +	int iChng = sql_value_int(&argv[1]);
>  
> -	UNUSED_PARAMETER(argc);
> -	UNUSED_PARAMETER(context);
>  	assert(p->nCol > 0);
>  	/* iChng == p->nCol means that the current and previous rows are identical */
>  	assert(iChng <= p->nCol);
> @@ -569,8 +583,8 @@ statPush(sql_context * context, int argc, sql_value ** argv)
>  		}
>  	}
>  	p->nRow++;
> -	sampleSetKey(p->db, &p->current, sql_value_bytes(argv[2]),
> -		     sql_value_blob(argv[2]));
> +	sampleSetKey(p->db, &p->current, sql_value_bytes(&argv[2]),
> +		     sql_value_blob(&argv[2]));
>  	p->current.iHash = p->iPrn = p->iPrn * 1103515245 + 12345;
>  	{
>  		tRowcnt nLt = p->current.anLt[p->nCol];
> @@ -592,6 +606,7 @@ statPush(sql_context * context, int argc, sql_value ** argv)
>  			}
>  		}
>  	}
> +	return 0;
>  }
>  
>  #define STAT_GET_STAT1 0	/* "stat" column of stat1 table */
> @@ -608,12 +623,22 @@ statPush(sql_context * context, int argc, sql_value ** argv)
>   * The content to returned is determined by the parameter J
>   * which is one of the STAT_GET_xxxx values defined above.
>   */
> -static void
> -statGet(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_stat_get(struct func *func, struct port *args, struct port *ret)
>  {
> -	Stat4Accum *p = (Stat4Accum *) sql_value_blob(argv[0]);
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
> +	struct Stat4Accum *p = (struct Stat4Accum *) sql_value_blob(&argv[0]);
>  	/* STAT4 have a parameter on this routine. */
> -	int eCall = sql_value_int(argv[1]);
> +	int eCall = sql_value_int(&argv[1]);
>  	assert(argc == 2);
>  	assert(eCall == STAT_GET_STAT1 || eCall == STAT_GET_NEQ
>  	       || eCall == STAT_GET_KEY || eCall == STAT_GET_NLT
> @@ -644,10 +669,8 @@ statGet(sql_context * context, int argc, sql_value ** argv)
>  		int i;
>  
>  		char *zRet = sqlMallocZero((p->nKeyCol + 1) * 25);
> -		if (zRet == 0) {
> -			context->is_aborted = true;
> -			return;
> -		}
> +		if (zRet == NULL)
> +			return -1;
>  
>  		sql_snprintf(24, zRet, "%llu", (u64) p->nRow);
>  		z = zRet + sqlStrlen30(zRet);
> @@ -659,8 +682,8 @@ statGet(sql_context * context, int argc, sql_value ** argv)
>  			assert(p->current.anEq[i]);
>  		}
>  		assert(z[0] == '\0' && z > zRet);
> -
> -		sql_result_text(context, zRet, -1, sql_free);
> +		if (sqlVdbeMemSetStr(val, zRet, -1, 1, SQL_DYNAMIC) != 0)
> +			return -1;
>  	} else if (eCall == STAT_GET_KEY) {
>  		if (p->iGet < 0) {
>  			samplePushPrevious(p, 0);
> @@ -668,8 +691,9 @@ statGet(sql_context * context, int argc, sql_value ** argv)
>  		}
>  		if (p->iGet < p->nSample) {
>  			Stat4Sample *pS = p->a + p->iGet;
> -			sql_result_blob(context, pS->aKey, pS->nKey,
> -					    SQL_TRANSIENT);
> +			if (sqlVdbeMemSetStr(val, (const char *)pS->aKey,
> +					     pS->nKey, 0, SQL_TRANSIENT) != 0)
> +				return -1;
>  		}
>  	} else {
>  		tRowcnt *aCnt = 0;
> @@ -687,12 +711,11 @@ statGet(sql_context * context, int argc, sql_value ** argv)
>  			p->iGet++;
>  			break;
>  		}
> -	}
> +		}
>  
> -	char *zRet = sqlMallocZero(p->nCol * 25);
> -	if (zRet == 0) {
> -		context->is_aborted = true;
> -	} else {
> +		char *zRet = sqlMallocZero(p->nCol * 25);
> +		if (zRet == NULL)
> +			return -1;
>  		int i;
>  		char *z = zRet;
>  		for (i = 0; i < p->nCol; i++) {
> @@ -701,17 +724,15 @@ statGet(sql_context * context, int argc, sql_value ** argv)
>  		}
>  		assert(z[0] == '\0' && z > zRet);
>  		z[-1] = '\0';
> -		sql_result_text(context, zRet, -1, sql_free);
> +		if (sqlVdbeMemSetStr(val, zRet, -1, 1, SQL_DYNAMIC) != 0)
> +			return -1;
>  	}
> -
> -}
> -#ifndef SQL_DEBUG
> -UNUSED_PARAMETER(argc);
> -#endif
> +	return 0;
>  }
>  
>  static void
> -callStatGet(Vdbe * v, int regStat4, int iParam, int regOut)
> +vdbe_emit_analyze_stat_get(struct Vdbe * v, int regStat4, int iParam,
> +			   int regOut)
>  {
>  	assert(regOut != regStat4 && regOut != regStat4 + 1);
>  	sqlVdbeAddOp2(v, OP_Integer, iParam, regStat4 + 1);
> @@ -964,7 +985,7 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
>  		sqlVdbeChangeP5(v, 3);
>  		sqlVdbeAddOp2(v, OP_Next, idx_cursor, next_row_addr);
>  		/* Add the entry to the stat1 table. */
> -		callStatGet(v, stat4_reg, STAT_GET_STAT1, stat1_reg);
> +		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_STAT1, stat1_reg);
>  		enum field_type types[4] = { FIELD_TYPE_STRING,
>  					     FIELD_TYPE_STRING,
>  					     FIELD_TYPE_STRING,
> @@ -982,12 +1003,12 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
>  		int sample_key_reg = col_reg + part_count;
>  		parse->nMem = MAX(parse->nMem, col_reg + part_count);
>  		int next_addr = sqlVdbeCurrentAddr(v);
> -		callStatGet(v, stat4_reg, STAT_GET_KEY, sample_key_reg);
> +		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_KEY, sample_key_reg);
>  		int is_null_addr = sqlVdbeAddOp1(v, OP_IsNull,
>  						     sample_key_reg);
> -		callStatGet(v, stat4_reg, STAT_GET_NEQ, eq_reg);
> -		callStatGet(v, stat4_reg, STAT_GET_NLT, lt_reg);
> -		callStatGet(v, stat4_reg, STAT_GET_NDLT, dlt_reg);
> +		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_NEQ, eq_reg);
> +		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_NLT, lt_reg);
> +		vdbe_emit_analyze_stat_get(v, stat4_reg, STAT_GET_NDLT, dlt_reg);
>  		sqlVdbeAddOp4Int(v, OP_NotFound, tab_cursor, next_addr,
>  				     sample_key_reg, 0);
>  		/*
> @@ -1752,9 +1773,12 @@ void
>  sql_register_analyze_builtins(void)
>  {
>  	static FuncDef funcs[] = {
> -		FUNCTION(_sql_stat_get, 2, 0, 0, statGet, FIELD_TYPE_ANY),
> -		FUNCTION(_sql_stat_push, 3, 0, 0, statPush, FIELD_TYPE_ANY),
> -		FUNCTION(_sql_stat_init, 3, 0, 0, statInit, FIELD_TYPE_ANY),
> +		FUNCTION(_sql_stat_get, 2, 0, 0, sql_builtin_stat_get,
> +			 FIELD_TYPE_ANY),
> +		FUNCTION(_sql_stat_push, 3, 0, 0, sql_builtin_stat_push,
> +			 FIELD_TYPE_ANY),
> +		FUNCTION(_sql_stat_init, 3, 0, 0, sql_builtin_stat_init,
> +			 FIELD_TYPE_ANY),
>  	};
>  	sqlInsertBuiltinFuncs(funcs, nelem(funcs));
>  }
> diff --git a/src/box/sql/func.c b/src/box/sql/func.c
> index d59aba9ee..18db8530d 100644
> --- a/src/box/sql/func.c
> +++ b/src/box/sql/func.c
> @@ -39,6 +39,7 @@
>  #include "version.h"
>  #include "coll/coll.h"
>  #include "box/func.h"
> +#include "box/port.h"
>  #include "tarantoolInt.h"
>  #include "box/session.h"
>  #include <unicode/ustring.h>
> @@ -71,45 +72,99 @@ sqlSkipAccumulatorLoad(sql_context * context)
>  	context->skipFlag = 1;
>  }
>  
> +static const struct port_vtab port_vdbemem_vtab;
> +
> +void
> +port_vdbemem_create(struct port *base, struct sql_value *mem, uint32_t size,
> +		    struct sql_context *ctx)
> +{
> +	struct port_vdbemem *port = (struct port_vdbemem *) base;
> +	port->vtab = &port_vdbemem_vtab;
> +	port->mem = mem;
> +	port->size = size;
> +	port->ctx = ctx;
> +}
> +
> +static struct sql_value *
> +port_vdbemem_get_vdbemem(struct port *base, uint32_t *size)
> +{
> +	struct port_vdbemem *port = (struct port_vdbemem *) base;
> +	assert(port->vtab == &port_vdbemem_vtab);
> +	*size = port->size;
> +	return port->mem;
> +}
> +
> +void *
> +port_vdbemem_get_context(struct port *base)
> +{
> +	struct port_vdbemem *port = (struct port_vdbemem *) base;
> +	return port->ctx;
> +}
> +
> +static const struct port_vtab port_vdbemem_vtab = {
> +	.dump_msgpack = NULL,
> +	.dump_msgpack_16 = NULL,
> +	.dump_lua = NULL,
> +	.dump_plain = NULL,
> +	.get_msgpack = NULL,
> +	.get_vdbemem = port_vdbemem_get_vdbemem,
> +	.get_context = port_vdbemem_get_context,
> +	.destroy = NULL,
> +};
> +
>  /*
>   * Implementation of the non-aggregate min() and max() functions
>   */
> -static void
> -minmaxFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_minmax(struct func *func, struct port *args, struct port *ret)
>  {
> -	int i;
> -	int mask;		/* 0 for min() or 0xffffffff for max() */
> -	int iBest;
> -	struct coll *pColl;
> -
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
>  	assert(argc > 1);
> -	mask = sql_user_data(context) == 0 ? 0 : -1;
> -	pColl = sqlGetFuncCollSeq(context);
> +	/* 0 for min() or 0xffffffff for max() */
> +	int mask = sql_user_data(ctx) == 0 ? 0 : -1;
>  	assert(mask == -1 || mask == 0);
> -	iBest = 0;
> -	if (sql_value_is_null(argv[0]))
> -		return;
> -	for (i = 1; i < argc; i++) {
> -		if (sql_value_is_null(argv[i]))
> -			return;
> -		if ((sqlMemCompare(argv[iBest], argv[i], pColl) ^ mask) >=
> -		    0) {
> -			testcase(mask == 0);
> -			iBest = i;
> -		}
> +	struct coll *coll = sqlGetFuncCollSeq(ctx);
> +
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
> +	int best = 0;
> +	if (sql_value_is_null(argv))
> +		return 0;
> +	for (uint32_t i = 1; i < argc; i++) {
> +		if (sql_value_is_null(&argv[i]))
> +			return 0;
> +		if ((sqlMemCompare(&argv[best], &argv[i], coll) ^ mask) >= 0)
> +			best = i;
>  	}
> -	sql_result_value(context, argv[iBest]);
> +	return sqlVdbeMemCopy(val, &argv[best]) != 0 ? -1 : 0;
>  }
>  
>  /*
>   * Return the type of the argument.
>   */
> -static void
> -typeofFunc(sql_context * context, int NotUsed, sql_value ** argv)
> +static int
> +sql_builtin_typeof(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
>  	const char *z = 0;
> -	UNUSED_PARAMETER(NotUsed);
> -	switch (sql_value_type(argv[0])) {
> +	switch (sql_value_type(&argv[0])) {
>  	case MP_INT:
>  		z = "integer";
>  		break;
> @@ -129,40 +184,48 @@ typeofFunc(sql_context * context, int NotUsed, sql_value ** argv)
>  		z = "null";
>  		break;
>  	}
> -	sql_result_text(context, z, -1, SQL_STATIC);
> +	return sqlVdbeMemSetStr(val, z, -1, 1, SQL_STATIC) != 0 ? -1 : 0;
>  }
>  
>  /*
>   * Implementation of the length() function
>   */
> -static void
> -lengthFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_length(struct func *func, struct port *args, struct port *ret)
>  {
> -	int len;
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
>  
>  	assert(argc == 1);
> -	UNUSED_PARAMETER(argc);
> -	switch (sql_value_type(argv[0])) {
> +	switch (sql_value_type(&argv[0])) {
>  	case MP_BIN:
>  	case MP_INT:
>  	case MP_DOUBLE:{
> -			sql_result_int(context,
> -					   sql_value_bytes(argv[0]));
> +			sqlVdbeMemSetInt64(val, sql_value_bytes(&argv[0]));
>  			break;
>  		}
>  	case MP_STR:{
> -			const unsigned char *z = sql_value_text(argv[0]);
> +			const unsigned char *z = sql_value_text(&argv[0]);
>  			if (z == 0)
> -				return;
> -			len = sql_utf8_char_count(z, sql_value_bytes(argv[0]));
> -			sql_result_int(context, len);
> +				return 0;
> +			int len = sql_utf8_char_count(z,
> +						sql_value_bytes(&argv[0]));
> +			sqlVdbeMemSetInt64(val, len);
>  			break;
>  		}
>  	default:{
> -			sql_result_null(context);
> +			sqlVdbeMemSetNull(val);
>  			break;
>  		}
>  	}
> +	return 0;
>  }
>  
>  /*
> @@ -171,36 +234,43 @@ lengthFunc(sql_context * context, int argc, sql_value ** argv)
>   * IMP: R-23979-26855 The abs(X) function returns the absolute value of
>   * the numeric argument X.
>   */
> -static void
> -absFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_abs(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	assert(argc == 1);
> -	UNUSED_PARAMETER(argc);
> -	switch (sql_value_type(argv[0])) {
> +	switch (sql_value_type(&argv[0])) {
>  	case MP_INT:{
> -			i64 iVal = sql_value_int64(argv[0]);
> +			i64 iVal = sql_value_int64(&argv[0]);
>  			if (iVal < 0) {
>  				if (iVal == SMALLEST_INT64) {
>  					diag_set(ClientError, ER_SQL_EXECUTE,
>  						 "integer is overflowed");
> -					context->is_aborted = true;
> -					return;
> +					return -1;
>  				}
>  				iVal = -iVal;
>  			}
> -			sql_result_int64(context, iVal);
> +			sqlVdbeMemSetInt64(val, iVal);
>  			break;
>  		}
>  	case MP_NIL:{
>  			/* IMP: R-37434-19929 Abs(X) returns NULL if X is NULL. */
> -			sql_result_null(context);
> +			sqlVdbeMemSetNull(val);
>  			break;
>  		}
>  	case MP_BOOL: {
>  		diag_set(ClientError, ER_INCONSISTENT_TYPES, "number",
>  			 "boolean");
> -		context->is_aborted = true;
> -		return;
> +		return -1;
>  	}
>  	default:{
>  			/* Because sql_value_double() returns 0.0 if the argument is not
> @@ -208,13 +278,14 @@ absFunc(sql_context * context, int argc, sql_value ** argv)
>  			 * IMP: R-01992-00519 Abs(X) returns 0.0 if X is a string or blob
>  			 * that cannot be converted to a numeric value.
>  			 */
> -			double rVal = sql_value_double(argv[0]);
> +			double rVal = sql_value_double(&argv[0]);
>  			if (rVal < 0)
>  				rVal = -rVal;
> -			sql_result_double(context, rVal);
> +			sqlVdbeMemSetDouble(val, rVal);
>  			break;
>  		}
>  	}
> +	return 0;
>  }
>  
>  /**
> @@ -228,17 +299,28 @@ absFunc(sql_context * context, int argc, sql_value ** argv)
>   * more than the number of bytes in haystack prior to the first
>   * occurrence of needle, or 0 if needle never occurs in haystack.
>   */
> -static void
> -position_func(struct sql_context *context, int argc, struct Mem **argv)
> +static int
> +sql_builtin_position(struct func *func, struct port *args, struct port *ret)
>  {
> -	UNUSED_PARAMETER(argc);
> -	struct Mem *needle = argv[0];
> -	struct Mem *haystack = argv[1];
> +	(void)func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
> +	struct Mem *needle = &argv[0];
> +	struct Mem *haystack = &argv[1];
>  	enum mp_type needle_type = sql_value_type(needle);
>  	enum mp_type haystack_type = sql_value_type(haystack);
>  
>  	if (haystack_type == MP_NIL || needle_type == MP_NIL)
> -		return;
> +		return 0;
>  	/*
>  	 * Position function can be called only with string
>  	 * or blob params.
> @@ -251,8 +333,7 @@ position_func(struct sql_context *context, int argc, struct Mem **argv)
>  	if (inconsistent_type_arg != NULL) {
>  		diag_set(ClientError, ER_INCONSISTENT_TYPES, "TEXT or BLOB",
>  			 mem_type_to_str(inconsistent_type_arg));
> -		context->is_aborted = true;
> -		return;
> +		return -1;
>  	}
>  	/*
>  	 * Both params of Position function must be of the same
> @@ -261,8 +342,7 @@ position_func(struct sql_context *context, int argc, struct Mem **argv)
>  	if (haystack_type != needle_type) {
>  		diag_set(ClientError, ER_INCONSISTENT_TYPES,
>  			 mem_type_to_str(needle), mem_type_to_str(haystack));
> -		context->is_aborted = true;
> -		return;
> +		return -1;
>  	}
>  
>  	int n_needle_bytes = sql_value_bytes(needle);
> @@ -328,7 +408,7 @@ position_func(struct sql_context *context, int argc, struct Mem **argv)
>  					       n_haystack_bytes);
>  			}
>  			int beg_offset = 0;
> -			struct coll *coll = sqlGetFuncCollSeq(context);
> +			struct coll *coll = sqlGetFuncCollSeq(ctx);
>  			int c;
>  			for (c = 0; c + n_needle_chars <= n_haystack_chars; c++) {
>  				if (coll->cmp((const char *) haystack_str + beg_offset,
> @@ -348,34 +428,47 @@ position_func(struct sql_context *context, int argc, struct Mem **argv)
>  		}
>  	}
>  finish:
> -	sql_result_int(context, position);
> +	sqlVdbeMemSetInt64(val, position);
> +	return 0;
>  }
>  
>  /*
>   * Implementation of the printf() function.
>   */
> -static void
> -printfFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_printf(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	PrintfArguments x;
>  	StrAccum str;
>  	const char *zFormat;
>  	int n;
> -	sql *db = sql_context_db_handle(context);
> +	struct sql *db = sql_get();
>  
> -	if (argc >= 1
> -	    && (zFormat = (const char *)sql_value_text(argv[0])) != 0) {
> +	if (argc >= 1 &&
> +	    (zFormat = (const char *)sql_value_text(&argv[0])) != 0) {
>  		x.nArg = argc - 1;
>  		x.nUsed = 0;
> -		x.apArg = argv + 1;
> +		x.apArg = &argv[1];
>  		sqlStrAccumInit(&str, db, 0, 0,
>  				    db->aLimit[SQL_LIMIT_LENGTH]);
>  		str.printfFlags = SQL_PRINTF_SQLFUNC;
>  		sqlXPrintf(&str, zFormat, &x);
>  		n = str.nChar;
> -		sql_result_text(context, sqlStrAccumFinish(&str), n,
> -				    SQL_DYNAMIC);
> +		if (sqlVdbeMemSetStr(val, sqlStrAccumFinish(&str), n,
> +				     1, SQL_DYNAMIC) != 0)
> +			return -1;
>  	}
> +	return 0;
>  }
>  
>  /*
> @@ -390,9 +483,20 @@ printfFunc(sql_context * context, int argc, sql_value ** argv)
>   *
>   * If p2 is negative, return the p2 characters preceding p1.
>   */
> -static void
> -substrFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_substr(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct sql *db = sql_get();
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	const unsigned char *z;
>  	const unsigned char *z2;
>  	int len;
> @@ -401,36 +505,33 @@ substrFunc(sql_context * context, int argc, sql_value ** argv)
>  	int negP2 = 0;
>  
>  	assert(argc == 3 || argc == 2);
> -	if (sql_value_is_null(argv[1])
> -	    || (argc == 3 && sql_value_is_null(argv[2]))
> -	    ) {
> -		return;
> -	}
> -	p0type = sql_value_type(argv[0]);
> -	p1 = sql_value_int(argv[1]);
> +	if (sql_value_is_null(&argv[1]) ||
> +	    (argc == 3 && sql_value_is_null(&argv[2])))
> +		return 0;
> +	p0type = sql_value_type(&argv[0]);
> +	p1 = sql_value_int(&argv[1]);
>  	if (p0type == MP_BIN) {
> -		len = sql_value_bytes(argv[0]);
> -		z = sql_value_blob(argv[0]);
> +		len = sql_value_bytes(&argv[0]);
> +		z = sql_value_blob(&argv[0]);
>  		if (z == 0)
> -			return;
> -		assert(len == sql_value_bytes(argv[0]));
> +			return 0;
> +		assert(len == sql_value_bytes(&argv[0]));
>  	} else {
> -		z = sql_value_text(argv[0]);
> +		z = sql_value_text(&argv[0]);
>  		if (z == 0)
> -			return;
> +			return 0;
>  		len = 0;
>  		if (p1 < 0)
> -			len = sql_utf8_char_count(z, sql_value_bytes(argv[0]));
> +			len = sql_utf8_char_count(z, sql_value_bytes(&argv[0]));
>  	}
>  	if (argc == 3) {
> -		p2 = sql_value_int(argv[2]);
> +		p2 = sql_value_int(&argv[2]);
>  		if (p2 < 0) {
>  			p2 = -p2;
>  			negP2 = 1;
>  		}
>  	} else {
> -		p2 = sql_context_db_handle(context)->
> -		    aLimit[SQL_LIMIT_LENGTH];
> +		p2 = db->aLimit[SQL_LIMIT_LENGTH];
>  	}
>  	if (p1 < 0) {
>  		p1 += len;
> @@ -459,7 +560,7 @@ substrFunc(sql_context * context, int argc, sql_value ** argv)
>  		 * used because '\0' is not supposed to be
>  		 * end-of-string symbol.
>  		 */
> -		int byte_size = sql_value_bytes(argv[0]);
> +		int byte_size = sql_value_bytes(&argv[0]);
>  		int n_chars = sql_utf8_char_count(z, byte_size);
>  		int cnt = 0;
>  		int i = 0;
> @@ -475,38 +576,51 @@ substrFunc(sql_context * context, int argc, sql_value ** argv)
>  			cnt++;
>  		}
>  		z2 += i;
> -		sql_result_text64(context, (char *)z, z2 - z,
> -				      SQL_TRANSIENT);
> +		if (sqlVdbeMemSetStr(val, (char *)z, z2 - z, 1,
> +				     SQL_TRANSIENT) != 0)
> +			return -1;
>  	} else {
>  		if (p1 + p2 > len) {
>  			p2 = len - p1;
>  			if (p2 < 0)
>  				p2 = 0;
>  		}
> -		sql_result_blob64(context, (char *)&z[p1], (u64) p2,
> -				      SQL_TRANSIENT);
> +		if (sqlVdbeMemSetStr(val, (char *)&z[p1], (u64) p2, 0,
> +				     SQL_TRANSIENT) != 0)
> +			return -1;
>  	}
> +	return 0;
>  }
>  
>  /*
>   * Implementation of the round() function
>   */
> -static void
> -roundFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_round(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	int n = 0;
>  	double r;
>  	assert(argc == 1 || argc == 2);
>  	if (argc == 2) {
> -		if (sql_value_is_null(argv[1]))
> -			return;
> -		n = sql_value_int(argv[1]);
> +		if (sql_value_is_null(&argv[1]))
> +			return 0;
> +		n = sql_value_int(&argv[1]);
>  		if (n < 0)
>  			n = 0;
>  	}
> -	if (sql_value_is_null(argv[0]))
> -		return;
> -	r = sql_value_double(argv[0]);
> +	if (sql_value_is_null(&argv[0]))
> +		return 0;
> +	r = sql_value_double(&argv[0]);
>  	/* If Y==0 and X will fit in a 64-bit int,
>  	 * handle the rounding directly,
>  	 * otherwise use printf.
> @@ -519,7 +633,8 @@ roundFunc(sql_context * context, int argc, sql_value ** argv)
>  		const char *rounded_value = tt_sprintf("%.*f", n, r);
>  		sqlAtoF(rounded_value, &r, sqlStrlen30(rounded_value));
>  	}
> -	sql_result_double(context, r);
> +	sqlVdbeMemSetDouble(val, r);
> +	return 0;
>  }
>  
>  /*
> @@ -528,24 +643,15 @@ roundFunc(sql_context * context, int argc, sql_value ** argv)
>   * maximum string or blob length, then raise an error and return
>   * NULL.
>   */
> -static void *
> -contextMalloc(sql_context * context, i64 nByte)
> +static char *
> +sql_blob_alloc(int64_t size)
>  {
> -	char *z;
> -	sql *db = sql_context_db_handle(context);
> -	assert(nByte > 0);
> -	testcase(nByte == db->aLimit[SQL_LIMIT_LENGTH]);
> -	testcase(nByte == db->aLimit[SQL_LIMIT_LENGTH] + 1);
> -	if (nByte > db->aLimit[SQL_LIMIT_LENGTH]) {
> +	struct sql *db = sql_get();
> +	if (size > db->aLimit[SQL_LIMIT_LENGTH]) {
>  		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob too big");
> -		context->is_aborted = true;
> -		z = 0;
> -	} else {
> -		z = sqlMalloc(nByte);
> -		if (z == NULL)
> -			context->is_aborted = true;
> +		return NULL;
>  	}
> -	return z;
> +	return sql_malloc64(size);
>  }
>  
>  /*
> @@ -553,29 +659,38 @@ contextMalloc(sql_context * context, i64 nByte)
>   */
>  
>  #define ICU_CASE_CONVERT(case_type)                                            \
> -static void                                                                    \
> -case_type##ICUFunc(sql_context *context, int argc, sql_value **argv)   \
> +static int                                                                     \
> +sql_builtin_ICU##case_type(struct func *func, struct port *args,               \
> +			   struct port *ret)                                   \
>  {                                                                              \
> +	(void) func;                                                           \
> +	uint32_t argc;                                                         \
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);        \
> +	if (argv == NULL)                                                      \
> +		return -1;                                                     \
> +	struct sql_context *ctx = (struct sql_context *)port_get_context(args);\
> +	assert(ctx != NULL);                                                   \
> +	struct Mem *val = vdbemem_alloc_on_region(1);                           \
> +	if (val == NULL)                                                       \
> +		return -1;                                                     \
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);            \
>  	char *z1;                                                              \
>  	const char *z2;                                                        \
>  	int n;                                                                 \
> -	UNUSED_PARAMETER(argc);                                                \
> -	z2 = (char *)sql_value_text(argv[0]);                              \
> -	n = sql_value_bytes(argv[0]);                                      \
> +	z2 = (char *)sql_value_text(&argv[0]);                                 \
> +	n = sql_value_bytes(&argv[0]);                                         \
>  	/*                                                                     \
>  	 * Verify that the call to _bytes()                                    \
>  	 * does not invalidate the _text() pointer.                            \
>  	 */                                                                    \
> -	assert(z2 == (char *)sql_value_text(argv[0]));                     \
> +	assert(z2 == (char *)sql_value_text(&argv[0]));                        \
>  	if (!z2)                                                               \
> -		return;                                                        \
> -	z1 = contextMalloc(context, ((i64) n) + 1);                            \
> -	if (z1 == NULL) {                                                      \
> -		context->is_aborted = true;                                    \
> -		return;                                                        \
> -	}                                                                      \
> +		return 0;                                                      \
> +	z1 = sql_blob_alloc(((i64) n) + 1);                                    \
> +	if (z1 == NULL)                                                        \
> +		return -1;                                                     \
>  	UErrorCode status = U_ZERO_ERROR;                                      \
> -	struct coll *coll = sqlGetFuncCollSeq(context);                    \
> +	struct coll *coll = sqlGetFuncCollSeq(ctx);                            \
>  	const char *locale = NULL;                                             \
>  	if (coll != NULL && coll->type == COLL_TYPE_ICU) {                     \
>  		locale = ucol_getLocaleByType(coll->collator,                  \
> @@ -586,17 +701,15 @@ case_type##ICUFunc(sql_context *context, int argc, sql_value **argv)   \
>  	int len = ucasemap_utf8To##case_type(case_map, z1, n, z2, n, &status); \
>  	if (len > n) {                                                         \
>  		status = U_ZERO_ERROR;                                         \
> -		sql_free(z1);                                              \
> -		z1 = contextMalloc(context, ((i64) len) + 1);                  \
> -		if (z1 == NULL) {                                              \
> -			context->is_aborted = true;                            \
> -			return;                                                \
> -		}                                                              \
> +		sql_free(z1);                                                  \
> +		z1 = sql_blob_alloc(((i64) len) + 1);                          \
> +		if (z1 == NULL)                                                \
> +			return -1;                                             \
>  		ucasemap_utf8To##case_type(case_map, z1, len, z2, n, &status); \
>  	}                                                                      \
>  	ucasemap_close(case_map);                                              \
> -	sql_result_text(context, z1, len, sql_free);                   \
> -}                                                                              \
> +	return sqlVdbeMemSetStr(val, z1, len, 1, SQL_DYNAMIC) != 0 ? -1 : 0;   \
> +}
>  
>  ICU_CASE_CONVERT(Lower);
>  ICU_CASE_CONVERT(Upper);
> @@ -607,21 +720,27 @@ ICU_CASE_CONVERT(Upper);
>   * as VDBE code so that unused argument values do not have to be
>   * computed.
>   * However, we still need some kind of function implementation for
> - * this routines in the function table. The noopFunc macro
> - * provides this. noopFunc will never be called so it doesn't
> - * matter what the implementation is. We might as well use the
> - * "version()" function as a substitute.
> + * this routines in the function table. The sql_builtin_noop macro
> + * provides this. sql_builtin_noop will never be called so it
> + * doesn't matter what the implementation is. We might as well
> + * use the "version()" function as a substitute.
>   */
> -#define noopFunc sql_func_version /* Substitute function - never called */
> +#define sql_builtin_noop sql_builtin_version
>  
>  /*
>   * Implementation of random().  Return a random integer.
>   */
> -static void
> -randomFunc(sql_context * context, int NotUsed, sql_value ** NotUsed2)
> +static int
> +sql_builtin_random(struct func *func, struct port *args, struct port *ret)
>  {
> -	sql_int64 r;
> -	UNUSED_PARAMETER2(NotUsed, NotUsed2);
> +	(void) func;
> +	(void) args;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
> +	int64_t r;
>  	sql_randomness(sizeof(r), &r);
>  	if (r < 0) {
>  		/* We need to prevent a random number of 0x8000000000000000
> @@ -634,28 +753,36 @@ randomFunc(sql_context * context, int NotUsed, sql_value ** NotUsed2)
>  		 */
>  		r = -(r & LARGEST_INT64);
>  	}
> -	sql_result_int64(context, r);
> +	sqlVdbeMemSetInt64(val, r);
> +	return 0;
>  }
>  
>  /*
> - * Implementation of randomblob(N).  Return a random blob
> + * Implementation of sql_builtin_random_blob(N). Return a random blob
>   * that is N bytes long.
>   */
> -static void
> -randomBlob(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_random_blob(struct func *func, struct port *args, struct port *ret)
>  {
> -	int n;
> -	unsigned char *p;
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	assert(argc == 1);
> -	UNUSED_PARAMETER(argc);
> -	n = sql_value_int(argv[0]);
> +	int n = sql_value_int(&argv[0]);
>  	if (n < 1)
> -		return;
> -	p = contextMalloc(context, n);
> -	if (p) {
> -		sql_randomness(n, p);
> -		sql_result_blob(context, (char *)p, n, sql_free);
> -	}
> +		return 0;
> +	char *p = sql_blob_alloc(n);
> +	if (p == NULL)
> +		return -1;
> +	sql_randomness(n, p);
> +	return sqlVdbeMemSetStr(val, p, n, 0, SQL_DYNAMIC) != 0 ? -1 : 0;
>  }
>  
>  #define Utf8Read(s, e) \
> @@ -880,49 +1007,57 @@ sql_strlike_ci(const char *zPattern, const char *zStr, unsigned int esc)
>   * Both arguments (A and B) must be of type TEXT. If one arguments
>   * is NULL then result is NULL as well.
>   */
> -static void
> -likeFunc(sql_context *context, int argc, sql_value **argv)
> +static int
> +sql_builtin_like(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	u32 escape = SQL_END_OF_STRING;
>  	int nPat;
> -	sql *db = sql_context_db_handle(context);
> +	struct sql *db = sql_get();
>  	bool is_like_ci =
>  		(current_session()->sql_flags & LIKE_CASE_SENS_FLAG) == 0;
> -	int rhs_type = sql_value_type(argv[0]);
> -	int lhs_type = sql_value_type(argv[1]);
> +	int rhs_type = sql_value_type(&argv[0]);
> +	int lhs_type = sql_value_type(&argv[1]);
>  
>  	if (lhs_type != MP_STR || rhs_type != MP_STR) {
>  		if (lhs_type == MP_NIL || rhs_type == MP_NIL)
> -			return;
> +			return 0;
>  		char *inconsistent_type = rhs_type != MP_STR ?
> -					  mem_type_to_str(argv[0]) :
> -					  mem_type_to_str(argv[1]);
> +					  mem_type_to_str(&argv[0]) :
> +					  mem_type_to_str(&argv[1]);
>  		diag_set(ClientError, ER_INCONSISTENT_TYPES, "TEXT",
>  			 inconsistent_type);
> -		context->is_aborted = true;
> -		return;
> +		return -1;
>  	}
> -	const char *zB = (const char *) sql_value_text(argv[0]);
> -	const char *zA = (const char *) sql_value_text(argv[1]);
> -	const char *zB_end = zB + sql_value_bytes(argv[0]);
> -	const char *zA_end = zA + sql_value_bytes(argv[1]);
> +	const char *zB = (const char *) sql_value_text(&argv[0]);
> +	const char *zA = (const char *) sql_value_text(&argv[1]);
> +	const char *zB_end = zB + sql_value_bytes(&argv[0]);
> +	const char *zA_end = zA + sql_value_bytes(&argv[1]);
>  
>  	/*
>  	 * Limit the length of the LIKE pattern to avoid problems
>  	 * of deep recursion and N*N behavior in
>  	 * sql_utf8_pattern_compare().
>  	 */
> -	nPat = sql_value_bytes(argv[0]);
> +	nPat = sql_value_bytes(&argv[0]);
>  	testcase(nPat == db->aLimit[SQL_LIMIT_LIKE_PATTERN_LENGTH]);
>  	testcase(nPat == db->aLimit[SQL_LIMIT_LIKE_PATTERN_LENGTH] + 1);
>  	if (nPat > db->aLimit[SQL_LIMIT_LIKE_PATTERN_LENGTH]) {
>  		diag_set(ClientError, ER_SQL_EXECUTE, "LIKE pattern is too "\
>  			 "complex");
> -		context->is_aborted = true;
> -		return;
> +		return -1;
>  	}
>  	/* Encoding did not change */
> -	assert(zB == (const char *) sql_value_text(argv[0]));
> +	assert(zB == (const char *) sql_value_text(&argv[0]));
>  
>  	if (argc == 3) {
>  		/*
> @@ -930,29 +1065,28 @@ likeFunc(sql_context *context, int argc, sql_value **argv)
>  		 * single UTF-8 character. Otherwise, return an
>  		 * error.
>  		 */
> -		const unsigned char *zEsc = sql_value_text(argv[2]);
> +		const unsigned char *zEsc = sql_value_text(&argv[2]);
>  		if (zEsc == 0)
> -			return;
> -		if (sql_utf8_char_count(zEsc, sql_value_bytes(argv[2])) != 1) {
> +			return 0;
> +		if (sql_utf8_char_count(zEsc, sql_value_bytes(&argv[2])) != 1) {
>  			diag_set(ClientError, ER_SQL_EXECUTE, "ESCAPE "\
>  				 "expression must be a single character");
> -			context->is_aborted = true;
> -			return;
> +			return -1;
>  		}
>  		escape = sqlUtf8Read(&zEsc);
>  	}
>  	if (!zA || !zB)
> -		return;
> +		return 0;
>  	int res;
>  	res = sql_utf8_pattern_compare(zB, zA, zB_end, zA_end,
>  				       is_like_ci, escape);
>  	if (res == INVALID_PATTERN) {
>  		diag_set(ClientError, ER_SQL_EXECUTE, "LIKE pattern can only "\
>  			 "contain UTF-8 characters");
> -		context->is_aborted = true;
> -		return;
> +		return -1;
>  	}
> -	sql_result_bool(context, res == MATCH);
> +	mem_set_bool(val, res == MATCH);
> +	return 0;
>  }
>  
>  /*
> @@ -960,14 +1094,26 @@ likeFunc(sql_context *context, int argc, sql_value **argv)
>   * argument if the arguments are different.  The result is NULL if the
>   * arguments are equal to each other.
>   */
> -static void
> -nullifFunc(sql_context * context, int NotUsed, sql_value ** argv)
> +static int
> +sql_builtin_nullif(struct func *func, struct port *args, struct port *ret)
>  {
> -	struct coll *pColl = sqlGetFuncCollSeq(context);
> -	UNUSED_PARAMETER(NotUsed);
> -	if (sqlMemCompare(argv[0], argv[1], pColl) != 0) {
> -		sql_result_value(context, argv[0]);
> -	}
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
> +	struct coll *pColl = sqlGetFuncCollSeq(ctx);
> +	if (sqlMemCompare(&argv[0], &argv[1], pColl) != 0 &&
> +	    sqlVdbeMemCopy(val, &argv[0]) != 0)
> +		return -1;
> +	return 0;
>  }
>  
>  /**
> @@ -978,12 +1124,17 @@ nullifFunc(sql_context * context, int NotUsed, sql_value ** argv)
>   * @param unused1 Unused.
>   * @param unused2 Unused.
>   */
> -static void
> -sql_func_version(struct sql_context *context,
> -		 MAYBE_UNUSED int unused1,
> -		 MAYBE_UNUSED sql_value **unused2)
> +static int
> +sql_builtin_version(struct func *func, struct port *args, struct port *ret)
>  {
> -	sql_result_text(context, tarantool_version(), -1, SQL_STATIC);
> +	(void) func;
> +	(void) args;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +	return sqlVdbeMemSetStr(val, tarantool_version(), -1, 1,
> +				SQL_STATIC) != 0 ? -1 : 0;
>  }
>  
>  /* Array for converting from half-bytes (nybbles) into ASCII hex
> @@ -1001,108 +1152,126 @@ static const char hexdigits[] = {
>   * "NULL".  Otherwise, the argument is enclosed in single quotes with
>   * single-quote escapes.
>   */
> -static void
> -quoteFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_quote(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	assert(argc == 1);
> -	UNUSED_PARAMETER(argc);
> -	switch (sql_value_type(argv[0])) {
> +	switch (sql_value_type(&argv[0])) {
>  	case MP_DOUBLE:{
>  			double r1, r2;
>  			char zBuf[50];
> -			r1 = sql_value_double(argv[0]);
> +			r1 = sql_value_double(&argv[0]);
>  			sql_snprintf(sizeof(zBuf), zBuf, "%!.15g", r1);
>  			sqlAtoF(zBuf, &r2, 20);
>  			if (r1 != r2) {
>  				sql_snprintf(sizeof(zBuf), zBuf, "%!.20e",
>  						 r1);
>  			}
> -			sql_result_text(context, zBuf, -1,
> -					    SQL_TRANSIENT);
> +			if (sqlVdbeMemSetStr(val, zBuf, -1, 1,
> +					     SQL_TRANSIENT) != 0)
> +				return -1;
>  			break;
>  		}
>  	case MP_INT:{
> -			sql_result_value(context, argv[0]);
> +			if (sqlVdbeMemCopy(val, &argv[0]) != 0)
> +				return -1;
>  			break;
>  		}
>  	case MP_BIN:{
> -			char *zText = 0;
> -			char const *zBlob = sql_value_blob(argv[0]);
> -			int nBlob = sql_value_bytes(argv[0]);
> -			assert(zBlob == sql_value_blob(argv[0]));	/* No encoding change */
> -			zText =
> -			    (char *)contextMalloc(context,
> -						  (2 * (i64) nBlob) + 4);
> -			if (zText) {
> -				int i;
> -				for (i = 0; i < nBlob; i++) {
> -					zText[(i * 2) + 2] =
> -					    hexdigits[(zBlob[i] >> 4) & 0x0F];
> -					zText[(i * 2) + 3] =
> -					    hexdigits[(zBlob[i]) & 0x0F];
> -				}
> -				zText[(nBlob * 2) + 2] = '\'';
> -				zText[(nBlob * 2) + 3] = '\0';
> -				zText[0] = 'X';
> -				zText[1] = '\'';
> -				sql_result_text(context, zText, -1,
> -						    SQL_TRANSIENT);
> -				sql_free(zText);
> +			char const *zBlob = sql_value_blob(&argv[0]);
> +			int nBlob = sql_value_bytes(&argv[0]);
> +			assert(zBlob == sql_value_blob(&argv[0]));	/* No encoding change */
> +			char *z = sql_blob_alloc((2 * (i64) nBlob) + 4);
> +			if (z == NULL)
> +				return -1;
> +			for (int i = 0; i < nBlob; i++) {
> +				z[(i * 2) + 2] =
> +					hexdigits[(zBlob[i] >> 4) & 0x0F];
> +				z[(i * 2) + 3] =
> +					hexdigits[(zBlob[i]) & 0x0F];
>  			}
> +			z[(nBlob * 2) + 2] = '\'';
> +			z[(nBlob * 2) + 3] = '\0';
> +			z[0] = 'X';
> +			z[1] = '\'';
> +			if (sqlVdbeMemSetStr(val, z, -1, 1, SQL_DYNAMIC) != 0)
> +				return -1;
>  			break;
>  		}
>  	case MP_STR:{
>  			int i, j;
>  			u64 n;
> -			const unsigned char *zArg = sql_value_text(argv[0]);
> -			char *z;
> -
> +			const unsigned char *zArg = sql_value_text(&argv[0]);
>  			if (zArg == 0)
> -				return;
> +				return 0;
>  			for (i = 0, n = 0; zArg[i]; i++) {
>  				if (zArg[i] == '\'')
>  					n++;
>  			}
> -			z = contextMalloc(context, ((i64) i) + ((i64) n) + 3);
> -			if (z) {
> -				z[0] = '\'';
> -				for (i = 0, j = 1; zArg[i]; i++) {
> -					z[j++] = zArg[i];
> -					if (zArg[i] == '\'') {
> -						z[j++] = '\'';
> -					}
> +			char *z = sql_blob_alloc(((i64) i) + ((i64) n) + 3);
> +			if (z == NULL)
> +				return -1;
> +			z[0] = '\'';
> +			for (i = 0, j = 1; zArg[i]; i++) {
> +				z[j++] = zArg[i];
> +				if (zArg[i] == '\'') {
> +					z[j++] = '\'';
>  				}
> -				z[j++] = '\'';
> -				z[j] = 0;
> -				sql_result_text(context, z, j,
> -						    sql_free);
>  			}
> +			z[j++] = '\'';
> +			z[j] = 0;
> +			if (sqlVdbeMemSetStr(val, z, j, 1, SQL_DYNAMIC) != 0)
> +				return -1;
>  			break;
>  		}
>  	case MP_BOOL: {
> -		sql_result_text(context, sql_value_boolean(argv[0]) ?
> -				"true" : "false", -1, SQL_TRANSIENT);
> +		if (sqlVdbeMemSetStr(val, sql_value_boolean(&argv[0]) ?
> +				     "true" : "false", -1, 1, SQL_STATIC) != 0)
> +			return -1;
>  		break;
>  	}
>  	default:{
> -			assert(sql_value_is_null(argv[0]));
> -			sql_result_text(context, "NULL", 4, SQL_STATIC);
> +			assert(sql_value_is_null(&argv[0]));
> +			if (sqlVdbeMemSetStr(val, "NULL", 4, 1,
> +					     SQL_STATIC) != 0)
> +				return -1;
>  			break;
>  		}
>  	}
> +	return 0;
>  }
>  
>  /*
>   * The unicode() function.  Return the integer unicode code-point value
>   * for the first character of the input string.
>   */
> -static void
> -unicodeFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_unicode(struct func *func, struct port *args, struct port *ret)
>  {
> -	const unsigned char *z = sql_value_text(argv[0]);
> -	(void)argc;
> -	if (z && z[0])
> -		sql_result_int(context, sqlUtf8Read(&z));
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
> +	const unsigned char *z = sql_value_text(&argv[0]);
> +	if (z != NULL && z[0] != '\0')
> +		sqlVdbeMemSetInt64(val, sqlUtf8Read(&z));
> +	return 0;
>  }
>  
>  /*
> @@ -1110,20 +1279,27 @@ unicodeFunc(sql_context * context, int argc, sql_value ** argv)
>   * an integer.  It constructs a string where each character of the string
>   * is the unicode character for the corresponding integer argument.
>   */
> -static void
> -charFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_char(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	unsigned char *z, *zOut;
> -	int i;
>  	zOut = z = sql_malloc64(argc * 4 + 1);
> -	if (z == NULL) {
> -		context->is_aborted = true;
> -		return;
> -	}
> -	for (i = 0; i < argc; i++) {
> +	if (z == NULL)
> +		return -1;
> +	for (uint32_t i = 0; i < argc; i++) {
>  		sql_int64 x;
>  		unsigned c;
> -		x = sql_value_int64(argv[i]);
> +		x = sql_value_int64(&argv[i]);
>  		if (x < 0 || x > 0x10ffff)
>  			x = 0xfffd;
>  		c = (unsigned)(x & 0x1fffff);
> @@ -1143,52 +1319,79 @@ charFunc(sql_context * context, int argc, sql_value ** argv)
>  			*zOut++ = 0x80 + (u8) (c & 0x3F);
>  		}
>  	}
> -	sql_result_text64(context, (char *)z, zOut - z, sql_free);
> +	if (zOut - z > sql_get()->aLimit[SQL_LIMIT_LENGTH]) {
> +		sql_free(z);
> +		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob too big");
> +		return -1;
> +	}
> +	return sqlVdbeMemSetStr(val, (char *)z, zOut - z, 1,
> +				SQL_DYNAMIC) != 0 ? -1 : 0;
>  }
>  
>  /*
>   * The hex() function.  Interpret the argument as a blob.  Return
>   * a hexadecimal rendering as text.
>   */
> -static void
> -hexFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_hex(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	assert(argc == 1);
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	int i, n;
>  	const unsigned char *pBlob;
>  	char *zHex, *z;
> -	assert(argc == 1);
> -	UNUSED_PARAMETER(argc);
> -	pBlob = sql_value_blob(argv[0]);
> -	n = sql_value_bytes(argv[0]);
> -	assert(pBlob == sql_value_blob(argv[0]));	/* No encoding change */
> -	z = zHex = contextMalloc(context, ((i64) n) * 2 + 1);
> -	if (zHex) {
> -		for (i = 0; i < n; i++, pBlob++) {
> -			unsigned char c = *pBlob;
> -			*(z++) = hexdigits[(c >> 4) & 0xf];
> -			*(z++) = hexdigits[c & 0xf];
> -		}
> -		*z = 0;
> -		sql_result_text(context, zHex, n * 2, sql_free);
> +	pBlob = sql_value_blob(&argv[0]);
> +	n = sql_value_bytes(&argv[0]);
> +	assert(pBlob == sql_value_blob(&argv[0]));	/* No encoding change */
> +	z = zHex = sql_blob_alloc(((i64) n) * 2 + 1);
> +	if (z == NULL)
> +		return -1;
> +	for (i = 0; i < n; i++, pBlob++) {
> +		unsigned char c = *pBlob;
> +		*(z++) = hexdigits[(c >> 4) & 0xf];
> +		*(z++) = hexdigits[c & 0xf];
>  	}
> +	*z = 0;
> +	return sqlVdbeMemSetStr(val, zHex, n * 2, 1, SQL_DYNAMIC) != 0 ? -1 : 0;
>  }
>  
>  /*
>   * The zeroblob(N) function returns a zero-filled blob of size N bytes.
>   */
> -static void
> -zeroblobFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_zeroblob(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	i64 n;
>  	assert(argc == 1);
>  	UNUSED_PARAMETER(argc);
> -	n = sql_value_int64(argv[0]);
> +	n = sql_value_int64(&argv[0]);
>  	if (n < 0)
>  		n = 0;
> -	if (sql_result_zeroblob64(context, n) != 0) {
> +	if (n > sql_get()->aLimit[SQL_LIMIT_LENGTH]) {
>  		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob too big");
> -		context->is_aborted = true;
> +		return -1;
>  	}
> +	sqlVdbeMemSetZeroBlob(val, n);
> +	return 0;
>  }
>  
>  /*
> @@ -1197,9 +1400,19 @@ zeroblobFunc(sql_context * context, int argc, sql_value ** argv)
>   * from A by replacing every occurrence of B with C.  The match
>   * must be exact.  Collating sequences are not used.
>   */
> -static void
> -replaceFunc(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_replace(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +
>  	const unsigned char *zStr;	/* The input string A */
>  	const unsigned char *zPattern;	/* The pattern string B */
>  	const unsigned char *zRep;	/* The replacement string C */
> @@ -1213,35 +1426,30 @@ replaceFunc(sql_context * context, int argc, sql_value ** argv)
>  
>  	assert(argc == 3);
>  	UNUSED_PARAMETER(argc);
> -	zStr = sql_value_text(argv[0]);
> +	zStr = sql_value_text(&argv[0]);
>  	if (zStr == 0)
> -		return;
> -	nStr = sql_value_bytes(argv[0]);
> -	assert(zStr == sql_value_text(argv[0]));	/* No encoding change */
> -	zPattern = sql_value_text(argv[1]);
> -	if (zPattern == 0) {
> -		assert(sql_value_is_null(argv[1])
> -		       || sql_context_db_handle(context)->mallocFailed);
> -		return;
> -	}
> -	nPattern = sql_value_bytes(argv[1]);
> +		return 0;
> +	nStr = sql_value_bytes(&argv[0]);
> +	assert(zStr == sql_value_text(&argv[0]));
> +	zPattern = sql_value_text(&argv[1]);
> +	if (zPattern == 0)
> +		return 0;
> +	nPattern = sql_value_bytes(&argv[1]);
>  	if (nPattern == 0) {
> -		assert(! sql_value_is_null(argv[1]));
> -		sql_result_value(context, argv[0]);
> -		return;
> +		assert(! sql_value_is_null(&argv[1]));
> +		return sqlVdbeMemCopy(val, &argv[0]) != 0 ? -1 : 0;
>  	}
> -	assert(zPattern == sql_value_text(argv[1]));	/* No encoding change */
> -	zRep = sql_value_text(argv[2]);
> +	assert(zPattern == sql_value_text(&argv[1]));
> +	zRep = sql_value_text(&argv[2]);
>  	if (zRep == 0)
> -		return;
> -	nRep = sql_value_bytes(argv[2]);
> -	assert(zRep == sql_value_text(argv[2]));
> +		return 0;
> +	nRep = sql_value_bytes(&argv[2]);
> +	assert(zRep == sql_value_text(&argv[2]));
>  	nOut = nStr + 1;
>  	assert(nOut < SQL_MAX_LENGTH);
> -	zOut = contextMalloc(context, (i64) nOut);
> -	if (zOut == 0) {
> -		return;
> -	}
> +	zOut = (unsigned char *)sql_blob_alloc(nOut);
> +	if (zOut == NULL)
> +		return -1;
>  	loopLimit = nStr - nPattern;
>  	for (i = j = 0; i <= loopLimit; i++) {
>  		if (zStr[i] != zPattern[0]
> @@ -1249,23 +1457,21 @@ replaceFunc(sql_context * context, int argc, sql_value ** argv)
>  			zOut[j++] = zStr[i];
>  		} else {
>  			u8 *zOld;
> -			sql *db = sql_context_db_handle(context);
> +			struct sql *db = sql_get();
>  			nOut += nRep - nPattern;
>  			testcase(nOut - 1 == db->aLimit[SQL_LIMIT_LENGTH]);
>  			testcase(nOut - 2 == db->aLimit[SQL_LIMIT_LENGTH]);
>  			if (nOut - 1 > db->aLimit[SQL_LIMIT_LENGTH]) {
>  				diag_set(ClientError, ER_SQL_EXECUTE, "string "\
>  					 "or blob too big");
> -				context->is_aborted = true;
>  				sql_free(zOut);
> -				return;
> +				return -1;
>  			}
>  			zOld = zOut;
>  			zOut = sql_realloc64(zOut, (int)nOut);
>  			if (zOut == 0) {
> -				context->is_aborted = true;
>  				sql_free(zOld);
> -				return;
> +				return -1;
>  			}
>  			memcpy(&zOut[j], zRep, nRep);
>  			j += nRep;
> @@ -1277,23 +1483,25 @@ replaceFunc(sql_context * context, int argc, sql_value ** argv)
>  	j += nStr - i;
>  	assert(j <= nOut);
>  	zOut[j] = 0;
> -	sql_result_text(context, (char *)zOut, j, sql_free);
> +	return sqlVdbeMemSetStr(val, (char *)zOut, j, 1,
> +				SQL_DYNAMIC) != 0 ? -1 : 0;
>  }
>  
>  /**
>   * Remove characters included in @a trim_set from @a input_str
>   * until encounter a character that doesn't belong to @a trim_set.
>   * Remove from the side specified by @a flags.
> - * @param context SQL context.
> + * @param ret SQL value to store result.
>   * @param flags Trim specification: left, right or both.
>   * @param trim_set The set of characters for trimming.
>   * @param char_len Lengths of each UTF-8 character in @a trim_set.
>   * @param char_cnt A number of UTF-8 characters in @a trim_set.
>   * @param input_str Input string for trimming.
>   * @param input_str_sz Input string size in bytes.
> + * @return 0 On success, -1 otherwise.
>   */
> -static void
> -trim_procedure(struct sql_context *context, enum trim_side_mask flags,
> +static int
> +trim_procedure(struct Mem *val, enum trim_side_mask flags,
>  	       const unsigned char *trim_set, const uint8_t *char_len,
>  	       int char_cnt, const unsigned char *input_str, int input_str_sz)
>  {
> @@ -1332,8 +1540,8 @@ trim_procedure(struct sql_context *context, enum trim_side_mask flags,
>  		}
>  	}
>  finish:
> -	sql_result_text(context, (char *)input_str, input_str_sz,
> -			SQL_TRANSIENT);
> +	return sqlVdbeMemSetStr(val, (char *)input_str, input_str_sz, 1,
> +				SQL_TRANSIENT) != 0 ? -1 : 0;
>  }
>  
>  /**
> @@ -1348,8 +1556,7 @@ finish:
>   * @retval -1 Memory allocation error.
>   */
>  static int
> -trim_prepare_char_len(struct sql_context *context,
> -		      const unsigned char *trim_set, int trim_set_sz,
> +trim_prepare_char_len(const unsigned char *trim_set, int trim_set_sz,
>  		      uint8_t **char_len)
>  {
>  	/*
> @@ -1364,7 +1571,7 @@ trim_prepare_char_len(struct sql_context *context,
>  		return 0;
>  	}
>  
> -	if ((*char_len = (uint8_t *)contextMalloc(context, char_cnt)) == NULL)
> +	if ((*char_len = (uint8_t *)sql_blob_alloc(char_cnt)) == NULL)
>  		return -1;
>  
>  	int i = 0, j = 0;
> @@ -1385,20 +1592,29 @@ trim_prepare_char_len(struct sql_context *context,
>   * Call trimming procedure with TRIM_BOTH as the flags and " " as
>   * the trimming set.
>   */
> -static void
> -trim_func_one_arg(struct sql_context *context, int argc, sql_value **argv)
> +static int
> +sql_builtin_trim_one_arg(struct func *func, struct port *args, struct port *ret)
>  {
> -	assert(argc == 1);
> -	(void) argc;
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
>  
> +	assert(argc == 1);
>  	const unsigned char *input_str;
> -	if ((input_str = sql_value_text(argv[0])) == NULL)
> -		return;
> +	if ((input_str = sql_value_text(&argv[0])) == NULL)
> +		return 0;
>  
> -	int input_str_sz = sql_value_bytes(argv[0]);
> +	int input_str_sz = sql_value_bytes(&argv[0]);
>  	uint8_t len_one = 1;
> -	trim_procedure(context, TRIM_BOTH, (const unsigned char *) " ",
> +	trim_procedure(val, TRIM_BOTH, (const unsigned char *) " ",
>  		       &len_one, 1, input_str, input_str_sz);
> +	return 0;
>  }
>  
>  /**
> @@ -1412,33 +1628,46 @@ trim_func_one_arg(struct sql_context *context, int argc, sql_value **argv)
>   * If user has specified side keyword only, then call trimming
>   * procedure with the specified side and " " as the trimming set.
>   */
> -static void
> -trim_func_two_args(struct sql_context *context, int argc, sql_value **argv)
> +static int
> +sql_builtin_trim_two_args(struct func *func, struct port *args,
> +			  struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
>  	assert(argc == 2);
> -	(void) argc;
>  
>  	const unsigned char *input_str, *trim_set;
> -	if ((input_str = sql_value_text(argv[1])) == NULL)
> -		return;
> +	if ((input_str = sql_value_text(&argv[1])) == NULL)
> +		return 0;
>  
> -	int input_str_sz = sql_value_bytes(argv[1]);
> -	if (sql_value_type(argv[0]) == MP_INT) {
> +	int input_str_sz = sql_value_bytes(&argv[1]);
> +	if (sql_value_type(&argv[0]) == MP_INT) {
>  		uint8_t len_one = 1;
> -		trim_procedure(context, sql_value_int(argv[0]),
> -			       (const unsigned char *) " ", &len_one, 1,
> -			       input_str, input_str_sz);
> -	} else if ((trim_set = sql_value_text(argv[0])) != NULL) {
> -		int trim_set_sz = sql_value_bytes(argv[0]);
> -		uint8_t *char_len;
> -		int char_cnt = trim_prepare_char_len(context, trim_set,
> -						     trim_set_sz, &char_len);
> -		if (char_cnt == -1)
> -			return;
> -		trim_procedure(context, TRIM_BOTH, trim_set, char_len, char_cnt,
> -			       input_str, input_str_sz);
> +		if (trim_procedure(val, sql_value_int(&argv[0]),
> +				   (const unsigned char *) " ", &len_one, 1,
> +				   input_str, input_str_sz) != 0)
> +			return -1;
> +	} else if ((trim_set = sql_value_text(&argv[0])) != NULL) {
> +		int trim_set_sz = sql_value_bytes(&argv[0]);
> +		uint8_t *char_len = NULL;
> +		int char_cnt =
> +			trim_prepare_char_len(trim_set, trim_set_sz, &char_len);
> +		if (char_cnt == -1 ||
> +		    trim_procedure(val, TRIM_BOTH, trim_set, char_len,
> +				   char_cnt, input_str, input_str_sz) != 0) {
> +			sql_free(char_len);
> +			return -1;
> +		}
>  		sql_free(char_len);
>  	}
> +	return 0;
>  }
>  
>  /**
> @@ -1448,28 +1677,37 @@ trim_func_two_args(struct sql_context *context, int argc, sql_value **argv)
>   * If user has specified side keyword and <character_set>, then
>   * call trimming procedure with that args.
>   */
> -static void
> -trim_func_three_args(struct sql_context *context, int argc, sql_value **argv)
> +static int
> +sql_builtin_trim_three_args(struct func *func, struct port *args,
> +			    struct port *ret)
>  {
> +	(void) func;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
>  	assert(argc == 3);
> -	(void) argc;
>  
> -	assert(sql_value_type(argv[0]) == MP_INT);
> +	assert(sql_value_type(&argv[0]) == MP_INT);
>  	const unsigned char *input_str, *trim_set;
> -	if ((input_str = sql_value_text(argv[2])) == NULL ||
> -	    (trim_set = sql_value_text(argv[1])) == NULL)
> -		return;
> +	if ((input_str = sql_value_text(&argv[2])) == NULL ||
> +	    (trim_set = sql_value_text(&argv[1])) == NULL)
> +		return 0;
>  
> -	int trim_set_sz = sql_value_bytes(argv[1]);
> -	int input_str_sz = sql_value_bytes(argv[2]);
> +	int trim_set_sz = sql_value_bytes(&argv[1]);
> +	int input_str_sz = sql_value_bytes(&argv[2]);
>  	uint8_t *char_len;
> -	int char_cnt = trim_prepare_char_len(context, trim_set, trim_set_sz,
> -					     &char_len);
> +	int char_cnt = trim_prepare_char_len(trim_set, trim_set_sz, &char_len);
>  	if (char_cnt == -1)
> -		return;
> -	trim_procedure(context, sql_value_int(argv[0]), trim_set, char_len,
> -		       char_cnt, input_str, input_str_sz);
> +		return -1;
> +	int rc = trim_procedure(val, sql_value_int(&argv[0]), trim_set, char_len,
> +				char_cnt, input_str, input_str_sz);
>  	sql_free(char_len);
> +	return rc;
>  }
>  
>  /*
> @@ -1495,72 +1733,101 @@ struct SumCtx {
>   * value.  TOTAL never fails, but SUM might through an exception if
>   * it overflows an integer.
>   */
> -static void
> -sum_step(struct sql_context *context, int argc, sql_value **argv)
> +static int
> +sql_builtin_sum_step(struct func *func, struct port *args, struct port *ret)
>  {
> +	(void) func;
> +	(void) ret;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
>  	assert(argc == 1);
> -	UNUSED_PARAMETER(argc);
> -	struct SumCtx *p = sql_aggregate_context(context, sizeof(*p));
> -	int type = sql_value_type(argv[0]);
> +	struct SumCtx *p = sql_aggregate_context(ctx, sizeof(*p));
> +	int type = sql_value_type(&argv[0]);
>  	if (type == MP_NIL || p == NULL)
> -		return;
> +		return 0;
>  	if (type != MP_DOUBLE && type != MP_INT) {
> -		if (mem_apply_numeric_type(argv[0]) != 0) {
> +		if (mem_apply_numeric_type(&argv[0]) != 0) {
>  			diag_set(ClientError, ER_SQL_TYPE_MISMATCH,
> -				 sql_value_text(argv[0]), "number");
> -			context->is_aborted = true;
> -			return;
> +				 sql_value_text(&argv[0]), "number");
> +			return -1;
>  		}
> -		type = sql_value_type(argv[0]);
> +		type = sql_value_type(&argv[0]);
>  	}
>  	p->cnt++;
>  	if (type == MP_INT) {
> -		int64_t v = sql_value_int64(argv[0]);
> +		int64_t v = sql_value_int64(&argv[0]);
>  		p->rSum += v;
>  		if ((p->approx | p->overflow) == 0 &&
>  		    sqlAddInt64(&p->iSum, v) != 0) {
>  			p->overflow = 1;
>  		}
>  	} else {
> -		p->rSum += sql_value_double(argv[0]);
> +		p->rSum += sql_value_double(&argv[0]);
>  		p->approx = 1;
>  	}
> +	return 0;
>  }
>  
> -static void
> -sumFinalize(sql_context * context)
> +static int
> +sql_builtin_sum_finalize(struct func *func, struct port *args, struct port *ret)
>  {
> -	SumCtx *p;
> -	p = sql_aggregate_context(context, 0);
> +	(void) func;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	struct SumCtx *p = sql_aggregate_context(ctx, 0);
>  	if (p && p->cnt > 0) {
>  		if (p->overflow) {
> -			diag_set(ClientError, ER_SQL_EXECUTE, "integer "\
> -				 "overflow");
> -			context->is_aborted = true;
> +			diag_set(ClientError, ER_SQL_EXECUTE,
> +				 "integer overflow");
> +			return -1;
>  		} else if (p->approx) {
> -			sql_result_double(context, p->rSum);
> +			sqlVdbeMemSetDouble(val, p->rSum);
>  		} else {
> -			sql_result_int64(context, p->iSum);
> +			sqlVdbeMemSetInt64(val, p->iSum);
>  		}
>  	}
> +	return 0;
>  }
>  
> -static void
> -avgFinalize(sql_context * context)
> +static int
> +sql_builtin_avg_finalize(struct func *func, struct port *args,
> +			 struct port *ret)
>  {
> -	SumCtx *p;
> -	p = sql_aggregate_context(context, 0);
> -	if (p && p->cnt > 0) {
> -		sql_result_double(context, p->rSum / (double)p->cnt);
> -	}
> +	(void) func;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	struct SumCtx *p = sql_aggregate_context(ctx, 0);
> +	if (p && p->cnt > 0)
> +		sqlVdbeMemSetDouble(val, p->rSum / (double)p->cnt);
> +	return 0;
>  }
>  
> -static void
> -totalFinalize(sql_context * context)
> +static int
> +sql_builtin_total_finalize(struct func *func, struct port *args,
> +			   struct port *ret)
>  {
> -	SumCtx *p;
> -	p = sql_aggregate_context(context, 0);
> -	sql_result_double(context, p ? p->rSum : (double)0);
> +	(void) func;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	struct SumCtx *p = sql_aggregate_context(ctx, 0);
> +	sqlVdbeMemSetDouble(val, p != NULL ? p->rSum : (double)0);
> +	return 0;
>  }
>  
>  /*
> @@ -1575,45 +1842,67 @@ struct CountCtx {
>  /*
>   * Routines to implement the count() aggregate function.
>   */
> -static void
> -countStep(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_count_step(struct func *func, struct port *args, struct port *ret)
>  {
> -	CountCtx *p;
> -	p = sql_aggregate_context(context, sizeof(*p));
> -	if ((argc == 0 || ! sql_value_is_null(argv[0])) && p) {
> +	(void) func;
> +	(void) ret;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	struct CountCtx *p = sql_aggregate_context(ctx, sizeof(*p));
> +	if ((argc == 0 || ! sql_value_is_null(&argv[0])) && p)
>  		p->n++;
> -	}
> +	return 0;
>  }
>  
> -static void
> -countFinalize(sql_context * context)
> +static int
> +sql_builtin_count_finalize(struct func *func, struct port *args,
> +			   struct port *ret)
>  {
> -	CountCtx *p;
> -	p = sql_aggregate_context(context, 0);
> -	sql_result_int64(context, p ? p->n : 0);
> +	(void) func;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	struct CountCtx *p = sql_aggregate_context(ctx, 0);
> +	sqlVdbeMemSetInt64(val, p != NULL ? p->n : 0);
> +	return 0;
>  }
>  
>  /*
>   * Routines to implement min() and max() aggregate functions.
>   */
> -static void
> -minmaxStep(sql_context * context, int NotUsed, sql_value ** argv)
> +static int
> +sql_builtin_minmax_step(struct func *func, struct port *args, struct port *ret)
>  {
> -	Mem *pArg = (Mem *) argv[0];
> +	(void) func;
> +	(void) ret;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	Mem *pArg = (Mem *) &argv[0];
>  	Mem *pBest;
> -	UNUSED_PARAMETER(NotUsed);
>  
> -	pBest = (Mem *) sql_aggregate_context(context, sizeof(*pBest));
> -	if (!pBest)
> -		return;
> +	pBest = (Mem *) sql_aggregate_context(ctx, sizeof(*pBest));
> +	if (pBest == NULL)
> +		return -1;
>  
> -	if (sql_value_is_null(argv[0])) {
> +	if (sql_value_is_null(&argv[0])) {
>  		if (pBest->flags)
> -			sqlSkipAccumulatorLoad(context);
> +			sqlSkipAccumulatorLoad(ctx);
>  	} else if (pBest->flags) {
>  		int max;
>  		int cmp;
> -		struct coll *pColl = sqlGetFuncCollSeq(context);
> +		struct coll *pColl = sqlGetFuncCollSeq(ctx);
>  		/* This step function is used for both the min() and max() aggregates,
>  		 * the only difference between the two being that the sense of the
>  		 * comparison is inverted. For the max() aggregate, the
> @@ -1622,88 +1911,120 @@ minmaxStep(sql_context * context, int NotUsed, sql_value ** argv)
>  		 * Therefore the next statement sets variable 'max' to 1 for the max()
>  		 * aggregate, or 0 for min().
>  		 */
> -		max = sql_user_data(context) != 0;
> +		max = sql_user_data(ctx) != 0;
>  		cmp = sqlMemCompare(pBest, pArg, pColl);
>  		if ((max && cmp < 0) || (!max && cmp > 0)) {
> -			sqlVdbeMemCopy(pBest, pArg);
> +			if (sqlVdbeMemCopy(pBest, pArg) != 0)
> +				return -1;
>  		} else {
> -			sqlSkipAccumulatorLoad(context);
> +			sqlSkipAccumulatorLoad(ctx);
>  		}
>  	} else {
> -		pBest->db = sql_context_db_handle(context);
> -		sqlVdbeMemCopy(pBest, pArg);
> +		pBest->db = sql_get();
> +		if (sqlVdbeMemCopy(pBest, pArg) != 0)
> +			return -1;
>  	}
> +	return 0;
>  }
>  
> -static void
> -minMaxFinalize(sql_context * context)
> +static int
> +sql_builtin_minmax_finalize(struct func *func, struct port *args,
> +			    struct port *ret)
>  {
> -	sql_value *pRes;
> -	pRes = (sql_value *) sql_aggregate_context(context, 0);
> -	if (pRes) {
> -		if (pRes->flags) {
> -			sql_result_value(context, pRes);
> +	(void) func;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +	struct Mem *res =
> +		(struct Mem *) sql_aggregate_context(ctx, 0);
> +	if (res != NULL) {
> +		if (res->flags != 0 &&
> +		    sqlVdbeMemCopy(val, res) != 0) {
> +			sqlVdbeMemRelease(res);
> +			return -1;
>  		}
> -		sqlVdbeMemRelease(pRes);
> +		sqlVdbeMemRelease(res);
>  	}
> +	return 0;
>  }
>  
>  /*
>   * group_concat(EXPR, ?SEPARATOR?)
>   */
> -static void
> -groupConcatStep(sql_context * context, int argc, sql_value ** argv)
> +static int
> +sql_builtin_group_concat_step(struct func *func, struct port *args,
> +			      struct port *ret)
>  {
> -	const char *zVal;
> -	StrAccum *pAccum;
> -	const char *zSep;
> -	int nVal, nSep;
> +	(void) func;
> +	(void) ret;
> +	uint32_t argc;
> +	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> +	if (argv == NULL)
> +		return -1;
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +
>  	assert(argc == 1 || argc == 2);
> -	if (sql_value_is_null(argv[0]))
> -		return;
> -	pAccum =
> -	    (StrAccum *) sql_aggregate_context(context, sizeof(*pAccum));
> -
> -	if (pAccum) {
> -		sql *db = sql_context_db_handle(context);
> -		int firstTerm = pAccum->mxAlloc == 0;
> -		pAccum->mxAlloc = db->aLimit[SQL_LIMIT_LENGTH];
> -		if (!firstTerm) {
> -			if (argc == 2) {
> -				zSep = (char *)sql_value_text(argv[1]);
> -				nSep = sql_value_bytes(argv[1]);
> -			} else {
> -				zSep = ",";
> -				nSep = 1;
> -			}
> -			if (zSep)
> -				sqlStrAccumAppend(pAccum, zSep, nSep);
> +	if (sql_value_is_null(&argv[0]))
> +		return 0;
> +	struct StrAccum *p =
> +		(struct StrAccum *) sql_aggregate_context(ctx, sizeof(*p));
> +	if (p == NULL)
> +		return -1;
> +
> +	struct sql *db = sql_get();
> +	int firstTerm = p->mxAlloc == 0;
> +	p->mxAlloc = db->aLimit[SQL_LIMIT_LENGTH];
> +	if (!firstTerm) {
> +		const char *zSep = NULL;
> +		int nSep;
> +		if (argc == 2) {
> +			zSep = (char *)sql_value_text(&argv[1]);
> +			nSep = sql_value_bytes(&argv[1]);
> +		} else {
> +			zSep = ",";
> +			nSep = 1;
>  		}
> -		zVal = (char *)sql_value_text(argv[0]);
> -		nVal = sql_value_bytes(argv[0]);
> -		if (zVal)
> -			sqlStrAccumAppend(pAccum, zVal, nVal);
> +		if (zSep != NULL)
> +			sqlStrAccumAppend(p, zSep, nSep);
>  	}
> +	const char *z = (char *)sql_value_text(&argv[0]);
> +	int n = sql_value_bytes(&argv[0]);
> +	if (z != NULL)
> +		sqlStrAccumAppend(p, z, n);
> +	return 0;
>  }
>  
> -static void
> -groupConcatFinalize(sql_context * context)
> +static int
> +sql_builtin_group_concat_finalize(struct func *func, struct port *args,
> +				  struct port *ret)
>  {
> -	StrAccum *pAccum;
> -	pAccum = sql_aggregate_context(context, 0);
> -	if (pAccum) {
> -		if (pAccum->accError == STRACCUM_TOOBIG) {
> -			diag_set(ClientError, ER_SQL_EXECUTE, "string or blob "\
> -				 "too big");
> -			context->is_aborted = true;
> -		} else if (pAccum->accError == STRACCUM_NOMEM) {
> -			context->is_aborted = true;
> -		} else {
> -			sql_result_text(context,
> -					    sqlStrAccumFinish(pAccum),
> -					    pAccum->nChar, sql_free);
> -		}
> +	(void) func;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> +	assert(ctx != NULL);
> +
> +
> +	struct StrAccum *p = sql_aggregate_context(ctx, 0);
> +	if (p == NULL)
> +		return 0;
> +	if (p->accError == STRACCUM_TOOBIG) {
> +		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob too big");
> +		return -1;
> +	} else if (p->accError == STRACCUM_NOMEM) {
> +		return -1;
> +	} else {
> +		if (sqlVdbeMemSetStr(val, sqlStrAccumFinish(p), p->nChar,
> +				     1, SQL_DYNAMIC) != 0)
> +			return -1;
>  	}
> +	return 0;
>  }
>  
>  int
> @@ -1743,66 +2064,77 @@ sqlRegisterBuiltinFunctions(void)
>  	 * For peak efficiency, put the most frequently used function last.
>  	 */
>  	static FuncDef aBuiltinFunc[] = {
> -		FUNCTION_COLL(trim, 1, 3, 0, trim_func_one_arg),
> -		FUNCTION_COLL(trim, 2, 3, 0, trim_func_two_args),
> -		FUNCTION_COLL(trim, 3, 3, 0, trim_func_three_args),
> -		FUNCTION(min, -1, 0, 1, minmaxFunc, FIELD_TYPE_SCALAR),
> +		FUNCTION_COLL(trim, 1, 3, 0, sql_builtin_trim_one_arg),
> +		FUNCTION_COLL(trim, 2, 3, 0, sql_builtin_trim_two_args),
> +		FUNCTION_COLL(trim, 3, 3, 0, sql_builtin_trim_three_args),
> +		FUNCTION(min, -1, 0, 1, sql_builtin_minmax, FIELD_TYPE_SCALAR),
>  		FUNCTION(min, 0, 0, 1, 0, FIELD_TYPE_SCALAR),
> -		AGGREGATE2(min, 1, 0, 1, minmaxStep, minMaxFinalize,
> -			   SQL_FUNC_MINMAX, FIELD_TYPE_SCALAR),
> -		FUNCTION(max, -1, 1, 1, minmaxFunc, FIELD_TYPE_SCALAR),
> +		AGGREGATE2(min, 1, 0, 1, sql_builtin_minmax_step,
> +			   sql_builtin_minmax_finalize, SQL_FUNC_MINMAX,
> +			   FIELD_TYPE_SCALAR),
> +		FUNCTION(max, -1, 1, 1, sql_builtin_minmax, FIELD_TYPE_SCALAR),
>  		FUNCTION(max, 0, 1, 1, 0, FIELD_TYPE_SCALAR),
> -		AGGREGATE2(max, 1, 1, 1, minmaxStep, minMaxFinalize,
> -			   SQL_FUNC_MINMAX, FIELD_TYPE_SCALAR),
> -		FUNCTION2(typeof, 1, 0, 0, typeofFunc, SQL_FUNC_TYPEOF,
> +		AGGREGATE2(max, 1, 1, 1, sql_builtin_minmax_step,
> +			   sql_builtin_minmax_finalize, SQL_FUNC_MINMAX,
> +			   FIELD_TYPE_SCALAR),
> +		FUNCTION2(typeof, 1, 0, 0, sql_builtin_typeof, SQL_FUNC_TYPEOF,
>  			  FIELD_TYPE_STRING),
> -		FUNCTION2(length, 1, 0, 0, lengthFunc, SQL_FUNC_LENGTH,
> +		FUNCTION2(length, 1, 0, 0, sql_builtin_length, SQL_FUNC_LENGTH,
>  			  FIELD_TYPE_INTEGER),
> -		FUNCTION(position, 2, 0, 1, position_func, FIELD_TYPE_INTEGER),
> -		FUNCTION(printf, -1, 0, 0, printfFunc, FIELD_TYPE_STRING),
> -		FUNCTION(unicode, 1, 0, 0, unicodeFunc, FIELD_TYPE_STRING),
> -		FUNCTION(char, -1, 0, 0, charFunc, FIELD_TYPE_STRING),
> -		FUNCTION(abs, 1, 0, 0, absFunc, FIELD_TYPE_NUMBER),
> -		FUNCTION(round, 1, 0, 0, roundFunc, FIELD_TYPE_INTEGER),
> -		FUNCTION(round, 2, 0, 0, roundFunc, FIELD_TYPE_INTEGER),
> -		FUNCTION_COLL(upper, 1, 0, 1, UpperICUFunc),
> -		FUNCTION_COLL(lower, 1, 0, 1, LowerICUFunc),
> -		FUNCTION(hex, 1, 0, 0, hexFunc, FIELD_TYPE_STRING),
> -		FUNCTION2(ifnull, 2, 0, 0, noopFunc, SQL_FUNC_COALESCE,
> +		FUNCTION(position, 2, 0, 1, sql_builtin_position,
> +			 FIELD_TYPE_INTEGER),
> +		FUNCTION(printf, -1, 0, 0, sql_builtin_printf,
> +			 FIELD_TYPE_STRING),
> +		FUNCTION(unicode, 1, 0, 0, sql_builtin_unicode,
> +			 FIELD_TYPE_STRING),
> +		FUNCTION(char, -1, 0, 0, sql_builtin_char, FIELD_TYPE_STRING),
> +		FUNCTION(abs, 1, 0, 0, sql_builtin_abs, FIELD_TYPE_NUMBER),
> +		FUNCTION(round, 1, 0, 0, sql_builtin_round, FIELD_TYPE_INTEGER),
> +		FUNCTION(round, 2, 0, 0, sql_builtin_round, FIELD_TYPE_INTEGER),
> +		FUNCTION_COLL(upper, 1, 0, 1, sql_builtin_ICUUpper),
> +		FUNCTION_COLL(lower, 1, 0, 1, sql_builtin_ICULower),
> +		FUNCTION(hex, 1, 0, 0, sql_builtin_hex, FIELD_TYPE_STRING),
> +		FUNCTION2(ifnull, 2, 0, 0, sql_builtin_noop, SQL_FUNC_COALESCE,
>  			  FIELD_TYPE_INTEGER),
> -		VFUNCTION(random, 0, 0, 0, randomFunc, FIELD_TYPE_INTEGER),
> -		VFUNCTION(randomblob, 1, 0, 0, randomBlob, FIELD_TYPE_SCALAR),
> -		FUNCTION(nullif, 2, 0, 1, nullifFunc, FIELD_TYPE_SCALAR),
> -		FUNCTION(version, 0, 0, 0, sql_func_version, FIELD_TYPE_STRING),
> -		FUNCTION(quote, 1, 0, 0, quoteFunc, FIELD_TYPE_STRING),
> -		VFUNCTION(row_count, 0, 0, 0, sql_row_count, FIELD_TYPE_INTEGER),
> -		FUNCTION_COLL(replace, 3, 0, 0, replaceFunc),
> -		FUNCTION(zeroblob, 1, 0, 0, zeroblobFunc, FIELD_TYPE_SCALAR),
> -		FUNCTION_COLL(substr, 2, 0, 0, substrFunc),
> -		FUNCTION_COLL(substr, 3, 0, 0, substrFunc),
> -		AGGREGATE(sum, 1, 0, 0, sum_step, sumFinalize,
> -			  FIELD_TYPE_NUMBER),
> -		AGGREGATE(total, 1, 0, 0, sum_step, totalFinalize,
> -			  FIELD_TYPE_NUMBER),
> -		AGGREGATE(avg, 1, 0, 0, sum_step, avgFinalize,
> -			  FIELD_TYPE_NUMBER),
> -		AGGREGATE2(count, 0, 0, 0, countStep, countFinalize,
> -			   SQL_FUNC_COUNT, FIELD_TYPE_INTEGER),
> -		AGGREGATE(count, 1, 0, 0, countStep, countFinalize,
> +		VFUNCTION(random, 0, 0, 0, sql_builtin_random,
> +			  FIELD_TYPE_INTEGER),
> +		VFUNCTION(randomblob, 1, 0, 0, sql_builtin_random_blob,
> +			  FIELD_TYPE_SCALAR),
> +		FUNCTION(nullif, 2, 0, 1, sql_builtin_nullif,
> +			 FIELD_TYPE_SCALAR),
> +		FUNCTION(version, 0, 0, 0, sql_builtin_version,
> +			 FIELD_TYPE_STRING),
> +		FUNCTION(quote, 1, 0, 0, sql_builtin_quote, FIELD_TYPE_STRING),
> +		VFUNCTION(row_count, 0, 0, 0, sql_builtin_row_count,
> +			  FIELD_TYPE_INTEGER),
> +		FUNCTION_COLL(replace, 3, 0, 0, sql_builtin_replace),
> +		FUNCTION(zeroblob, 1, 0, 0, sql_builtin_zeroblob,
> +			 FIELD_TYPE_SCALAR),
> +		FUNCTION_COLL(substr, 2, 0, 0, sql_builtin_substr),
> +		FUNCTION_COLL(substr, 3, 0, 0, sql_builtin_substr),
> +		AGGREGATE(sum, 1, 0, 0, sql_builtin_sum_step,
> +			  sql_builtin_sum_finalize, FIELD_TYPE_NUMBER),
> +		AGGREGATE(total, 1, 0, 0, sql_builtin_sum_step,
> +			  sql_builtin_total_finalize, FIELD_TYPE_NUMBER),
> +		AGGREGATE(avg, 1, 0, 0, sql_builtin_sum_step,
> +			  sql_builtin_avg_finalize, FIELD_TYPE_NUMBER),
> +		AGGREGATE2(count, 0, 0, 0, sql_builtin_count_step,
> +			   sql_builtin_count_finalize, SQL_FUNC_COUNT,
> +			   FIELD_TYPE_INTEGER),
> +		AGGREGATE(count, 1, 0, 0, sql_builtin_count_step,
> +			  sql_builtin_count_finalize, FIELD_TYPE_INTEGER),
> +		AGGREGATE(group_concat, 1, 0, 0, sql_builtin_group_concat_step,
> +			  sql_builtin_group_concat_finalize, FIELD_TYPE_STRING),
> +		AGGREGATE(group_concat, 2, 0, 0, sql_builtin_group_concat_step,
> +			  sql_builtin_group_concat_finalize, FIELD_TYPE_STRING),
> +		FUNCTION2(like, 2, 1, 0, sql_builtin_like, SQL_FUNC_LIKE,
> +			  FIELD_TYPE_INTEGER),
> +		FUNCTION2(like, 3, 1, 0, sql_builtin_like, SQL_FUNC_LIKE,
>  			  FIELD_TYPE_INTEGER),
> -		AGGREGATE(group_concat, 1, 0, 0, groupConcatStep,
> -			  groupConcatFinalize, FIELD_TYPE_STRING),
> -		AGGREGATE(group_concat, 2, 0, 0, groupConcatStep,
> -			  groupConcatFinalize, FIELD_TYPE_STRING),
> -
> -		LIKEFUNC(like, 2, 1, SQL_FUNC_LIKE,
> -			 FIELD_TYPE_INTEGER),
> -		LIKEFUNC(like, 3, 1, SQL_FUNC_LIKE,
> -			 FIELD_TYPE_INTEGER),
>  		FUNCTION(coalesce, 1, 0, 0, 0, FIELD_TYPE_SCALAR),
>  		FUNCTION(coalesce, 0, 0, 0, 0, FIELD_TYPE_SCALAR),
> -		FUNCTION2(coalesce, -1, 0, 0, noopFunc, SQL_FUNC_COALESCE,
> -			  FIELD_TYPE_SCALAR),
> +		FUNCTION2(coalesce, -1, 0, 0, sql_builtin_noop,
> +			  SQL_FUNC_COALESCE, FIELD_TYPE_SCALAR),
>  	};
>  	sql_register_analyze_builtins();
>  	sqlRegisterDateTimeFunctions();
> diff --git a/src/box/sql/main.c b/src/box/sql/main.c
> index 89d83d242..baaf7dcc9 100644
> --- a/src/box/sql/main.c
> +++ b/src/box/sql/main.c
> @@ -38,6 +38,7 @@
>  #include "sqlInt.h"
>  #include "vdbeInt.h"
>  #include "version.h"
> +#include "box/port.h"
>  #include "box/session.h"
>  
>  /*
> @@ -151,12 +152,18 @@ sql_initialize(void)
>  	return 0;
>  }
>  
> -void
> -sql_row_count(struct sql_context *context, MAYBE_UNUSED int unused1,
> -	      MAYBE_UNUSED sql_value **unused2)
> +int
> +sql_builtin_row_count(struct func *func, struct port *args, struct port *ret)
>  {
> -	sql *db = sql_context_db_handle(context);
> -	sql_result_int(context, db->nChange);
> +	(void) func;
> +	(void) args;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> +	struct sql *db = sql_get();
> +	sqlVdbeMemSetInt64(val, db->nChange);
> +	return 0;
>  }
>  
>  /*
> @@ -214,16 +221,15 @@ sqlRollbackAll(Vdbe * pVdbe)
>   * is returned and the mallocFailed flag cleared.
>   */
>  int
> -sqlCreateFunc(sql * db,
> -		  const char *zFunctionName,
> -		  enum field_type type,
> -		  int nArg,
> -		  int flags,
> -		  void *pUserData,
> -		  void (*xSFunc) (sql_context *, int, sql_value **),
> -		  void (*xStep) (sql_context *, int, sql_value **),
> -		  void (*xFinal) (sql_context *),
> -		  FuncDestructor * pDestructor)
> +sqlCreateFunc(struct sql * db, const char *zFunctionName, enum field_type type,
> +	      int nArg, int flags, void *pUserData,
> +	      int (*xSFunc) (struct func *func, struct port *args,
> +			     struct port *ret),
> +	      int (*xStep) (struct func *func, struct port *args,
> +			    struct port *ret),
> +	      int (*xFinal) (struct func *func, struct port *args,
> +			     struct port *ret),
> +	      struct FuncDestructor * pDestructor)
>  {
>  	FuncDef *p;
>  	int extraFlags;
> @@ -284,19 +290,17 @@ sqlCreateFunc(sql * db,
>  	return 0;
>  }
>  
> +
>  int
> -sql_create_function_v2(sql * db,
> -			   const char *zFunc,
> -			   enum field_type type,
> -			   int nArg,
> -			   int flags,
> -			   void *p,
> -			   void (*xSFunc) (sql_context *, int,
> -					   sql_value **),
> -			   void (*xStep) (sql_context *, int,
> -					  sql_value **),
> -			   void (*xFinal) (sql_context *),
> -			   void (*xDestroy) (void *))
> +sql_create_function_v2(struct sql * db, const char *zFunc, enum field_type type,
> +		       int nArg, int flags, void *p,
> +		       int (*xSFunc) (struct func *func, struct port *args,
> +				      struct port *ret),
> +		       int (*xStep) (struct func *func, struct port *args,
> +				     struct port *ret),
> +		       int (*xFinal) (struct func *func, struct port *args,
> +				      struct port *ret),
> +		       void (*xDestroy) (void *ctx))
>  {
>  	FuncDestructor *pArg = 0;
>  
> diff --git a/src/box/sql/printf.c b/src/box/sql/printf.c
> index 98372f028..b9e6fbc31 100644
> --- a/src/box/sql/printf.c
> +++ b/src/box/sql/printf.c
> @@ -10,6 +10,7 @@
>   * sql.
>   */
>  #include "sqlInt.h"
> +#include "vdbeInt.h"
>  
>  /*
>   * Conversion types fall into various categories as defined by the
> @@ -143,7 +144,7 @@ getIntArg(PrintfArguments * p)
>  {
>  	if (p->nArg <= p->nUsed)
>  		return 0;
> -	return sql_value_int64(p->apArg[p->nUsed++]);
> +	return sql_value_int64(&p->apArg[p->nUsed++]);
>  }
>  
>  static double
> @@ -151,7 +152,7 @@ getDoubleArg(PrintfArguments * p)
>  {
>  	if (p->nArg <= p->nUsed)
>  		return 0.0;
> -	return sql_value_double(p->apArg[p->nUsed++]);
> +	return sql_value_double(&p->apArg[p->nUsed++]);
>  }
>  
>  static char *
> @@ -159,7 +160,7 @@ getTextArg(PrintfArguments * p)
>  {
>  	if (p->nArg <= p->nUsed)
>  		return 0;
> -	return (char *)sql_value_text(p->apArg[p->nUsed++]);
> +	return (char *)sql_value_text(&p->apArg[p->nUsed++]);
>  }
>  
>  /*
> diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
> index 515fce3a9..ce15500e5 100644
> --- a/src/box/sql/sqlInt.h
> +++ b/src/box/sql/sqlInt.h
> @@ -263,6 +263,7 @@
>  #include <assert.h>
>  #include <stddef.h>
>  
> +struct port;
>  typedef long long int sql_int64;
>  typedef unsigned long long int sql_uint64;
>  typedef sql_int64 sql_int64;
> @@ -379,52 +380,6 @@ sql_value_is_null(sql_value *value)
>  	return sql_value_type(value) == MP_NIL;
>  }
>  
> -sql *
> -sql_context_db_handle(sql_context *);
> -
> -
> -void
> -sql_result_blob(sql_context *, const void *,
> -		    int, void (*)(void *));
> -
> -void
> -sql_result_blob64(sql_context *, const void *,
> -		      sql_uint64, void (*)(void *));
> -
> -void
> -sql_result_double(sql_context *, double);
> -
> -void
> -sql_result_int(sql_context *, int);
> -
> -void
> -sql_result_bool(struct sql_context *ctx, bool value);
> -
> -void
> -sql_result_int64(sql_context *, sql_int64);
> -
> -void
> -sql_result_null(sql_context *);
> -
> -void
> -sql_result_text(sql_context *, const char *,
> -		    int, void (*)(void *));
> -
> -void
> -sql_result_text64(sql_context *, const char *,
> -		      sql_uint64, void (*)(void *));
> -
> -void
> -sql_result_value(sql_context *,
> -		     sql_value *);
> -
> -void
> -sql_result_zeroblob(sql_context *, int n);
> -
> -int
> -sql_result_zeroblob64(sql_context *,
> -			  sql_uint64 n);
> -
>  char *
>  sql_mprintf(const char *, ...);
>  char *
> @@ -537,9 +492,8 @@ sql_randomness(int N, void *P);
>  /**
>   * Return the number of affected rows in the last SQL statement.
>   */
> -void
> -sql_row_count(struct sql_context *context, MAYBE_UNUSED int unused1,
> -	      MAYBE_UNUSED sql_value **unused2);
> +int
> +sql_builtin_row_count(struct func *func, struct port *args, struct port *ret);
>  
>  void *
>  sql_user_data(sql_context *);
> @@ -569,22 +523,15 @@ sql_initialize(void);
>  #define SQL_DETERMINISTIC    0x800
>  
>  int
> -sql_create_function_v2(sql * db,
> -			   const char *zFunctionName,
> -			   enum field_type type,
> -			   int nArg,
> -			   int flags,
> -			   void *pApp,
> -			   void (*xFunc) (sql_context *,
> -					  int,
> -					  sql_value **),
> -			   void (*xStep) (sql_context *,
> -					  int,
> -					  sql_value **),
> -			   void (*xFinal)
> -			   (sql_context *),
> -			   void (*xDestroy) (void *)
> -	);
> +sql_create_function_v2(struct sql * db, const char *zFunc, enum field_type type,
> +		       int nArg, int flags, void *p,
> +		       int (*xSFunc) (struct func *func, struct port *args,
> +				      struct port *ret),
> +		       int (*xStep) (struct func *func, struct port *args,
> +				     struct port *ret),
> +		       int (*xFinal) (struct func *func, struct port *args,
> +				      struct port *ret),
> +		       void (*xDestroy) (void *ctx));
>  
>  #define SQL_OPEN_READONLY         0x00000001	/* Ok for sql_open_v2() */
>  #define SQL_OPEN_READWRITE        0x00000002	/* Ok for sql_open_v2() */
> @@ -1109,7 +1056,7 @@ struct sql {
>  	u8 dfltLockMode;	/* Default locking-mode for attached dbs */
>  	u8 mTrace;		/* zero or more sql_TRACE flags */
>  	u32 magic;		/* Magic number for detect library misuse */
> -	/** Value returned by sql_row_count(). */
> +	/** Value returned by sql_builtin_row_count(). */
>  	int nChange;
>  	int aLimit[SQL_N_LIMIT];	/* Limits */
>  	int nMaxSorterMmap;	/* Maximum size of regions mapped by sorter */
> @@ -1219,8 +1166,8 @@ struct FuncDef {
>  	u16 funcFlags;		/* Some combination of sql_FUNC_* */
>  	void *pUserData;	/* User data parameter */
>  	FuncDef *pNext;		/* Next function with same name */
> -	void (*xSFunc) (sql_context *, int, sql_value **);	/* func or agg-step */
> -	void (*xFinalize) (sql_context *);	/* Agg finalizer */
> +	int (*xSFunc) (struct func *func, struct port *args, struct port *ret);
> +	int (*xFinalize) (struct func *func, struct port *args, struct port *ret);
>  	const char *zName;	/* SQL name of the function. */
>  	union {
>  		FuncDef *pHash;	/* Next with a different name but the same hash */
> @@ -2667,7 +2614,7 @@ int sqlIsNaN(double);
>  struct PrintfArguments {
>  	int nArg;		/* Total number of arguments */
>  	int nUsed;		/* Number of arguments used so far */
> -	sql_value **apArg;	/* The argument values */
> +	struct Mem *apArg;	/* The argument values */
>  };
>  
>  void sqlVXPrintf(StrAccum *, const char *, va_list);
> @@ -4273,12 +4220,16 @@ sql_key_info_to_key_def(struct sql_key_info *key_info);
>  int
>  sql_is_like_func(struct sql *db, struct Expr *expr, int *is_like_ci);
>  
> -int sqlCreateFunc(sql *, const char *, enum field_type,
> -		      int, int, void *,
> -		      void (*)(sql_context *, int, sql_value **),
> -		      void (*)(sql_context *, int, sql_value **),
> -		      void (*)(sql_context *),
> -		      FuncDestructor * pDestructor);
> +int
> +sqlCreateFunc(struct sql * db, const char *zFunctionName, enum field_type type,
> +	      int nArg, int flags, void *pUserData,
> +	      int (*xSFunc) (struct func *func, struct port *args,
> +			     struct port *ret),
> +	      int (*xStep) (struct func *func, struct port *args,
> +			    struct port *ret),
> +	      int (*xFinal) (struct func *func, struct port *args,
> +			     struct port *ret),
> +	      struct FuncDestructor * pDestructor);
>  
>  /** Set OOM error flag. */
>  static inline void
> diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
> index c8887f9b7..92c99877a 100644
> --- a/src/box/sql/vdbe.c
> +++ b/src/box/sql/vdbe.c
> @@ -44,6 +44,7 @@
>  #include "box/fk_constraint.h"
>  #include "box/txn.h"
>  #include "box/tuple.h"
> +#include "box/port.h"
>  #include "sqlInt.h"
>  #include "vdbeInt.h"
>  #include "tarantoolInt.h"
> @@ -1719,21 +1720,18 @@ case OP_CollSeq: {
>   * See also: Function0, AggStep, AggFinal
>   */
>  case OP_Function0: {
> -	int n;
>  	sql_context *pCtx;
>  
>  	assert(pOp->p4type==P4_FUNCDEF);
> -	n = pOp->p5;
>  	assert(pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor));
> -	assert(n==0 || (pOp->p2>0 && pOp->p2+n<=(p->nMem+1 - p->nCursor)+1));
> -	assert(pOp->p3<pOp->p2 || pOp->p3>=pOp->p2+n);
> -	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx) + (n-1)*sizeof(sql_value*));
> +	assert(pOp->p5 == 0 ||
> +	       (pOp->p2 > 0 && pOp->p2 + pOp->p5 <= (p->nMem+1 - p->nCursor)+1));
> +	assert(pOp->p3 < pOp->p2 || pOp->p3>=pOp->p2 + pOp->p5);
> +	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx));
>  	if (pCtx==0) goto no_mem;
> -	pCtx->pOut = 0;
>  	pCtx->pFunc = pOp->p4.pFunc;
>  	pCtx->iOp = (int)(pOp - aOp);
>  	pCtx->pVdbe = p;
> -	pCtx->argc = n;
>  	pOp->p4type = P4_FUNCCTX;
>  	pOp->p4.pCtx = pCtx;
>  	pOp->opcode = OP_Function;
> @@ -1741,7 +1739,6 @@ case OP_Function0: {
>  	FALLTHROUGH;
>  }
>  case OP_Function: {
> -	int i;
>  	sql_context *pCtx;
>  
>  	assert(pOp->p4type==P4_FUNCCTX);
> @@ -1753,33 +1750,39 @@ case OP_Function: {
>  	 * reinitializes the relavant parts of the sql_context object
>  	 */
>  	pOut = &aMem[pOp->p3];
> -	if (pCtx->pOut != pOut) {
> -		pCtx->pOut = pOut;
> -		for(i=pCtx->argc-1; i>=0; i--) pCtx->argv[i] = &aMem[pOp->p2+i];
> -	}
> +	struct region *region = &fiber()->gc;
> +	size_t region_svp = region_used(region);
> +	struct port args, ret;
> +	port_vdbemem_create(&args, (struct sql_value *)&aMem[pOp->p2],
> +			    pOp->p5, pCtx);
>  
> -	memAboutToChange(p, pCtx->pOut);
> +	memAboutToChange(p, pOut);
>  #ifdef SQL_DEBUG
> -	for(i=0; i<pCtx->argc; i++) {
> -		assert(memIsValid(pCtx->argv[i]));
> -		REGISTER_TRACE(p, pOp->p2+i, pCtx->argv[i]);
> +	for(int i = 0; i < pOp->p5; i++) {
> +		assert(memIsValid(&aMem[pOp->p2 + i]));
> +		REGISTER_TRACE(p, pOp->p2+i, &aMem[pOp->p2 + i]);
>  	}
>  #endif
> -	MemSetTypeFlag(pCtx->pOut, MEM_Null);
> -	pCtx->is_aborted = false;
> -	(*pCtx->pFunc->xSFunc)(pCtx, pCtx->argc, pCtx->argv);/* IMP: R-24505-23230 */
> -
> -	/* If the function returned an error, throw an exception */
> -	if (pCtx->is_aborted)
> +	MemSetTypeFlag(pOut, MEM_Null);
> +	rc = (*pCtx->pFunc->xSFunc)(NULL, &args, &ret);
> +	if (rc != 0)
> +		goto abort_due_to_error;
> +	uint32_t size;
> +	struct Mem *mem = (struct Mem *)port_get_vdbemem(&ret, &size);
> +	if (mem == NULL) {
> +		region_truncate(region, region_svp);
>  		goto abort_due_to_error;
> +	}
> +	*pOut = mem[0];
> +	region_truncate(region, region_svp);
>  
>  	/* Copy the result of the function into register P3 */
>  	if (pOut->flags & (MEM_Str|MEM_Blob)) {
> -		if (sqlVdbeMemTooBig(pCtx->pOut)) goto too_big;
> +		if (sqlVdbeMemTooBig(pOut)) goto too_big;
>  	}
>  
> -	REGISTER_TRACE(p, pOp->p3, pCtx->pOut);
> -	UPDATE_MAX_BLOBSIZE(pCtx->pOut);
> +	REGISTER_TRACE(p, pOp->p3, pOut);
> +	UPDATE_MAX_BLOBSIZE(pOut);
>  	break;
>  }
>  
> @@ -4964,21 +4967,19 @@ case OP_DecrJumpZero: {      /* jump, in1 */
>   * step function.
>   */
>  case OP_AggStep0: {
> -	int n;
>  	sql_context *pCtx;
>  
> -	assert(pOp->p4type==P4_FUNCDEF);
> -	n = pOp->p5;
> +	assert(pOp->p4type == P4_FUNCDEF);
>  	assert(pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor));
> -	assert(n==0 || (pOp->p2>0 && pOp->p2+n<=(p->nMem+1 - p->nCursor)+1));
> -	assert(pOp->p3<pOp->p2 || pOp->p3>=pOp->p2+n);
> -	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx) + (n-1)*sizeof(sql_value*));
> +	assert(pOp->p5 == 0 ||
> +	       (pOp->p2 > 0 && pOp->p2 + pOp->p5 <= (p->nMem+1 - p->nCursor)+1));
> +	assert(pOp->p3 < pOp->p2 || pOp->p3 >= pOp->p2 + pOp->p5);
> +	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx));
>  	if (pCtx==0) goto no_mem;
>  	pCtx->pMem = 0;
>  	pCtx->pFunc = pOp->p4.pFunc;
>  	pCtx->iOp = (int)(pOp - aOp);
>  	pCtx->pVdbe = p;
> -	pCtx->argc = n;
>  	pOp->p4type = P4_FUNCCTX;
>  	pOp->p4.pCtx = pCtx;
>  	pOp->opcode = OP_AggStep;
> @@ -4988,41 +4989,33 @@ case OP_AggStep0: {
>  case OP_AggStep: {
>  	int i;
>  	sql_context *pCtx;
> -	Mem *pMem;
>  	Mem t;
>  
>  	assert(pOp->p4type==P4_FUNCCTX);
>  	pCtx = pOp->p4.pCtx;
> -	pMem = &aMem[pOp->p3];
> +	pCtx->pMem = &aMem[pOp->p3];
>  
> -	/* If this function is inside of a trigger, the register array in aMem[]
> -	 * might change from one evaluation to the next.  The next block of code
> -	 * checks to see if the register array has changed, and if so it
> -	 * reinitializes the relavant parts of the sql_context object
> -	 */
> -	if (pCtx->pMem != pMem) {
> -		pCtx->pMem = pMem;
> -		for(i=pCtx->argc-1; i>=0; i--) pCtx->argv[i] = &aMem[pOp->p2+i];
> -	}
> +	struct region *region = &fiber()->gc;
> +	size_t region_svp = region_used(region);
> +	struct port args, ret;
> +	port_vdbemem_create(&args, (struct sql_value *)&aMem[pOp->p2],
> +			    pOp->p5, pCtx);
>  
>  #ifdef SQL_DEBUG
> -	for(i=0; i<pCtx->argc; i++) {
> -		assert(memIsValid(pCtx->argv[i]));
> -		REGISTER_TRACE(p, pOp->p2+i, pCtx->argv[i]);
> +	for(i = 0; i < pOp->p5; i++) {
> +		assert(memIsValid(&aMem[pOp->p2 + i]));
> +		REGISTER_TRACE(p, pOp->p2+i, &aMem[pOp->p2 + i]);
>  	}
>  #endif
>  
> -	pMem->n++;
>  	sqlVdbeMemInit(&t, db, MEM_Null);
> -	pCtx->pOut = &t;
> -	pCtx->is_aborted = false;
> +	pOut = &t;
>  	pCtx->skipFlag = 0;
> -	(pCtx->pFunc->xSFunc)(pCtx,pCtx->argc,pCtx->argv); /* IMP: R-24505-23230 */
> -	if (pCtx->is_aborted) {
> -		sqlVdbeMemRelease(&t);
> +	rc = (*pCtx->pFunc->xSFunc)(NULL, &args, &ret);
> +	region_truncate(region, region_svp);
> +	if (rc != 0)
>  		goto abort_due_to_error;
> -	}
> -	assert(t.flags==MEM_Null);
> +
>  	if (pCtx->skipFlag) {
>  		assert(pOp[-1].opcode==OP_CollSeq);
>  		i = pOp[-1].p1;
> diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
> index 6bfeecc85..4c8363a22 100644
> --- a/src/box/sql/vdbeInt.h
> +++ b/src/box/sql/vdbeInt.h
> @@ -181,10 +181,7 @@ struct Mem {
>  	u32 uTemp;		/* Transient storage for serial_type in OP_MakeRecord */
>  	sql *db;		/* The associated database connection */
>  	void (*xDel) (void *);	/* Destructor for Mem.z - only valid if MEM_Dyn */
> -#ifdef SQL_DEBUG
>  	Mem *pScopyFrom;	/* This Mem is a shallow copy of pScopyFrom */
> -	void *pFiller;		/* So that sizeof(Mem) is a multiple of 8 */
> -#endif
>  };
>  
>  /*
> @@ -294,19 +291,11 @@ mem_apply_numeric_type(struct Mem *record);
>   * (Mem) which are only defined there.
>   */
>  struct sql_context {
> -	Mem *pOut;		/* The return value is stored here */
>  	FuncDef *pFunc;		/* Pointer to function information */
>  	Mem *pMem;		/* Memory cell used to store aggregate context */
>  	Vdbe *pVdbe;		/* The VM that owns this context */
>  	int iOp;		/* Instruction number of OP_Function */
> -	/*
> -	 * True, if an error occurred during the execution of the
> -	 * function.
> -	 */
> -	bool is_aborted;
>  	u8 skipFlag;		/* Skip accumulator loading if true */
> -	u8 argc;		/* Number of arguments */
> -	sql_value *argv[1];	/* Argument set */
>  };
>  
>  /* A bitfield type for use inside of structures.  Always follow with :N where
> @@ -584,4 +573,11 @@ char *
>  sql_vdbe_mem_encode_tuple(struct Mem *fields, uint32_t field_count,
>  			  uint32_t *tuple_size, struct region *region);
>  
> +/**
> + * Allocate a sequence of initialized vdbe memory registers
> + * on region.
> + */
> +struct Mem *
> +vdbemem_alloc_on_region(uint32_t count);
> +
>  #endif				/* !defined(SQL_VDBEINT_H) */
> diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
> index ef0ab79d2..d014e8258 100644
> --- a/src/box/sql/vdbeapi.c
> +++ b/src/box/sql/vdbeapi.c
> @@ -69,6 +69,23 @@ invokeProfileCallback(sql * db, Vdbe * p)
>  #define checkProfileCallback(DB,P) \
>     if( ((P)->startTime)>0 ){ invokeProfileCallback(DB,P); }
>  
> +struct Mem *
> +vdbemem_alloc_on_region(uint32_t count)
> +{
> +	struct region *region = &fiber()->gc;
> +	struct Mem *ret = region_alloc(region, count * sizeof(*ret));
> +	if (ret == NULL) {
> +		diag_set(OutOfMemory, count * sizeof(*ret), "sqlValueNew", "ret");
> +		return NULL;
> +	}
> +	memset(ret, 0, count * sizeof(*ret));
> +	for (uint32_t i = 0; i < count; i++) {
> +		sqlVdbeMemInit(&ret[i], sql_get(), MEM_Null);
> +		assert(memIsValid(&ret[i]));
> +	}
> +	return ret;
> +}
> +
>  /*
>   * The following routine destroys a virtual machine that is created by
>   * the sql_compile() routine. The integer returned is an SQL_
> @@ -254,22 +271,9 @@ sql_value_free(sql_value * pOld)
>   * The invokeValueDestructor(P,X) routine invokes destructor function X()
>   * on value P is not going to be used and need to be destroyed.
>   */
> -static void
> -setResultStrOrError(sql_context * pCtx,	/* Function context */
> -		    const char *z,	/* String pointer */
> -		    int n,	/* Bytes in string, or negative */
> -		    void (*xDel) (void *)	/* Destructor function */
> -    )
> -{
> -	if (sqlVdbeMemSetStr(pCtx->pOut, z, n, 1, xDel) != 0)
> -		pCtx->is_aborted = true;
> -}
>  
>  static int
> -invokeValueDestructor(const void *p,	/* Value to destroy */
> -		      void (*xDel) (void *),	/* The destructor */
> -		      sql_context *pCtx	/* Set an error if no NULL */
> -    )
> +invokeValueDestructor(const void *p, void (*xDel) (void *))
>  {
>  	assert(xDel != SQL_DYNAMIC);
>  	if (xDel == 0) {
> @@ -279,114 +283,9 @@ invokeValueDestructor(const void *p,	/* Value to destroy */
>  	} else {
>  		xDel((void *)p);
>  	}
> -	if (pCtx) {
> -		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob is too "\
> -			 "big");
> -		pCtx->is_aborted = true;
> -	}
>  	return -1;
>  }
>  
> -void
> -sql_result_blob(sql_context * pCtx,
> -		    const void *z, int n, void (*xDel) (void *)
> -    )
> -{
> -	assert(n >= 0);
> -	if (sqlVdbeMemSetStr(pCtx->pOut, z, n, 0, xDel) != 0)
> -		pCtx->is_aborted = true;
> -}
> -
> -void
> -sql_result_blob64(sql_context * pCtx,
> -		      const void *z, sql_uint64 n, void (*xDel) (void *)
> -    )
> -{
> -	assert(xDel != SQL_DYNAMIC);
> -	if (n > 0x7fffffff) {
> -		(void)invokeValueDestructor(z, xDel, pCtx);
> -	} else {
> -		setResultStrOrError(pCtx, z, (int)n, xDel);
> -	}
> -}
> -
> -void
> -sql_result_double(sql_context * pCtx, double rVal)
> -{
> -	sqlVdbeMemSetDouble(pCtx->pOut, rVal);
> -}
> -
> -void
> -sql_result_int(sql_context * pCtx, int iVal)
> -{
> -	sqlVdbeMemSetInt64(pCtx->pOut, (i64) iVal);
> -}
> -
> -void
> -sql_result_bool(struct sql_context *ctx, bool value)
> -{
> -	mem_set_bool(ctx->pOut, value);
> -}
> -
> -void
> -sql_result_int64(sql_context * pCtx, i64 iVal)
> -{
> -	sqlVdbeMemSetInt64(pCtx->pOut, iVal);
> -}
> -
> -void
> -sql_result_null(sql_context * pCtx)
> -{
> -	sqlVdbeMemSetNull(pCtx->pOut);
> -}
> -
> -void
> -sql_result_text(sql_context * pCtx,
> -		    const char *z, int n, void (*xDel) (void *)
> -    )
> -{
> -	setResultStrOrError(pCtx, z, n, xDel);
> -}
> -
> -void
> -sql_result_text64(sql_context * pCtx,
> -		      const char *z,
> -		      sql_uint64 n,
> -		      void (*xDel) (void *))
> -{
> -	assert(xDel != SQL_DYNAMIC);
> -	if (n > 0x7fffffff) {
> -		(void)invokeValueDestructor(z, xDel, pCtx);
> -	} else {
> -		setResultStrOrError(pCtx, z, (int)n, xDel);
> -	}
> -}
> -
> -void
> -sql_result_value(sql_context * pCtx, sql_value * pValue)
> -{
> -	sqlVdbeMemCopy(pCtx->pOut, pValue);
> -}
> -
> -void
> -sql_result_zeroblob(sql_context * pCtx, int n)
> -{
> -	sqlVdbeMemSetZeroBlob(pCtx->pOut, n);
> -}
> -
> -int
> -sql_result_zeroblob64(sql_context * pCtx, u64 n)
> -{
> -	Mem *pOut = pCtx->pOut;
> -	if (n > (u64) pOut->db->aLimit[SQL_LIMIT_LENGTH]) {
> -		diag_set(ClientError, ER_SQL_EXECUTE, "string or blob is too "\
> -			 "big");
> -		return -1;
> -	}
> -	sqlVdbeMemSetZeroBlob(pCtx->pOut, (int)n);
> -	return 0;
> -}
> -
>  /*
>   * Execute the statement pStmt, either until a row of data is ready, the
>   * statement is completely executed or an error occurs.
> @@ -475,23 +374,6 @@ sql_user_data(sql_context * p)
>  	return p->pFunc->pUserData;
>  }
>  
> -/*
> - * Extract the user data from a sql_context structure and return a
> - * pointer to it.
> - *
> - * IMPLEMENTATION-OF: R-46798-50301 The sql_context_db_handle() interface
> - * returns a copy of the pointer to the database connection (the 1st
> - * parameter) of the sql_create_function() and
> - * sql_create_function16() routines that originally registered the
> - * application defined function.
> - */
> -sql *
> -sql_context_db_handle(sql_context * p)
> -{
> -	assert(p && p->pOut);
> -	return p->pOut->db;
> -}
> -
>  /*
>   * Return the current time for a statement.  If the current time
>   * is requested more than once within the same run of a single prepared
> @@ -512,7 +394,7 @@ sqlStmtCurrentTime(sql_context * p)
>  	    p->pVdbe != 0 ? &p->pVdbe->iCurrentTime : &iTime;
>  #endif
>  	if (*piTime == 0) {
> -		rc = sqlOsCurrentTimeInt64(p->pOut->db->pVfs, piTime);
> +		rc = sqlOsCurrentTimeInt64(sql_get()->pVfs, piTime);
>  		if (rc)
>  			*piTime = 0;
>  	}
> @@ -614,10 +496,7 @@ columnNullValue(void)
>  		    /* .uTemp      = */ (u32) 0,
>  		    /* .db         = */ (sql *) 0,
>  		    /* .xDel       = */ (void (*)(void *))0,
> -#ifdef SQL_DEBUG
>  		    /* .pScopyFrom = */ (Mem *) 0,
> -		    /* .pFiller    = */ (void *)0,
> -#endif
>  	};
>  	return &nullMem;
>  }
> @@ -934,7 +813,7 @@ sql_bind_blob64(sql_stmt * pStmt,
>  {
>  	assert(xDel != SQL_DYNAMIC);
>  	if (nData > 0x7fffffff) {
> -		return invokeValueDestructor(zData, xDel, 0);
> +		return invokeValueDestructor(zData, xDel);
>  	} else {
>  		return sql_bind_blob(pStmt, i, zData, (int)nData, xDel);
>  	}
> @@ -1017,7 +896,7 @@ sql_bind_text64(sql_stmt * pStmt,
>  {
>  	assert(xDel != SQL_DYNAMIC);
>  	if (nData > 0x7fffffff) {
> -		return invokeValueDestructor(zData, xDel, 0);
> +		return invokeValueDestructor(zData, xDel);
>  	} else {
>  		return bindText(pStmt, i, zData, (int)nData, xDel);
>  	}
> diff --git a/src/box/sql/vdbemem.c b/src/box/sql/vdbemem.c
> index f52035b0e..7bd5cdd71 100644
> --- a/src/box/sql/vdbemem.c
> +++ b/src/box/sql/vdbemem.c
> @@ -41,6 +41,7 @@
>  #include "tarantoolInt.h"
>  #include "box/schema.h"
>  #include "box/tuple.h"
> +#include "box/port.h"
>  #include "mpstream.h"
>  
>  #ifdef SQL_DEBUG
> @@ -315,26 +316,31 @@ sqlVdbeMemStringify(Mem * pMem, u8 bForce)
>  int
>  sqlVdbeMemFinalize(Mem * pMem, FuncDef * pFunc)
>  {
> +	int rc = 0;
>  	if (ALWAYS(pFunc && pFunc->xFinalize)) {
> -		sql_context ctx;
> -		Mem t;
>  		assert((pMem->flags & MEM_Null) != 0 || pFunc == pMem->u.pDef);
> +		struct sql_context ctx;
>  		memset(&ctx, 0, sizeof(ctx));
> -		memset(&t, 0, sizeof(t));
> -		t.flags = MEM_Null;
> -		t.db = pMem->db;
> -		ctx.pOut = &t;
>  		ctx.pMem = pMem;
>  		ctx.pFunc = pFunc;
> -		pFunc->xFinalize(&ctx);	/* IMP: R-24505-23230 */
> +		struct region *region = &fiber()->gc;
> +		size_t region_svp = region_used(region);
> +		struct port args, ret;
> +		port_vdbemem_create(&args, NULL, 0, &ctx);
> +		rc = pFunc->xFinalize(NULL, &args, &ret);
>  		assert((pMem->flags & MEM_Dyn) == 0);
>  		if (pMem->szMalloc > 0)
>  			sqlDbFree(pMem->db, pMem->zMalloc);
> -		memcpy(pMem, &t, sizeof(t));
> -		if (ctx.is_aborted)
> -			return -1;
> +		uint32_t size;
> +		struct Mem *mem = (struct Mem *)port_get_vdbemem(&ret, &size);
> +		if (mem != NULL) {
> +			*pMem = mem[0];
> +		} else {
> +			rc = -1;
> +		}
> +		region_truncate(region, region_svp);
>  	}
> -	return 0;
> +	return rc;
>  }
>  
>  /*
> @@ -1239,7 +1245,6 @@ valueFromFunction(sql * db,	/* The database connection */
>  		  struct ValueNewStat4Ctx *pCtx	/* Second argument for valueNew() */
>      )
>  {
> -	sql_context ctx;	/* Context object for function invocation */
>  	sql_value **apVal = 0;	/* Function arguments */
>  	int nVal = 0;		/* Size of apVal[] array */
>  	FuncDef *pFunc = 0;	/* Function definition */
> @@ -1260,6 +1265,8 @@ valueFromFunction(sql * db,	/* The database connection */
>  	    ) {
>  		return 0;
>  	}
> +	struct region *region = &fiber()->gc;
> +	size_t region_svp = region_used(region);
>  
>  	if (pList) {
>  		apVal =
> @@ -1285,15 +1292,25 @@ valueFromFunction(sql * db,	/* The database connection */
>  	}
>  
>  	assert(!pCtx->pParse->is_aborted);
> -	memset(&ctx, 0, sizeof(ctx));
> -	ctx.pOut = pVal;
> -	ctx.pFunc = pFunc;
> -	pFunc->xSFunc(&ctx, nVal, apVal);
> -	assert(!ctx.is_aborted);
> +
> +	struct port args, ret;
> +	port_vdbemem_create(&args, (struct sql_value *)apVal, nVal, NULL);
> +	if ((*pFunc->xSFunc)(NULL, &args, &ret) != 0) {
> +		rc = -1;
> +		goto value_from_function_out;
> +	}
>  	sql_value_apply_type(pVal, type);
>  	assert(rc == 0);
> +	uint32_t size;
> +	struct Mem *mem = (struct Mem *)port_get_vdbemem(&ret, &size);
> +	if (mem == NULL) {
> +		rc = -1;
> +		goto value_from_function_out;
> +	}
> +	*pVal = mem[0];
>  
>   value_from_function_out:
> +	region_truncate(region, region_svp);
>  	if (rc != 0)
>  		pVal = 0;
>  	if (apVal) {
> diff --git a/src/lib/core/port.h b/src/lib/core/port.h
> index 09a026df5..cd8cd31f6 100644
> --- a/src/lib/core/port.h
> +++ b/src/lib/core/port.h
> @@ -40,6 +40,7 @@ extern "C" {
>  struct obuf;
>  struct lua_State;
>  struct port;
> +struct sql_value;
>  
>  /**
>   * A single port represents a destination of any output. One such
> @@ -98,6 +99,24 @@ struct port_vtab {
>  	 * is responsible for cleaning up.
>  	 **/
>  	const char *(*get_msgpack)(struct port *port, uint32_t *size);
> +	/**
> +	 * Get the content of a port as a sequence of vdbe memory
> +	 * variables. The SQL VDBE uses this representation for
> +	 * process data. This API is usefull to pass VDBE
> +	 * variables to sql builtin functions.
> +	 * The lifecycle of the returned value is
> +	 * implementation-specific: it may either be returned
> +	 * directly from the port, in which case the data will
> +	 * stay alive as long as the port is alive, or it may be
> +	 * allocated on the fiber()->gc, in which case the caller
> +	 * is responsible for cleaning up.
> +	 */
> +	struct sql_value *(*get_vdbemem)(struct port *port, uint32_t *size);
> +	/**
> +	 * Get additional implementation-specific information
> +	 * stored in port. Used for aggregate functions in SQL.
> +	 */
> +	void *(*get_context)(struct port *port);
>  	/** Destroy a port and release associated resources. */
>  	void (*destroy)(struct port *port);
>  };
> @@ -150,6 +169,18 @@ port_get_msgpack(struct port *port, uint32_t *size)
>  	return port->vtab->get_msgpack(port, size);
>  }
>  
> +static inline struct sql_value *
> +port_get_vdbemem(struct port *port, uint32_t *size)
> +{
> +	return port->vtab->get_vdbemem(port, size);
> +}
> +
> +static inline const void *
> +port_get_context(struct port *port)
> +{
> +	return port->vtab->get_context(port);
> +}
> +
>  #if defined(__cplusplus)
>  } /* extern "C" */
>  #endif /* defined __cplusplus */
> -- 
> 2.21.0
> 

-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 02/12] sql: get rid of LIKELY, UNLIKELY and LIKEHOOD
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 02/12] sql: get rid of LIKELY, UNLIKELY and LIKEHOOD Kirill Shcherbatov
@ 2019-07-10 19:02   ` Konstantin Osipov
  2019-07-11  7:38     ` Kirill Shcherbatov
  0 siblings, 1 reply; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 19:02 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> Got rid of LIKELY, UNLIKELY and LIKEHOOD builtins because they
> are not a part of the Standard and make source code more
> complicated.

Ugh, I don't mind removing them, but for a non-reason?

There exist for a reason, especially in absence of statistics,
they allow the optimizer to guess the amount of rows a WHERE
clause returns.

Why did you get angry at them?


-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 03/12] sql: put analyze helpers to FuncDef cache
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 03/12] sql: put analyze helpers to FuncDef cache Kirill Shcherbatov
@ 2019-07-10 19:04   ` Konstantin Osipov
  2019-07-12  8:47   ` Kirill Yukhin
  1 sibling, 0 replies; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 19:04 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> Previously analyze functions refer to statically defined
> service FuncDef context. We need to change this approach due we
> going to rework the builtins functions machinery in following
> patches.

lgtm, but please ask Nikita to give another review.
> Needed for #4113, #2200, #2233

-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 04/12] sql: rework LIKE case-insensitive mode
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 04/12] sql: rework LIKE case-insensitive mode Kirill Shcherbatov
@ 2019-07-10 19:09   ` Konstantin Osipov
  0 siblings, 0 replies; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 19:09 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> The case_sensitive_like pragma used to process a strange way: on
> each call a new instance of function having appropriate
> properties was created. Reworked legacy code with user session
> object to define this property there. The new approach allows
> to rework sql builtins machinery with further patches.

Querying like case sensitivity from the current session flags is
extremely ugly, but this is being addressed in a separate commit
by Roman.

So LGTM.

-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 05/12] sql: replace bool is_derived_coll marker with flag
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 05/12] sql: replace bool is_derived_coll marker with flag Kirill Shcherbatov
@ 2019-07-10 19:10   ` Konstantin Osipov
  2019-07-12  8:48   ` Kirill Yukhin
  1 sibling, 0 replies; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 19:10 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> Introduce a new flag SQL_FUNC_DERIVEDCOLL for function that may
> require collation to be applied on its result instead of separate
> boolean variable. This is required to get rid of FuncDef in
> further patches.
> 
> Needed for #4113, #2200, #2233

LGTM. Please solicit Nikita's review.


-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 06/12] sql: remove SQL_PreferBuiltin flag
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 06/12] sql: remove SQL_PreferBuiltin flag Kirill Shcherbatov
@ 2019-07-10 19:11   ` Konstantin Osipov
  0 siblings, 0 replies; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 19:11 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> The SQL_PreferBuiltin flag is redundant (because builtin names
> are forbidden for UDFs) so we may to remove it.


Please write a docbot request documenting we always prefer
built-ins to UDFs.


-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 08/12] sql: rfc for SQL and Lua functions
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 08/12] sql: rfc for SQL and Lua functions Kirill Shcherbatov
@ 2019-07-10 19:17   ` Konstantin Osipov
  2019-07-10 19:18     ` Konstantin Osipov
  2019-07-11 13:59   ` Kirill Yukhin
  1 sibling, 1 reply; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 19:17 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> Part of #4182
> +4. name: ``stuid``\

setuid

> +7. name: ``routine_type``\
> +   type: ``string``\
> +   default: ``FUNCTION``\
> +   Type of registered object: `FUNCTION` or `PROCEDURE`; The procedure is a function that never returns result.

> +
> +The function creation process is also could be unsafe. To

is also -> also

> assemble a new function object, we must evaluate an expression
> given by use, that could be `body = 'fiber.yield()'` instead of
supplied by a user, 
that -> which 
> `body = 'function() return fiber.yield() end`. Therefore an
> assemble of a function object require own empty sandbox.

require -> requires
own -> an own

> +SQL defines some names for builtins. They are:
> +```
> +   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, GREATER, LESSER
> +```
> +(the functions are currently not implemented in Tarantool but must be implemented in future)
> +
> +We must forbid such names for `box.schema.func.create` endpoint to prevent a mess in SQL code.
> +To solve this problem, we may set `collation = 'unicode_ci'` for ``_func.name`` field and put all built-ins in bootstrap image.
> +
> +We also reserve service SQL method names
> +```
> + _sql_stat_get, _sql_stat_push, _sql_stat_init
> +```

What if I create TyPeOf function? Will this be allowed?

The spec generally looks good to me, but I don't see how you
addressed the case sensitivity issue. Please add name_ucase field
to the _func definition, and add a unique index over it, to
prevent duplicates with a differently cased name.

-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 08/12] sql: rfc for SQL and Lua functions
  2019-07-10 19:17   ` [tarantool-patches] " Konstantin Osipov
@ 2019-07-10 19:18     ` Konstantin Osipov
  2019-07-11  7:40       ` Kirill Shcherbatov
  0 siblings, 1 reply; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 19:18 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Konstantin Osipov <kostja@tarantool.org> [19/07/10 22:17]:
> What if I create TyPeOf function? Will this be allowed?
> 
> The spec generally looks good to me, but I don't see how you
> addressed the case sensitivity issue. Please add name_ucase field
> to the _func definition, and add a unique index over it, to
> prevent duplicates with a differently cased name.

Sorry, forget this. Hit "send" too hastily. I noticed that you
changed the unique index collation on "name" to case insensitive.
Please mention this in the spec, I missed that at first. After
that the spec is OK to push.


-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 09/12] box: introduce Lua persistent functions
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 09/12] box: introduce Lua persistent functions Kirill Shcherbatov
@ 2019-07-10 19:26   ` Konstantin Osipov
  2019-07-12 21:49   ` Konstantin Osipov
  1 sibling, 0 replies; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 19:26 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> 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"

Why do you need these includes? Please watch for unnecessary
includes when you do a self-review.

>  #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
> +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 ";

load_prefix, pref is ambiguous

The patch is LGTM. I would take another couple of hours to review
it carefully and tidy up - including the comments. 
Unfortunately I don't have them the moment. I hope Vova can take
the patch from here.

-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 12/12] sql: use schema's func hash instead of FuncDef hash
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 12/12] sql: use schema's func hash instead of FuncDef hash Kirill Shcherbatov
@ 2019-07-10 20:22   ` Konstantin Osipov
  0 siblings, 0 replies; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-10 20:22 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:

Can you keep all functions in the same cache but avoid calling
them using the same calling convention?

I see value in killing func_def cache, but I hate adamantly the
fact that the built-ins now depend on port object. It should be
possible to call a built-in without messing with a port.

> Part of #2200
> Closes #4113
> Closes #2233
> 
> @TarantoolBot document
> Title: Reworked SQL functions machinery
> 
> Now all functions in Tarantool use uniform API and calling
> convention. Therefore it is possible to call C and Lua functions
> from SQL.
> 
> To make a function available in SQL, you need to define it
> with list of types of it's arguments 'param_list', the type
> of returned value 'returns' and mention 'SQL' language in
> exports parameter. Deterministic functions must be defined with
> is_deterministic = true (the query planner use this information
> to build an effective vdbe code).
> 
> Example:
> -- C function is called in SQL
> box.schema.func.create("function1.divide", {language = 'C',
>                        returns = 'number', param_list = {'number', 'number'},
>                        is_deterministic = true, exports = {'LUA', 'SQL'}})
> box.execute('SELECT "function1.divide"(6, 3)')
> ---
> - metadata:
>   - name: '"function1.divide"(6, 3)'
>     type: number
>   rows:
>   - [2]
> ...
> 
> -- Persistent Lua function is called in SQL
> box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number',
> 			body = 'function (a, b) return a + b end',
> 			is_deterministic = true,
> 			param_list = {'number', 'number'},
> 			exports = {'LUA', 'SQL'}})
> box.execute('SELECT summarize(1, 2)')
> ---
> - metadata:
>   - name: summarize(1, 2)
>     type: number
>   rows:
>   - [3]
> ...
> 
> Moreover there is a predefined Lua function "LUA" that allows to
> call Lua code in SQL. The argument of LUA function is a valid
> Lua code that returns some scalar type.
> 
> Example:
> box.execute('SELECT lua(\'return box.cfg.memtx_memory\')')
> ---
> - metadata:
>   - name: lua('return box.cfg.memtx_memory')
>     type: any
>   rows:
>   - [268435456]
> ...
> ---
>  src/box/CMakeLists.txt          |   1 -
>  src/box/alter.cc                |   1 +
>  src/box/func.c                  |   3 +-
>  src/box/func.h                  |   5 +
>  src/box/func_def.c              |  19 +
>  src/box/func_def.h              |   2 +
>  src/box/lua/call.c              |   8 +-
>  src/box/lua/lua_sql.c           | 217 ----------
>  src/box/lua/lua_sql.h           |  39 --
>  src/box/lua/schema.lua          |   4 +-
>  src/box/port.c                  |   5 +-
>  src/box/sql.h                   |  13 +
>  src/box/sql/analyze.c           |  40 +-
>  src/box/sql/callback.c          | 208 ----------
>  src/box/sql/date.c              |  28 --
>  src/box/sql/expr.c              |  51 ++-
>  src/box/sql/func.c              | 697 +++++++++++++++++++++++++-------
>  src/box/sql/global.c            |   7 -
>  src/box/sql/main.c              | 134 ------
>  src/box/sql/resolve.c           |  55 ++-
>  src/box/sql/select.c            |  10 +-
>  src/box/sql/sqlInt.h            | 180 +--------
>  src/box/sql/vdbe.c              |  19 +-
>  src/box/sql/vdbe.h              |   6 +-
>  src/box/sql/vdbeInt.h           |  20 +-
>  src/box/sql/vdbeapi.c           |  11 +-
>  src/box/sql/vdbeaux.c           |  31 +-
>  src/box/sql/vdbemem.c           |  73 ++--
>  src/box/sql/whereexpr.c         |   4 +-
>  src/lib/coll/coll.c             |   1 +
>  test/box/function1.result       | 119 ++++++
>  test/box/function1.test.lua     |  35 ++
>  test/sql-tap/alias.test.lua     |  11 +-
>  test/sql-tap/check.test.lua     |  13 +-
>  test/sql-tap/func5.test.lua     |  29 +-
>  test/sql-tap/lua_sql.test.lua   | 120 +++---
>  test/sql-tap/subquery.test.lua  |  21 +-
>  test/sql-tap/trigger9.test.lua  |   6 +-
>  test/sql-tap/where2.test.lua    |   4 +-
>  test/sql/errinj.result          |  25 --
>  test/sql/errinj.test.lua        |  10 -
>  test/sql/func-recreate.result   |  41 +-
>  test/sql/func-recreate.test.lua |  28 +-
>  43 files changed, 1109 insertions(+), 1245 deletions(-)
>  delete mode 100644 src/box/lua/lua_sql.c
>  delete mode 100644 src/box/lua/lua_sql.h
> 
> diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
> index 481842a39..a0160912d 100644
> --- a/src/box/CMakeLists.txt
> +++ b/src/box/CMakeLists.txt
> @@ -128,7 +128,6 @@ add_library(box STATIC
>      ${lua_sources}
>      lua/init.c
>      lua/call.c
> -    lua/lua_sql.c
>      lua/cfg.cc
>      lua/console.c
>      lua/tuple.c
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index c92a1f710..cd85552a2 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -2786,6 +2786,7 @@ func_def_new_from_tuple(struct tuple *tuple)
>  		def->exports.lua = true;
>  		def->param_count = 0;
>  	}
> +	def->sql_flags = 0;
>  	def_guard.is_active = false;
>  	return def;
>  }
> diff --git a/src/box/func.c b/src/box/func.c
> index e36649d15..6e7d04f25 100644
> --- a/src/box/func.c
> +++ b/src/box/func.c
> @@ -34,13 +34,13 @@
>  #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"
>  #include "session.h"
> +#include "small/region.h"
>  #include <dlfcn.h>
>  
>  /**
> @@ -438,6 +438,7 @@ func_c_new(struct func_def *def)
>  		return NULL;
>  	}
>  	func->base.vtab = func_c_vtab;
> +	rlist_create(&func->base.signature);
>  	func->func = NULL;
>  	func->module = NULL;
>  	return &func->base;
> diff --git a/src/box/func.h b/src/box/func.h
> index 7e4dd37a3..133172bf0 100644
> --- a/src/box/func.h
> +++ b/src/box/func.h
> @@ -82,6 +82,11 @@ struct func {
>  	 * Cached runtime access information.
>  	 */
>  	struct access access[BOX_USER_MAX];
> +	/**
> +	 * A list of other functions with given name.
> +	 * Is valid for SQL builtins.
> +	 */
> +	struct rlist signature;
>  };
>  
>  /**
> diff --git a/src/box/func_def.c b/src/box/func_def.c
> index fb9f77df8..ffd76a514 100644
> --- a/src/box/func_def.c
> +++ b/src/box/func_def.c
> @@ -34,9 +34,28 @@ func_def_cmp(struct func_def *def1, struct func_def *def2)
>  		return def1->aggregate - def2->aggregate;
>  	if (def1->param_count != def2->param_count)
>  		return def1->param_count - def2->param_count;
> +	if (def1->sql_flags != def2->sql_flags)
> +		return def1->sql_flags - def2->sql_flags;
>  	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;
>  }
> +
> +struct func_def *
> +func_def_dup(struct func_def *def)
> +{
> +	uint32_t body_offset, comment_offset;
> +	uint32_t sz = func_def_sizeof(def->name_len,
> +				def->body != NULL ? strlen(def->body) : 0,
> +				def->comment != NULL ? strlen(def->comment) : 0,
> +				&body_offset, &comment_offset);
> +	struct func_def *new = (struct func_def *) malloc(sz);
> +	memcpy(new, def, sz);
> +	if (new->body != NULL)
> +		new->body = (char *)new + body_offset;
> +	if (new->comment != NULL)
> +		new->comment = (char *)new + comment_offset;
> +	return new;
> +}
> diff --git a/src/box/func_def.h b/src/box/func_def.h
> index 508580f78..809d74c42 100644
> --- a/src/box/func_def.h
> +++ b/src/box/func_def.h
> @@ -89,6 +89,8 @@ struct func_def {
>  	 * available.
>  	 */
>  	bool is_sandboxed;
> +	/** A set of SQL_FUNCTION_* flags. */
> +	uint16_t sql_flags;
>  	/** The count of function's input arguments. */
>  	int param_count;
>  	/** The type of the value returned by function. */
> diff --git a/src/box/lua/call.c b/src/box/lua/call.c
> index 752f05745..528c41310 100644
> --- a/src/box/lua/call.c
> +++ b/src/box/lua/call.c
> @@ -44,7 +44,6 @@
>  #include "box/port.h"
>  #include "box/lua/tuple.h"
>  #include "small/obuf.h"
> -#include "lua_sql.h"
>  #include "trivia/util.h"
>  #include "mpstream.h"
>  
> @@ -501,13 +500,16 @@ port_lua_destroy(struct port *base)
>  extern const char *
>  port_lua_dump_plain(struct port *port, uint32_t *size);
>  
> +extern struct sql_value *
> +port_lua_get_vdbemem(struct port *base, uint32_t *size);
> +
>  static const struct port_vtab port_lua_vtab = {
>  	.dump_msgpack = port_lua_dump,
>  	.dump_msgpack_16 = port_lua_dump_16,
>  	.dump_lua = port_lua_dump_lua,
>  	.dump_plain = port_lua_dump_plain,
>  	.get_msgpack = port_lua_get_msgpack,
> -	.get_vdbemem = NULL,
> +	.get_vdbemem = port_lua_get_vdbemem,
>  	.get_context = NULL,
>  	.destroy = port_lua_destroy,
>  };
> @@ -717,6 +719,7 @@ func_lua_new(struct func_def *def)
>  		func->lua_ref = LUA_REFNIL;
>  		func->base.vtab = func_lua_vtab;
>  	}
> +	rlist_create(&func->base.signature);
>  	return &func->base;
>  }
>  
> @@ -959,7 +962,6 @@ static struct trigger on_alter_func_in_lua = {
>  
>  static const struct luaL_Reg boxlib_internal[] = {
>  	{"call_loadproc",  lbox_call_loadproc},
> -	{"sql_create_function",  lbox_sql_create_function},
>  	{"module_reload", lbox_module_reload},
>  	{"func_call", lbox_func_call},
>  	{NULL, NULL}
> diff --git a/src/box/lua/lua_sql.c b/src/box/lua/lua_sql.c
> deleted file mode 100644
> index 0c5797fa2..000000000
> --- a/src/box/lua/lua_sql.c
> +++ /dev/null
> @@ -1,217 +0,0 @@
> -/*
> - * Copyright 2010-2017, 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 "lua.h"
> -#include "lua/utils.h"
> -
> -#include "box/lua/call.h"
> -#include "box/sql/sqlInt.h"
> -#include "box/port.h"
> -#include "box/sql/vdbeInt.h"
> -
> -struct lua_sql_func_info {
> -	int func_ref;
> -};
> -
> -/**
> - * This function is callback which is called by sql engine.
> - *
> - * Purpose of this function is to call lua func from sql.
> - * Lua func should be previously registered in sql
> - * (see lbox_sql_create_function).
> - */
> -static int
> -lua_sql_call(struct func *func, struct port *args, struct port *ret)
> -{
> -	(void) func;
> -	uint32_t argc;
> -	struct Mem *argv = (struct Mem *)port_get_vdbemem(args, &argc);
> -	if (argv == NULL)
> -		return -1;
> -	struct sql_context *ctx = (struct sql_context *) port_get_context(args);
> -	assert(ctx != NULL);
> -	struct Mem *val = vdbemem_alloc_on_region(1);
> -	if (val == NULL)
> -		return -1;
> -	port_vdbemem_create(ret, (struct sql_value *)val, 1, NULL);
> -
> -	lua_State *L = lua_newthread(tarantool_L);
> -	int coro_ref = luaL_ref(tarantool_L, LUA_REGISTRYINDEX);
> -	struct lua_sql_func_info *func_info = sql_user_data(ctx);
> -
> -	lua_rawgeti(L, LUA_REGISTRYINDEX, func_info->func_ref);
> -	for (uint32_t i = 0; i < argc; i++) {
> -		sql_value *param = (sql_value *)&argv[i];
> -		switch (sql_value_type(param)) {
> -		case MP_INT:
> -			luaL_pushint64(L, sql_value_int64(param));
> -			break;
> -		case MP_DOUBLE:
> -			lua_pushnumber(L, sql_value_double(param));
> -			break;
> -		case MP_STR:
> -			lua_pushstring(L, (const char *) sql_value_text(param));
> -			break;
> -		case MP_BIN:
> -			lua_pushlstring(L, sql_value_blob(param),
> -					(size_t) sql_value_bytes(param));
> -			break;
> -		case MP_NIL:
> -			lua_rawgeti(L, LUA_REGISTRYINDEX, luaL_nil_ref);
> -			break;
> -		case MP_BOOL:
> -			lua_pushboolean(L, sql_value_boolean(param));
> -			break;
> -		default:
> -			diag_set(ClientError, ER_SQL_EXECUTE, "Unsupported "\
> -				 "type passed to Lua");
> -			goto error;
> -		}
> -	}
> -	if (lua_pcall(L, lua_gettop(L) - 1, 1, 0) != 0){
> -		diag_set(ClientError, ER_SQL_EXECUTE, lua_tostring(L, -1));
> -		goto error;
> -	}
> -	switch(lua_type(L, -1)) {
> -	case LUA_TBOOLEAN:
> -		mem_set_bool(val, lua_toboolean(L, -1));
> -		break;
> -	case LUA_TNUMBER:
> -		sqlVdbeMemSetDouble(val, lua_tonumber(L, -1));
> -		break;
> -	case LUA_TSTRING:
> -		 if (sqlVdbeMemSetStr(val, lua_tostring(L, -1), -1,
> -					1, SQL_TRANSIENT) != 0)
> -			return -1;
> -		break;
> -	case LUA_TNIL:
> -		sqlVdbeMemSetNull(val);
> -		break;
> -	default:
> -		diag_set(ClientError, ER_SQL_EXECUTE, "Unsupported type "\
> -			 "passed from Lua");
> -		goto error;
> -	}
> -	return 0;
> -error:
> -	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, coro_ref);
> -	return -1;
> -}
> -
> -static void
> -lua_sql_destroy(void *p)
> -{
> -	struct lua_sql_func_info *func_info = p;
> -	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, func_info->func_ref);
> -	free(func_info);
> -	return;
> -}
> -
> -/**
> - * A helper to register lua function in SQL during runtime.
> - * It makes available queries like this: "SELECT lua_func(arg);"
> - *
> - * sql_create_function *p argument is used to store func ref
> - * to lua function (it identifies actual lua func to call if there
> - * are many of them). SQL function must have name and type of
> - * returning value. Additionally, it can feature number of
> - * arguments and deterministic flag.
> - */
> -int
> -lbox_sql_create_function(struct lua_State *L)
> -{
> -	struct sql *db = sql_get();
> -	if (db == NULL)
> -		return luaL_error(L, "Please call box.cfg{} first");
> -	int argc = lua_gettop(L);
> -	/*
> -	 * Three function prototypes are possible:
> -	 * 1. sql_create_function("func_name", "type", func);
> -	 * 2. sql_create_function("func_name", "type", func,
> -	 *                        func_arg_num);
> -	 * 3. sql_create_function("func_name", "type", func,
> -	 *                        func_arg_num, is_deterministic);
> -	 */
> -	if (!(argc == 3 && lua_isstring(L, 1) && lua_isstring(L, 2) &&
> -	    lua_isfunction(L, 3)) &&
> -	    !(argc == 4 && lua_isstring(L, 1) && lua_isstring(L, 2) &&
> -	      lua_isfunction(L, 3) && lua_isnumber(L, 4)) &&
> -	    !(argc == 5 && lua_isstring(L, 1) && lua_isstring(L, 2) &&
> -	      lua_isfunction(L, 3) && lua_isnumber(L, 4) &&
> -	      lua_isboolean(L, 5)))
> -		return luaL_error(L, "Invalid arguments");
> -	enum field_type type;
> -	const char *type_arg = lua_tostring(L, 2);
> -	if (strcmp(type_arg, "INT") == 0 || strcmp(type_arg, "INTEGER") == 0)
> -		type = FIELD_TYPE_INTEGER;
> -	else if (strcmp(type_arg, "TEXT") == 0)
> -		type = FIELD_TYPE_STRING;
> -	else if (strcmp(type_arg, "FLOAT") == 0)
> -		type = FIELD_TYPE_NUMBER;
> -	else if (strcmp(type_arg, "NUM") == 0)
> -		type = FIELD_TYPE_NUMBER;
> -	else if (strcmp(type_arg, "BLOB") == 0)
> -		type = FIELD_TYPE_SCALAR;
> -	else if (strcmp(type_arg, "BOOL") == 0 ||
> -		 strcmp(type_arg, "BOOLEAN") == 0)
> -		type = FIELD_TYPE_BOOLEAN;
> -	else
> -		return luaL_error(L, "Unknown type");
> -	/* -1 indicates any number of arguments. */
> -	int func_arg_num = -1;
> -	bool is_deterministic = false;
> -	if (argc == 4) {
> -		func_arg_num = lua_tointeger(L, 4);
> -		lua_pop(L, 1);
> -	} else if (argc == 5) {
> -		is_deterministic = lua_toboolean(L, 5);
> -		func_arg_num = lua_tointeger(L, 4);
> -		lua_pop(L, 2);
> -	}
> -	size_t name_len;
> -	const char *name = lua_tolstring(L, 1, &name_len);
> -	char *normalized_name =
> -		sql_normalized_name_region_new(&fiber()->gc, name, name_len);
> -	if (normalized_name == NULL)
> -		return luaT_error(L);
> -	struct lua_sql_func_info *func_info =
> -		(struct lua_sql_func_info *) malloc(sizeof(*func_info));
> -	if (func_info == NULL)
> -		return luaL_error(L, "out of memory");
> -	func_info->func_ref = luaL_ref(L, LUA_REGISTRYINDEX);
> -	int rc = sql_create_function_v2(db, normalized_name, type, func_arg_num,
> -					   is_deterministic ? SQL_DETERMINISTIC : 0,
> -					   func_info, lua_sql_call, NULL, NULL,
> -					   lua_sql_destroy);
> -	if (rc != 0)
> -		return luaT_error(L);
> -	return 0;
> -}
> diff --git a/src/box/lua/lua_sql.h b/src/box/lua/lua_sql.h
> deleted file mode 100644
> index b81093eca..000000000
> --- a/src/box/lua/lua_sql.h
> +++ /dev/null
> @@ -1,39 +0,0 @@
> -/*
> - * Copyright 2010-2017, 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.
> - */
> -
> -#ifndef TARANTOOL_LUA_SQL_H
> -#define TARANTOOL_LUA_SQL_H
> -
> -int
> -lbox_sql_create_function(struct lua_State *L);
> -
> -#endif //TARANTOOL_LUA_SQL_H
> -
> diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
> index aadcd3fa9..5c65eb792 100644
> --- a/src/box/lua/schema.lua
> +++ b/src/box/lua/schema.lua
> @@ -2109,7 +2109,9 @@ box.schema.func.create = function(name, opts)
>                                if_not_exists = 'boolean',
>                                language = 'string', body = 'string',
>                                is_deterministic = 'boolean',
> -                              is_sandboxed = 'boolean', comment = 'string' })
> +                              is_sandboxed = 'boolean', comment = 'string',
> +                              returns = 'string', param_list = 'table',
> +                              exports = 'table'})
>      local _func = box.space[box.schema.FUNC_ID]
>      local _vfunc = box.space[box.schema.VFUNC_ID]
>      local func = _vfunc.index.name:get{name}
> diff --git a/src/box/port.c b/src/box/port.c
> index 9e4ab9453..02c6a2245 100644
> --- a/src/box/port.c
> +++ b/src/box/port.c
> @@ -140,13 +140,16 @@ port_free(void)
>  	mempool_destroy(&port_tuple_entry_pool);
>  }
>  
> +extern struct sql_value *
> +port_tuple_get_vdbemem(struct port *base, uint32_t *size);
> +
>  const struct port_vtab port_tuple_vtab = {
>  	.dump_msgpack = port_tuple_dump_msgpack,
>  	.dump_msgpack_16 = port_tuple_dump_msgpack_16,
>  	.dump_lua = port_tuple_dump_lua,
>  	.dump_plain = NULL,
>  	.get_msgpack = NULL,
> -	.get_vdbemem = NULL,
> +	.get_vdbemem = port_tuple_get_vdbemem,
>  	.get_context = NULL,
>  	.destroy = port_tuple_destroy,
>  };
> diff --git a/src/box/sql.h b/src/box/sql.h
> index a078bfdec..ac10ae400 100644
> --- a/src/box/sql.h
> +++ b/src/box/sql.h
> @@ -33,6 +33,7 @@
>  
>  #include <stdbool.h>
>  #include <stdint.h>
> +#include "box/func.h"
>  
>  #if defined(__cplusplus)
>  extern "C" {
> @@ -409,6 +410,18 @@ vdbe_field_ref_prepare_tuple(struct vdbe_field_ref *field_ref,
>  struct func *
>  func_sql_builtin_new(struct func_def *def);
>  
> +struct func_sql_builtin {
> +	/** Function object base class. */
> +	struct func base;
> +	/** User data to pass in call method. */
> +	void *user_data;
> +	/** Finilize method for aggregate function. */
> +	int (*finalize)(struct func *func, struct port *args, struct port *ret);
> +};
> +
> +struct func *
> +sql_func_by_signature(const char *name, uint32_t name_len, int argc);
> +
>  #if defined(__cplusplus)
>  } /* extern "C" { */
>  #endif
> diff --git a/src/box/sql/analyze.c b/src/box/sql/analyze.c
> index 7764b48c5..02c922314 100644
> --- a/src/box/sql/analyze.c
> +++ b/src/box/sql/analyze.c
> @@ -264,7 +264,7 @@ stat4Destructor(void *pOld)
>   * return value is BLOB, but it is really just a pointer to the Stat4Accum
>   * object.
>   */
> -static int
> +MAYBE_UNUSED int
>  sql_builtin_stat_init(struct func *func, struct port *args, struct port *ret)
>  {
>  	(void) func;
> @@ -544,7 +544,7 @@ samplePushPrevious(Stat4Accum * p, int iChng)
>   *
>   * The R parameter is only used for STAT4
>   */
> -static int
> +MAYBE_UNUSED int
>  sql_builtin_stat_push(struct func *func, struct port *args, struct port *ret)
>  {
>  	(void) func;
> @@ -623,7 +623,7 @@ sql_builtin_stat_push(struct func *func, struct port *args, struct port *ret)
>   * The content to returned is determined by the parameter J
>   * which is one of the STAT_GET_xxxx values defined above.
>   */
> -static int
> +MAYBE_UNUSED int
>  sql_builtin_stat_get(struct func *func, struct port *args, struct port *ret)
>  {
>  	(void) func;
> @@ -736,11 +736,11 @@ vdbe_emit_analyze_stat_get(struct Vdbe * v, int regStat4, int iParam,
>  {
>  	assert(regOut != regStat4 && regOut != regStat4 + 1);
>  	sqlVdbeAddOp2(v, OP_Integer, iParam, regStat4 + 1);
> -	struct FuncDef *func =
> -		sqlFindFunction(sql_get(), "_sql_stat_get", 2, 0);
> +	struct func *func =
> +		sql_func_by_signature("_sql_stat_get", strlen("_sql_stat_get"), 2);
>  	assert(func != NULL);
>  	sqlVdbeAddOp4(v, OP_Function0, 0, regStat4, regOut,
> -		      (char *)func, P4_FUNCDEF);
> +		      (char *)func, P4_FUNC);
>  	sqlVdbeChangeP5(v, 2);
>  }
>  
> @@ -876,11 +876,12 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
>  		sqlVdbeAddOp2(v, OP_Count, idx_cursor, stat4_reg + 3);
>  		sqlVdbeAddOp2(v, OP_Integer, part_count, stat4_reg + 1);
>  		sqlVdbeAddOp2(v, OP_Integer, part_count, stat4_reg + 2);
> -		struct FuncDef *init_func =
> -			sqlFindFunction(sql_get(), "_sql_stat_init", 3, 0);
> +		struct func *init_func =
> +			sql_func_by_signature("_sql_stat_init",
> +					  strlen("_sql_stat_init"), 3);
>  		assert(init_func != NULL);
>  		sqlVdbeAddOp4(v, OP_Function0, 0, stat4_reg + 1, stat4_reg,
> -			      (char *)init_func, P4_FUNCDEF);
> +			      (char *)init_func, P4_FUNC);
>  		sqlVdbeChangeP5(v, 3);
>  		/*
>  		 * Implementation of the following:
> @@ -977,11 +978,12 @@ vdbe_emit_analyze_space(struct Parse *parse, struct space *space)
>  		sqlVdbeAddOp3(v, OP_MakeRecord, stat_key_reg,
>  				  pk_part_count, key_reg);
>  		assert(chng_reg == (stat4_reg + 1));
> -		struct FuncDef *push_func =
> -			sqlFindFunction(sql_get(), "_sql_stat_push", 3, 0);
> +		struct func *push_func =
> +			sql_func_by_signature("_sql_stat_push",
> +					  strlen("_sql_stat_push"), 3);
>  		assert(push_func != NULL);
>  		sqlVdbeAddOp4(v, OP_Function0, 1, stat4_reg, tmp_reg,
> -			      (char *)push_func, P4_FUNCDEF);
> +			      (char *)push_func, P4_FUNC);
>  		sqlVdbeChangeP5(v, 3);
>  		sqlVdbeAddOp2(v, OP_Next, idx_cursor, next_row_addr);
>  		/* Add the entry to the stat1 table. */
> @@ -1768,17 +1770,3 @@ fail:
>  	box_txn_rollback();
>  	return -1;
>  }
> -
> -void
> -sql_register_analyze_builtins(void)
> -{
> -	static FuncDef funcs[] = {
> -		FUNCTION(_sql_stat_get, 2, 0, 0, sql_builtin_stat_get,
> -			 FIELD_TYPE_ANY),
> -		FUNCTION(_sql_stat_push, 3, 0, 0, sql_builtin_stat_push,
> -			 FIELD_TYPE_ANY),
> -		FUNCTION(_sql_stat_init, 3, 0, 0, sql_builtin_stat_init,
> -			 FIELD_TYPE_ANY),
> -	};
> -	sqlInsertBuiltinFuncs(funcs, nelem(funcs));
> -}
> diff --git a/src/box/sql/callback.c b/src/box/sql/callback.c
> index 42fec2c6a..290363db6 100644
> --- a/src/box/sql/callback.c
> +++ b/src/box/sql/callback.c
> @@ -56,211 +56,3 @@ sql_get_coll_seq(Parse *parser, const char *name, uint32_t *coll_id)
>  		return p->coll;
>  	}
>  }
> -
> -/* During the search for the best function definition, this procedure
> - * is called to test how well the function passed as the first argument
> - * matches the request for a function with nArg arguments in a system
> - * that uses encoding enc. The value returned indicates how well the
> - * request is matched. A higher value indicates a better match.
> - *
> - * If nArg is -1 that means to only return a match (non-zero) if p->nArg
> - * is also -1.  In other words, we are searching for a function that
> - * takes a variable number of arguments.
> - *
> - * If nArg is -2 that means that we are searching for any function
> - * regardless of the number of arguments it uses, so return a positive
> - * match score for any
> - *
> - * The returned value is always between 0 and 6, as follows:
> - *
> - * 0: Not a match.
> - * 1: UTF8/16 conversion required and function takes any number of arguments.
> - * 2: UTF16 byte order change required and function takes any number of args.
> - * 3: encoding matches and function takes any number of arguments
> - * 4: UTF8/16 conversion required - argument count matches exactly
> - * 5: UTF16 byte order conversion required - argument count matches exactly
> - * 6: Perfect match:  encoding and argument count match exactly.
> - *
> - * If nArg==(-2) then any function with a non-null xSFunc is
> - * a perfect match and any function with xSFunc NULL is
> - * a non-match.
> - */
> -#define FUNC_PERFECT_MATCH 4	/* The score for a perfect match */
> -static int
> -matchQuality(FuncDef * p,	/* The function we are evaluating for match quality */
> -	     int nArg		/* Desired number of arguments.  (-1)==any */
> -    )
> -{
> -	int match;
> -
> -	/* nArg of -2 is a special case */
> -	if (nArg == (-2))
> -		return (p->xSFunc == 0) ? 0 : FUNC_PERFECT_MATCH;
> -
> -	/* Wrong number of arguments means "no match" */
> -	if (p->nArg != nArg && p->nArg >= 0)
> -		return 0;
> -
> -	/* Give a better score to a function with a specific number of arguments
> -	 * than to function that accepts any number of arguments.
> -	 */
> -	if (p->nArg == nArg) {
> -		match = 4;
> -	} else {
> -		match = 1;
> -	}
> -
> -	return match;
> -}
> -
> -/*
> - * Search a FuncDefHash for a function with the given name.  Return
> - * a pointer to the matching FuncDef if found, or 0 if there is no match.
> - */
> -static FuncDef *
> -functionSearch(int h,		/* Hash of the name */
> -	       const char *zFunc	/* Name of function */
> -    )
> -{
> -	FuncDef *p;
> -	for (p = sqlBuiltinFunctions.a[h]; p; p = p->u.pHash) {
> -		if (sqlStrICmp(p->zName, zFunc) == 0) {
> -			return p;
> -		}
> -	}
> -	return 0;
> -}
> -
> -/*
> - * Insert a new FuncDef into a FuncDefHash hash table.
> - */
> -void
> -sqlInsertBuiltinFuncs(FuncDef * aDef,	/* List of global functions to be inserted */
> -			  int nDef	/* Length of the apDef[] list */
> -    )
> -{
> -	int i;
> -	for (i = 0; i < nDef; i++) {
> -		FuncDef *pOther;
> -		const char *zName = aDef[i].zName;
> -		int nName = sqlStrlen30(zName);
> -		int h =
> -		    (sqlUpperToLower[(u8) zName[0]] +
> -		     nName) % SQL_FUNC_HASH_SZ;
> -		pOther = functionSearch(h, zName);
> -		if (pOther) {
> -			assert(pOther != &aDef[i] && pOther->pNext != &aDef[i]);
> -			aDef[i].pNext = pOther->pNext;
> -			pOther->pNext = &aDef[i];
> -		} else {
> -			aDef[i].pNext = 0;
> -			aDef[i].u.pHash = sqlBuiltinFunctions.a[h];
> -			sqlBuiltinFunctions.a[h] = &aDef[i];
> -		}
> -	}
> -}
> -
> -/*
> - * Locate a user function given a name, a number of arguments and a flag
> - * indicating whether the function prefers UTF-16 over UTF-8.  Return a
> - * pointer to the FuncDef structure that defines that function, or return
> - * NULL if the function does not exist.
> - *
> - * If the createFlag argument is true, then a new (blank) FuncDef
> - * structure is created and liked into the "db" structure if a
> - * no matching function previously existed.
> - *
> - * If nArg is -2, then the first valid function found is returned.  A
> - * function is valid if xSFunc is non-zero.  The nArg==(-2)
> - * case is used to see if zName is a valid function name for some number
> - * of arguments.  If nArg is -2, then createFlag must be 0.
> - *
> - * If createFlag is false, then a function with the required name and
> - * number of arguments may be returned even if the eTextRep flag does not
> - * match that requested.
> - */
> -FuncDef *
> -sqlFindFunction(sql * db,	/* An open database */
> -		    const char *zName,	/* Name of the function.  zero-terminated */
> -		    int nArg,	/* Number of arguments.  -1 means any number */
> -		    u8 createFlag	/* Create new entry if true and does not otherwise exist */
> -    )
> -{
> -	FuncDef *p;		/* Iterator variable */
> -	FuncDef *pBest = 0;	/* Best match found so far */
> -	int bestScore = 0;	/* Score of best match */
> -	int h;			/* Hash value */
> -	int nName;		/* Length of the name */
> -
> -	assert(nArg >= (-2));
> -	assert(nArg >= (-1) || createFlag == 0);
> -	nName = sqlStrlen30(zName);
> -
> -	/* First search for a match amongst the application-defined functions.
> -	 */
> -	p = (FuncDef *) sqlHashFind(&db->aFunc, zName);
> -	while (p) {
> -		int score = matchQuality(p, nArg);
> -		if (score > bestScore) {
> -			pBest = p;
> -			bestScore = score;
> -		}
> -		p = p->pNext;
> -	}
> -
> -	/* If no match is found, search the built-in functions.
> -	 *
> -	 * If the SQL_PreferBuiltin flag is set, then search the built-in
> -	 * functions even if a prior app-defined function was found.  And give
> -	 * priority to built-in functions.
> -	 *
> -	 * Except, if createFlag is true, that means that we are trying to
> -	 * install a new function.  Whatever FuncDef structure is returned it will
> -	 * have fields overwritten with new information appropriate for the
> -	 * new function.  But the FuncDefs for built-in functions are read-only.
> -	 * So we must not search for built-ins when creating a new function.
> -	 */
> -	if (!createFlag && (pBest == NULL)) {
> -		bestScore = 0;
> -		h = (sqlUpperToLower[(u8) zName[0]] +
> -		     nName) % SQL_FUNC_HASH_SZ;
> -		p = functionSearch(h, zName);
> -		while (p) {
> -			int score = matchQuality(p, nArg);
> -			if (score > bestScore) {
> -				pBest = p;
> -				bestScore = score;
> -			}
> -			p = p->pNext;
> -		}
> -	}
> -
> -	/* If the createFlag parameter is true and the search did not reveal an
> -	 * exact match for the name, number of arguments and encoding, then add a
> -	 * new entry to the hash table and return it.
> -	 */
> -	if (createFlag && bestScore < FUNC_PERFECT_MATCH &&
> -	    (pBest =
> -	     sqlDbMallocZero(db, sizeof(*pBest) + nName + 1)) != 0) {
> -		FuncDef *pOther;
> -		pBest->zName = (const char *)&pBest[1];
> -		pBest->nArg = (u16) nArg;
> -		pBest->funcFlags = 0;
> -		memcpy((char *)&pBest[1], zName, nName + 1);
> -		pOther =
> -		    (FuncDef *) sqlHashInsert(&db->aFunc, pBest->zName,
> -						  pBest);
> -		if (pOther == pBest) {
> -			sqlDbFree(db, pBest);
> -			sqlOomFault(db);
> -			return 0;
> -		} else {
> -			pBest->pNext = pOther;
> -		}
> -	}
> -
> -	if (pBest && (pBest->xSFunc || createFlag)) {
> -		return pBest;
> -	}
> -	return 0;
> -}
> diff --git a/src/box/sql/date.c b/src/box/sql/date.c
> index 2e2a71ad2..dffc23616 100644
> --- a/src/box/sql/date.c
> +++ b/src/box/sql/date.c
> @@ -1290,31 +1290,3 @@ currentTimeFunc(sql_context * context, int argc, sql_value ** argv)
>  	}
>  }
>  #endif
> -
> -/*
> - * This function registered all of the above C functions as SQL
> - * functions.  This should be the only routine in this file with
> - * external linkage.
> - */
> -void
> -sqlRegisterDateTimeFunctions(void)
> -{
> -	static FuncDef aDateTimeFuncs[] = {
> -#if 0
> -		DFUNCTION(julianday, -1, 0, 0, juliandayFunc, FIELD_TYPE_NUMBER),
> -		DFUNCTION(date, -1, 0, 0, dateFunc, FIELD_TYPE_STRING),
> -		DFUNCTION(time, -1, 0, 0, timeFunc, FIELD_TYPE_STRING),
> -		DFUNCTION(datetime, -1, 0, 0, datetimeFunc, FIELD_TYPE_STRING),
> -		DFUNCTION(strftime, -1, 0, 0, strftimeFunc, FIELD_TYPE_STRING),
> -		DFUNCTION(current_time, 0, 0, 0, ctimeFunc, FIELD_TYPE_STRING),
> -		DFUNCTION(current_timestamp, 0, 0, 0, ctimestampFunc,
> -			  FIELD_TYPE_STRING),
> -		DFUNCTION(current_date, 0, 0, 0, cdateFunc, FIELD_TYPE_STRING),
> -		STR_FUNCTION(current_time, 0, "%H:%M:%S", 0, currentTimeFunc),
> -		STR_FUNCTION(current_date, 0, "%Y-%m-%d", 0, currentTimeFunc),
> -		STR_FUNCTION(current_timestamp, 0, "%Y-%m-%d %H:%M:%S", 0,
> -			     currentTimeFunc),
> -#endif
> -	};
> -	sqlInsertBuiltinFuncs(aDateTimeFuncs, ArraySize(aDateTimeFuncs));
> -}
> diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
> index 7ee3e992e..003e49840 100644
> --- a/src/box/sql/expr.c
> +++ b/src/box/sql/expr.c
> @@ -35,6 +35,8 @@
>   */
>  #include "box/coll_id_cache.h"
>  #include "coll/coll.h"
> +#include "box/func.h"
> +#include "box/field_def.h"
>  #include "sqlInt.h"
>  #include "tarantoolInt.h"
>  #include "box/schema.h"
> @@ -275,12 +277,13 @@ sql_expr_coll(Parse *parse, Expr *p, bool *is_explicit_coll, uint32_t *coll_id,
>  		if (op == TK_FUNCTION) {
>  			uint32_t arg_count = p->x.pList == NULL ? 0 :
>  					     p->x.pList->nExpr;
> -			struct FuncDef *func = sqlFindFunction(parse->db,
> -							       p->u.zToken,
> -							       arg_count, 0);
> +			struct func *func =
> +				sql_func_by_signature(p->u.zToken,
> +						strlen(p->u.zToken), arg_count);
>  			if (func == NULL)
>  				break;
> -			if ((func->funcFlags & SQL_FUNC_DERIVEDCOLL) != 0) {
> +			if ((func->def->sql_flags &
> +			     SQL_FUNC_DERIVEDCOLL) != 0) {
>  				/*
>  				 * Now we use quite straightforward
>  				 * approach assuming that resulting
> @@ -289,7 +292,7 @@ sql_expr_coll(Parse *parse, Expr *p, bool *is_explicit_coll, uint32_t *coll_id,
>  				 * built-in functions: trim, upper,
>  				 * lower, replace, substr.
>  				 */
> -				assert(func->ret_type == FIELD_TYPE_STRING);
> +				assert(func->def->returns == FIELD_TYPE_STRING);
>  				p = p->x.pList->a->pExpr;
>  				continue;
>  			}
> @@ -3923,11 +3926,9 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
>  	case TK_FUNCTION:{
>  			ExprList *pFarg;	/* List of function arguments */
>  			int nFarg;	/* Number of function arguments */
> -			FuncDef *pDef;	/* The function definition object */
>  			const char *zId;	/* The function name */
>  			u32 constMask = 0;	/* Mask of function arguments that are constant */
>  			int i;	/* Loop counter */
> -			sql *db = pParse->db;	/* The database connection */
>  			struct coll *coll = NULL;
>  
>  			assert(!ExprHasProperty(pExpr, EP_xIsSelect));
> @@ -3939,8 +3940,10 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
>  			nFarg = pFarg ? pFarg->nExpr : 0;
>  			assert(!ExprHasProperty(pExpr, EP_IntValue));
>  			zId = pExpr->u.zToken;
> -			pDef = sqlFindFunction(db, zId, nFarg, 0);
> -			if (pDef == 0 || pDef->xFinalize != 0) {
> +			struct func *func =
> +				sql_func_by_signature(zId, strlen(zId), nFarg);
> +			if (func == NULL ||
> +			    func->def->aggregate == FUNC_AGGREGATE_GROUP) {
>  				diag_set(ClientError, ER_NO_SUCH_FUNCTION,
>  					 zId);
>  				pParse->is_aborted = true;
> @@ -3950,7 +3953,7 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
>  			 * IFNULL() functions.  This avoids unnecessary evaluation of
>  			 * arguments past the first non-NULL argument.
>  			 */
> -			if (pDef->funcFlags & SQL_FUNC_COALESCE) {
> +			if (func->def->sql_flags & SQL_FUNC_COALESCE) {
>  				int endCoalesce = sqlVdbeMakeLabel(v);
>  				assert(nFarg >= 2);
>  				sqlExprCode(pParse, pFarg->a[0].pExpr,
> @@ -3987,7 +3990,7 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
>  			 * is done using ANSI rules from
>  			 * collations_check_compatibility().
>  			 */
> -			if ((pDef->funcFlags & SQL_FUNC_NEEDCOLL) != 0) {
> +			if ((func->def->sql_flags & SQL_FUNC_NEEDCOLL) != 0) {
>  				struct coll *unused = NULL;
>  				uint32_t curr_id = COLL_NONE;
>  				bool is_curr_forced = false;
> @@ -4034,9 +4037,8 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
>  				 * or OPFLAG_TYPEOFARG respectively, to avoid unnecessary data
>  				 * loading.
>  				 */
> -				if ((pDef->
> -				     funcFlags & (SQL_FUNC_LENGTH |
> -						  SQL_FUNC_TYPEOF)) != 0) {
> +				if ((func->def->sql_flags & (SQL_FUNC_LENGTH |
> +						SQL_FUNC_TYPEOF)) != 0) {
>  					u8 exprOp;
>  					assert(nFarg == 1);
>  					assert(pFarg->a[0].pExpr != 0);
> @@ -4051,8 +4053,7 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
>  							 funcFlags &
>  							 OPFLAG_LENGTHARG);
>  						pFarg->a[0].pExpr->op2 =
> -						    pDef->
> -						    funcFlags &
> +						    func->def->sql_flags &
>  						    (OPFLAG_LENGTHARG |
>  						     OPFLAG_TYPEOFARG);
>  					}
> @@ -4066,12 +4067,12 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
>  			} else {
>  				r1 = 0;
>  			}
> -			if (pDef->funcFlags & SQL_FUNC_NEEDCOLL) {
> +			if (func->def->sql_flags & SQL_FUNC_NEEDCOLL) {
>  				sqlVdbeAddOp4(v, OP_CollSeq, 0, 0, 0,
>  						  (char *)coll, P4_COLLSEQ);
>  			}
>  			sqlVdbeAddOp4(v, OP_Function0, constMask, r1,
> -					  target, (char *)pDef, P4_FUNCDEF);
> +				      target, (char *)func, P4_FUNC);
>  			sqlVdbeChangeP5(v, (u8) nFarg);
>  			if (nFarg && constMask == 0) {
>  				sqlReleaseTempRange(pParse, r1, nFarg);
> @@ -5376,12 +5377,18 @@ analyzeAggregate(Walker * pWalker, Expr * pExpr)
>  						pItem->iMem = ++pParse->nMem;
>  						assert(!ExprHasProperty
>  						       (pExpr, EP_IntValue));
> -						pItem->pFunc = sqlFindFunction(
> -							pParse->db,
> +						pItem->func = sql_func_by_signature(
>  							pExpr->u.zToken,
> +							strlen(pExpr->u.zToken),
>  							pExpr->x.pList ?
> -							pExpr->x.pList->nExpr : 0,
> -							0);
> +							pExpr->x.pList->
> +							nExpr : 0);
> +						assert(pItem->func->def->
> +							language ==
> +							FUNC_LANGUAGE_SQL_BUILTIN &&
> +							pItem->func->def->
> +							aggregate ==
> +							FUNC_AGGREGATE_GROUP);
>  						if (pExpr->flags & EP_Distinct) {
>  							pItem->iDistinct =
>  								pParse->nTab++;
> diff --git a/src/box/sql/func.c b/src/box/sql/func.c
> index f79939cb6..e35b5c2a2 100644
> --- a/src/box/sql/func.c
> +++ b/src/box/sql/func.c
> @@ -39,14 +39,19 @@
>  #include "version.h"
>  #include "coll/coll.h"
>  #include "box/func.h"
> +#include "lua/utils.h"
> +#include "box/tuple.h"
> +#include "mpstream.h"
>  #include "box/port.h"
>  #include "tarantoolInt.h"
>  #include "box/session.h"
> +#include "box/schema.h"
>  #include <unicode/ustring.h>
>  #include <unicode/ucasemap.h>
>  #include <unicode/ucnv.h>
>  #include <unicode/uchar.h>
>  #include <unicode/ucol.h>
> +#include "small/rlist.h"
>  
>  /*
>   * Return the collating function associated with a function.
> @@ -101,17 +106,222 @@ port_vdbemem_get_context(struct port *base)
>  	return port->ctx;
>  }
>  
> +void
> +port_vdbemem_dump_lua(struct port *base, struct lua_State *L, bool is_flat)
> +{
> +	(void) is_flat;
> +	struct port_vdbemem *port = (struct port_vdbemem *) base;
> +	assert(is_flat == true);
> +	for (uint32_t i = 0; i < port->size; i++) {
> +		sql_value *param =
> +			(sql_value *)((struct Mem *)port->mem + i);
> +		switch (sql_value_type(param)) {
> +		case MP_INT:
> +			luaL_pushint64(L, sql_value_int64(param));
> +			break;
> +		case MP_DOUBLE:
> +			lua_pushnumber(L, sql_value_double(param));
> +			break;
> +		case MP_STR:
> +			lua_pushstring(L, (const char *) sql_value_text(param));
> +			break;
> +		case MP_BIN:
> +			lua_pushlstring(L, sql_value_blob(param),
> +				(size_t) sql_value_bytes(param));
> +			break;
> +		case MP_NIL:
> +			lua_pushnil(L);
> +			break;
> +		case MP_BOOL:
> +			lua_pushboolean(L, sql_value_boolean(param));
> +			break;
> +		default:
> +			unreachable();
> +		}
> +	}
> +}
> +
> +const char *
> +port_vdbemem_get_msgpack(struct port *base, uint32_t *size)
> +{
> +	struct port_vdbemem *port = (struct port_vdbemem *) base;
> +	struct region *region = &fiber()->gc;
> +	size_t region_svp = region_used(region);
> +	bool is_error = false;
> +	struct mpstream stream;
> +	mpstream_init(&stream, region, region_reserve_cb, region_alloc_cb,
> +		      set_encode_error, &is_error);
> +	mpstream_encode_array(&stream, port->size);
> +
> +	for (uint32_t i = 0; i < port->size && !is_error; i++) {
> +		sql_value *param =
> +			(sql_value *)((struct Mem *)port->mem + i);
> +		switch (sql_value_type(param)) {
> +		case MP_INT: {
> +			sql_int64 val = sql_value_int64(param);
> +			if (val < 0)
> +				mpstream_encode_int(&stream, val);
> +			else
> +				mpstream_encode_uint(&stream, val);
> +			break;
> +		}
> +		case MP_DOUBLE: {
> +			mpstream_encode_double(&stream,
> +					       sql_value_double(param));
> +			break;
> +		}
> +		case MP_STR: {
> +			mpstream_encode_str(&stream,
> +					(const char *) sql_value_text(param));
> +			break;
> +		}
> +		case MP_BIN: {
> +			mpstream_encode_binl(&stream, sql_value_bytes(param));
> +			mpstream_memcpy(&stream, sql_value_blob(param),
> +					sql_value_bytes(param));
> +			break;
> +		}
> +		case MP_NIL: {
> +			mpstream_encode_nil(&stream);
> +			break;
> +		}
> +		case MP_BOOL: {
> +			mpstream_encode_bool(&stream, sql_value_boolean(param));
> +			break;
> +		}
> +		default:
> +			unreachable();
> +		}
> +	}
> +	mpstream_flush(&stream);
> +	*size = region_used(region) - region_svp;
> +	if (is_error)
> +		goto error;
> +	const char *ret = (char *)region_join(region, *size);
> +	if (ret == NULL)
> +		goto error;
> +	return ret;
> +error:
> +	diag_set(OutOfMemory, *size, "region", "ret");
> +	return NULL;
> +}
> +
>  static const struct port_vtab port_vdbemem_vtab = {
>  	.dump_msgpack = NULL,
>  	.dump_msgpack_16 = NULL,
> -	.dump_lua = NULL,
> +	.dump_lua = port_vdbemem_dump_lua,
>  	.dump_plain = NULL,
> -	.get_msgpack = NULL,
> +	.get_msgpack = port_vdbemem_get_msgpack,
>  	.get_vdbemem = port_vdbemem_get_vdbemem,
>  	.get_context = port_vdbemem_get_context,
>  	.destroy = NULL,
>  };
>  
> +
> +struct sql_value *
> +port_lua_get_vdbemem(struct port *base, uint32_t *size)
> +{
> +	struct port_lua *port = (struct port_lua *) base;
> +	struct lua_State *L = port->L;
> +	int argc = lua_gettop(L);
> +	*size = argc;
> +	struct region *region = &fiber()->gc;
> +	size_t region_svp = region_used(region);
> +	struct Mem *val = vdbemem_alloc_on_region(argc);
> +	if (val == NULL)
> +		return NULL;
> +	for (int i = 0; i < argc; i++) {
> +		switch(lua_type(L, -1 - i)) {
> +		case LUA_TBOOLEAN:
> +			mem_set_bool(&val[i], lua_toboolean(L, -1 - i));
> +			break;
> +		case LUA_TNUMBER:
> +			sqlVdbeMemSetDouble(&val[i], lua_tonumber(L, -1 - i));
> +			break;
> +		case LUA_TSTRING:
> +			if (sqlVdbeMemSetStr(&val[i], lua_tostring(L, -1 - i), -1,
> +					     1, SQL_TRANSIENT) != 0)
> +				goto error;
> +			break;
> +		case LUA_TNIL:
> +			sqlVdbeMemSetNull(&val[i]);
> +			break;
> +		default:
> +			diag_set(ClientError, ER_SQL_EXECUTE,
> +				 "Unsupported type passed from Lua");
> +			goto error;
> +		}
> +	}
> +	return (struct sql_value *)val;
> +error:
> +	for (int i = 0; i < argc; i++)
> +		sqlVdbeMemRelease(&val[i]);
> +	region_truncate(region, region_svp);
> +	return NULL;
> +}
> +
> +struct sql_value *
> +port_tuple_get_vdbemem(struct port *base, uint32_t *size)
> +{
> +	struct port_tuple *port = (struct port_tuple *)base;
> +	*size = port->size;
> +	struct region *region = &fiber()->gc;
> +	size_t region_svp = region_used(region);
> +	struct Mem *val = vdbemem_alloc_on_region(port->size);
> +	if (val == NULL)
> +		return NULL;
> +	int i = 0;
> +	struct port_tuple_entry *pe;
> +	for (pe = port->first; pe != NULL; pe = pe->next) {
> +		const char *data = tuple_data(pe->tuple);
> +		if (mp_typeof(*data) != MP_ARRAY ||
> +		    mp_decode_array(&data) != 1) {
> +			diag_set(ClientError, ER_SQL_EXECUTE,
> +				 "Unsupported type passed from C");
> +			goto error;
> +		}
> +		uint32_t len;
> +		const char *str;
> +		switch (mp_typeof(*data)) {
> +		case MP_BOOL:
> +			mem_set_bool(&val[i], mp_decode_bool(&data));
> +			break;
> +		case MP_FLOAT:
> +			sqlVdbeMemSetDouble(&val[i], mp_decode_float(&data));
> +			break;
> +		case MP_DOUBLE:
> +			sqlVdbeMemSetDouble(&val[i], mp_decode_double(&data));
> +			break;
> +		case MP_INT:
> +			sqlVdbeMemSetInt64(val, mp_decode_int(&data));
> +			break;
> +		case MP_UINT:
> +			sqlVdbeMemSetInt64(val, mp_decode_uint(&data));
> +			break;
> +		case MP_STR:
> +			str = mp_decode_str(&data, &len);
> +			if (sqlVdbeMemSetStr(&val[i], str, len,
> +					     1, SQL_TRANSIENT) != 0)
> +				goto error;
> +			break;
> +		case MP_NIL:
> +			sqlVdbeMemSetNull(val);
> +			break;
> +		default:
> +			diag_set(ClientError, ER_SQL_EXECUTE,
> +				 "Unsupported type passed from C");
> +			goto error;
> +		}
> +		i++;
> +	}
> +	return (struct sql_value *) val;
> +error:
> +	for (int i = 0; i < port->size; i++)
> +		sqlVdbeMemRelease(&val[i]);
> +	region_truncate(region, region_svp);
> +	return NULL;
> +}
> +
>  /*
>   * Implementation of the non-aggregate min() and max() functions
>   */
> @@ -714,19 +924,6 @@ sql_builtin_ICU##case_type(struct func *func, struct port *args,               \
>  ICU_CASE_CONVERT(Lower);
>  ICU_CASE_CONVERT(Upper);
>  
> -
> -/*
> - * Some functions like COALESCE() and IFNULL() are implemented
> - * as VDBE code so that unused argument values do not have to be
> - * computed.
> - * However, we still need some kind of function implementation for
> - * this routines in the function table. The sql_builtin_noop macro
> - * provides this. sql_builtin_noop will never be called so it
> - * doesn't matter what the implementation is. We might as well
> - * use the "version()" function as a substitute.
> - */
> -#define sql_builtin_noop sql_builtin_version
> -
>  /*
>   * Implementation of random().  Return a random integer.
>   */
> @@ -2028,142 +2225,302 @@ sql_builtin_group_concat_finalize(struct func *func, struct port *args,
>  }
>  
>  int
> -sql_is_like_func(struct sql *db, struct Expr *expr, int *is_like_ci)
> +sql_is_like_func(struct Expr *expr, int *is_like_ci)
>  {
>  	if (expr->op != TK_FUNCTION || !expr->x.pList ||
>  	    expr->x.pList->nExpr != 2)
>  		return 0;
>  	assert(!ExprHasProperty(expr, EP_xIsSelect));
> -	struct FuncDef *func = sqlFindFunction(db, expr->u.zToken, 2, 0);
> +	struct func *func =
> +		sql_func_by_signature(expr->u.zToken, strlen(expr->u.zToken), 2);
>  	assert(func != NULL);
> -	if ((func->funcFlags & SQL_FUNC_LIKE) == 0)
> +	if ((func->def->sql_flags & SQL_FUNC_LIKE) == 0)
>  		return 0;
>  	*is_like_ci =
>  		(current_session()->sql_flags & LIKE_CASE_SENS_FLAG) == 0;
>  	return 1;
>  }
>  
> -/*
> - * All of the FuncDef structures in the aBuiltinFunc[] array above
> - * to the global function hash table.  This occurs at start-time (as
> - * a consequence of calling sql_initialize()).
> - *
> - * After this routine runs
> - */
> -void
> -sqlRegisterBuiltinFunctions(void)
> +static struct func_vtab func_sql_builtin_vtab;
> +
> +struct func *
> +sql_func_by_signature(const char *name, uint32_t name_len, int argc)
>  {
> -	/*
> -	 * The following array holds FuncDef structures for all of the functions
> -	 * defined in this file.
> -	 *
> -	 * The array cannot be constant since changes are made to the
> -	 * FuncDef.pHash elements at start-time.  The elements of this array
> -	 * are read-only after initialization is complete.
> -	 *
> -	 * For peak efficiency, put the most frequently used function last.
> -	 */
> -	static FuncDef aBuiltinFunc[] = {
> -		FUNCTION_COLL(trim, 1, 3, 0, sql_builtin_trim_one_arg),
> -		FUNCTION_COLL(trim, 2, 3, 0, sql_builtin_trim_two_args),
> -		FUNCTION_COLL(trim, 3, 3, 0, sql_builtin_trim_three_args),
> -		FUNCTION(min, -1, 0, 1, sql_builtin_minmax, FIELD_TYPE_SCALAR),
> -		FUNCTION(min, 0, 0, 1, 0, FIELD_TYPE_SCALAR),
> -		AGGREGATE2(min, 1, 0, 1, sql_builtin_minmax_step,
> -			   sql_builtin_minmax_finalize, SQL_FUNC_MINMAX,
> -			   FIELD_TYPE_SCALAR),
> -		FUNCTION(max, -1, 1, 1, sql_builtin_minmax, FIELD_TYPE_SCALAR),
> -		FUNCTION(max, 0, 1, 1, 0, FIELD_TYPE_SCALAR),
> -		AGGREGATE2(max, 1, 1, 1, sql_builtin_minmax_step,
> -			   sql_builtin_minmax_finalize, SQL_FUNC_MINMAX,
> -			   FIELD_TYPE_SCALAR),
> -		FUNCTION2(typeof, 1, 0, 0, sql_builtin_typeof, SQL_FUNC_TYPEOF,
> -			  FIELD_TYPE_STRING),
> -		FUNCTION2(length, 1, 0, 0, sql_builtin_length, SQL_FUNC_LENGTH,
> -			  FIELD_TYPE_INTEGER),
> -		FUNCTION(position, 2, 0, 1, sql_builtin_position,
> -			 FIELD_TYPE_INTEGER),
> -		FUNCTION(printf, -1, 0, 0, sql_builtin_printf,
> -			 FIELD_TYPE_STRING),
> -		FUNCTION(unicode, 1, 0, 0, sql_builtin_unicode,
> -			 FIELD_TYPE_STRING),
> -		FUNCTION(char, -1, 0, 0, sql_builtin_char, FIELD_TYPE_STRING),
> -		FUNCTION(abs, 1, 0, 0, sql_builtin_abs, FIELD_TYPE_NUMBER),
> -		FUNCTION(round, 1, 0, 0, sql_builtin_round, FIELD_TYPE_INTEGER),
> -		FUNCTION(round, 2, 0, 0, sql_builtin_round, FIELD_TYPE_INTEGER),
> -		FUNCTION_COLL(upper, 1, 0, 1, sql_builtin_ICUUpper),
> -		FUNCTION_COLL(lower, 1, 0, 1, sql_builtin_ICULower),
> -		FUNCTION(hex, 1, 0, 0, sql_builtin_hex, FIELD_TYPE_STRING),
> -		FUNCTION2(ifnull, 2, 0, 0, sql_builtin_noop, SQL_FUNC_COALESCE,
> -			  FIELD_TYPE_INTEGER),
> -		VFUNCTION(random, 0, 0, 0, sql_builtin_random,
> -			  FIELD_TYPE_INTEGER),
> -		VFUNCTION(randomblob, 1, 0, 0, sql_builtin_random_blob,
> -			  FIELD_TYPE_SCALAR),
> -		FUNCTION(nullif, 2, 0, 1, sql_builtin_nullif,
> -			 FIELD_TYPE_SCALAR),
> -		FUNCTION(version, 0, 0, 0, sql_builtin_version,
> -			 FIELD_TYPE_STRING),
> -		FUNCTION(quote, 1, 0, 0, sql_builtin_quote, FIELD_TYPE_STRING),
> -		VFUNCTION(row_count, 0, 0, 0, sql_builtin_row_count,
> -			  FIELD_TYPE_INTEGER),
> -		FUNCTION_COLL(replace, 3, 0, 0, sql_builtin_replace),
> -		FUNCTION(zeroblob, 1, 0, 0, sql_builtin_zeroblob,
> -			 FIELD_TYPE_SCALAR),
> -		FUNCTION_COLL(substr, 2, 0, 0, sql_builtin_substr),
> -		FUNCTION_COLL(substr, 3, 0, 0, sql_builtin_substr),
> -		AGGREGATE(sum, 1, 0, 0, sql_builtin_sum_step,
> -			  sql_builtin_sum_finalize, FIELD_TYPE_NUMBER),
> -		AGGREGATE(total, 1, 0, 0, sql_builtin_sum_step,
> -			  sql_builtin_total_finalize, FIELD_TYPE_NUMBER),
> -		AGGREGATE(avg, 1, 0, 0, sql_builtin_sum_step,
> -			  sql_builtin_avg_finalize, FIELD_TYPE_NUMBER),
> -		AGGREGATE2(count, 0, 0, 0, sql_builtin_count_step,
> -			   sql_builtin_count_finalize, SQL_FUNC_COUNT,
> -			   FIELD_TYPE_INTEGER),
> -		AGGREGATE(count, 1, 0, 0, sql_builtin_count_step,
> -			  sql_builtin_count_finalize, FIELD_TYPE_INTEGER),
> -		AGGREGATE(group_concat, 1, 0, 0, sql_builtin_group_concat_step,
> -			  sql_builtin_group_concat_finalize, FIELD_TYPE_STRING),
> -		AGGREGATE(group_concat, 2, 0, 0, sql_builtin_group_concat_step,
> -			  sql_builtin_group_concat_finalize, FIELD_TYPE_STRING),
> -		FUNCTION2(like, 2, 1, 0, sql_builtin_like, SQL_FUNC_LIKE,
> -			  FIELD_TYPE_INTEGER),
> -		FUNCTION2(like, 3, 1, 0, sql_builtin_like, SQL_FUNC_LIKE,
> -			  FIELD_TYPE_INTEGER),
> -		FUNCTION(coalesce, 1, 0, 0, 0, FIELD_TYPE_SCALAR),
> -		FUNCTION(coalesce, 0, 0, 0, 0, FIELD_TYPE_SCALAR),
> -		FUNCTION2(coalesce, -1, 0, 0, sql_builtin_noop,
> -			  SQL_FUNC_COALESCE, FIELD_TYPE_SCALAR),
> -	};
> -	sql_register_analyze_builtins();
> -	sqlRegisterDateTimeFunctions();
> -	sqlInsertBuiltinFuncs(aBuiltinFunc, ArraySize(aBuiltinFunc));
> -
> -#if 0				/* Enable to print out how the built-in functions are hashed */
> -	{
> -		int i;
> -		FuncDef *p;
> -		for (i = 0; i < SQL_FUNC_HASH_SZ; i++) {
> -			printf("FUNC-HASH %02d:", i);
> -			for (p = sqlBuiltinFunctions.a[i]; p;
> -			     p = p->u.pHash) {
> -				int n = sqlStrlen30(p->zName);
> -				int h = p->zName[0] + n;
> -				printf(" %s(%d)", p->zName, h);
> -			}
> -			printf("\n");
> +	struct func *ret = NULL;
> +	struct func *main_func = func_by_name(name, name_len);
> +	if (main_func == NULL || argc == -2)
> +		return main_func;
> +	if (main_func->def->param_count == argc) {
> +		ret = main_func;
> +		goto end;
> +	}
> +	ret = main_func->def->param_count < 0 ? main_func : NULL;
> +	struct func *func;
> +	rlist_foreach_entry(func, &main_func->signature, signature) {
> +		/** Exact match. */
> +		if (func->def->param_count == argc) {
> +			ret = func;
> +			goto end;
>  		}
> +		/** Multiparam function. */
> +		if (func->def->param_count < 0)
> +			ret = func;
>  	}
> -#endif
> +end:
> +	return ret != NULL && ret->def->exports.sql ? ret : NULL;
>  }
>  
> -struct func_sql_builtin {
> -	/** Function object base class. */
> -	struct func base;
> +static int
> +sql_builtin_stub(struct func *func, struct port *args, struct port *ret)
> +{
> +	(void) func; (void) args; (void) ret;
> +	struct Mem *val = vdbemem_alloc_on_region(1);
> +	if (val == NULL)
> +		return -1;
> +	diag_set(ClientError, ER_SQL_EXECUTE, "function is not implemented");
> +	return -1;
> +}
> +
> +struct sql_builtin_entry {
> +	int param_count;
> +	enum field_type returns;
> +	uint16_t sql_flags;
> +	int (*call)(struct func *func, struct port *args, struct port *ret);
> +	int (*finalize)(struct func *func, struct port *args, struct port *ret);
> +	void *user_data;
> +	enum func_aggregate aggregate;
> +	bool is_deterministic;
> +	bool is_available;
>  };
>  
> -static struct func_vtab func_sql_builtin_vtab;
> +#define BUILTIN_FUNCTION_ENTRY(param_count, returns, flags, call,\
> +			       user_data, is_deterministic,      \
> +			       is_available)                     \
> +	{param_count, returns, flags, call, NULL, user_data,     \
> +	 FUNC_AGGREGATE_NONE, is_deterministic, is_available}
> +
> +#define BUILTIN_AGGREGATE_ENTRY(param_count, returns, flags, call,\
> +				finalize, user_data, is_available)\
> +	{param_count, returns, flags, call, finalize, user_data,  \
> +	 FUNC_AGGREGATE_GROUP, false, is_available}
> +
> +#define BUILTIN_STUB_ENTRY() \
> +	{-1, FIELD_TYPE_ANY, 0, sql_builtin_stub, NULL,		  \
> +	 NULL, FUNC_AGGREGATE_NONE, false, false}
> +
> +/**
> + * A sequence of SQL builtins definitions in
> + * lexicographic order.
> + */
> +static struct {
> +	const char *name;
> +	int entries;
> +	struct sql_builtin_entry entry[5];
> +} sql_builtins[] = {
> +	{"ABS", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_NUMBER, 0,
> +				sql_builtin_abs, NULL, true, true),
> +	}},
> +	{"AVG", 1, {
> +	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_NUMBER, 0,
> +				 sql_builtin_sum_step,
> +				 sql_builtin_avg_finalize, NULL, true),
> +	}},
> +	{"CEIL", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"CEILING", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"CHAR", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_STRING, 0,
> +				sql_builtin_char, NULL, true, true),
> +	}},
> +	{"CHARACTER_LENGTH", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"CHAR_LENGTH", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"COALESCE", 3, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_SCALAR, 0,
> +				sql_builtin_stub, NULL, true, false),
> +	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_SCALAR, 0,
> +				sql_builtin_stub, NULL, true, false),
> +	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_SCALAR, SQL_FUNC_COALESCE,
> +				sql_builtin_stub, NULL, true, true),
> +	}},
> +	{"COUNT", 2, {
> +	 BUILTIN_AGGREGATE_ENTRY(0, FIELD_TYPE_INTEGER,
> +				 SQL_FUNC_COUNT,
> +				 sql_builtin_count_step,
> +				 sql_builtin_count_finalize, NULL, true),
> +	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_INTEGER, 0,
> +				 sql_builtin_count_step,
> +				 sql_builtin_count_finalize, NULL, true),
> +	}},
> +	{"CURRENT_DATE", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"CURRENT_TIME", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"CURRENT_TIMESTAMP", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"DATE", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"DATETIME", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"EVERY", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"EXISTS", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"EXP", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"EXTRACT", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"FLOOR", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"GREATER", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"GROUP_CONCAT", 2, {
> +	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_STRING, 0,
> +				 sql_builtin_group_concat_step,
> +				 sql_builtin_group_concat_finalize, NULL, true),
> +	 BUILTIN_AGGREGATE_ENTRY(2, FIELD_TYPE_STRING, 1,
> +				 sql_builtin_group_concat_step,
> +				 sql_builtin_group_concat_finalize, NULL, true),
> +	}},
> +	{"HEX", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, 0,
> +				sql_builtin_hex, NULL, true, true),
> +	}},
> +	{"IFNULL", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_INTEGER, SQL_FUNC_COALESCE,
> +				sql_builtin_stub, NULL, true, true),
> +	}},
> +	{"JULIANDAY", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"LENGTH", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_INTEGER, SQL_FUNC_LENGTH,
> +				sql_builtin_length, NULL, true, true),
> +	}},
> +	{"LESSER", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"LIKE", 2, {
> +	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_INTEGER, SQL_FUNC_LIKE,
> +				sql_builtin_like, SQL_INT_TO_PTR(1),
> +				true, true),
> +	 BUILTIN_FUNCTION_ENTRY(3, FIELD_TYPE_INTEGER, SQL_FUNC_LIKE,
> +				sql_builtin_like, SQL_INT_TO_PTR(1),
> +				true, true),
> +	}},
> +	{"LN", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"LOWER", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING,
> +				SQL_FUNC_DERIVEDCOLL | SQL_FUNC_NEEDCOLL,
> +				sql_builtin_ICULower, NULL, true, true),
> +	}},
> +	{"MAX", 3, {
> +	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_SCALAR, SQL_FUNC_NEEDCOLL,
> +				sql_builtin_minmax, SQL_INT_TO_PTR(1),
> +				true, true),
> +	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_SCALAR, 0,
> +				sql_builtin_stub, SQL_INT_TO_PTR(1),
> +				true, false),
> +	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_SCALAR,
> +				 SQL_FUNC_NEEDCOLL | SQL_FUNC_MINMAX,
> +				 sql_builtin_minmax_step,
> +				 sql_builtin_minmax_finalize,
> +				 SQL_INT_TO_PTR(1), true),
> +	}},
> +	{"MIN", 3, {
> +	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_SCALAR, SQL_FUNC_NEEDCOLL,
> +				sql_builtin_minmax, SQL_INT_TO_PTR(0),
> +				true, true),
> +	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_SCALAR, 0,
> +				sql_builtin_stub, SQL_INT_TO_PTR(0),
> +				true, false),
> +	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_SCALAR,
> +				 SQL_FUNC_NEEDCOLL | SQL_FUNC_MINMAX,
> +				 sql_builtin_minmax_step,
> +				 sql_builtin_minmax_finalize,
> +				 SQL_INT_TO_PTR(0), true),
> +	}},
> +	{"MOD", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"NULLIF", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_SCALAR, SQL_FUNC_NEEDCOLL,
> +				sql_builtin_nullif, NULL, true, true),
> +	}},
> +	{"OCTET_LENGTH", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"POSITION", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_INTEGER, SQL_FUNC_NEEDCOLL,
> +				sql_builtin_position, NULL, true, true),
> +	}},
> +	{"POWER", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"PRINTF", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(-1, FIELD_TYPE_STRING, 0,
> +				sql_builtin_printf, NULL, true, true),
> +	}},
> +	{"QUOTE", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, 0,
> +				sql_builtin_quote, NULL, true, true),
> +	}},
> +	{"RANDOM", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_INTEGER, 0,
> +				sql_builtin_random, NULL, false, true),
> +	}},
> +	{"RANDOMBLOB", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_SCALAR, 0,
> +				sql_builtin_random_blob, NULL, false, true),
> +	}},
> +	{"REPLACE", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(3, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
> +				sql_builtin_replace, NULL, true, true),
> +	}},
> +	{"ROUND", 2, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_INTEGER, 0,
> +				sql_builtin_round, NULL, true, true),
> +	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_INTEGER, 0,
> +				sql_builtin_round, NULL, true, true),
> +	}},
> +	{"ROW_COUNT", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_INTEGER, 0,
> +				sql_builtin_row_count, NULL, true, true),
> +	}},
> +	{"SOME", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"SQRT", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"STRFTIME", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"SUBSTR", 2, {
> +	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
> +				sql_builtin_substr, NULL, true, true),
> +	 BUILTIN_FUNCTION_ENTRY(3, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
> +				sql_builtin_substr, NULL, true, true),
> +	}},
> +	{"SUM", 1, {
> +	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_NUMBER, 0,
> +				 sql_builtin_sum_step,
> +				 sql_builtin_sum_finalize, NULL, true),
> +	}},
> +	{"TIME", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"TOTAL", 1, {
> +	 BUILTIN_AGGREGATE_ENTRY(1, FIELD_TYPE_NUMBER, 0,
> +				 sql_builtin_sum_step,
> +				 sql_builtin_total_finalize, NULL, true),
> +	}},
> +	{"TRIM", 3, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
> +				sql_builtin_trim_one_arg, NULL,
> +				true, true),
> +	 BUILTIN_FUNCTION_ENTRY(2, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
> +				sql_builtin_trim_two_args, NULL,
> +				true, true),
> +	 BUILTIN_FUNCTION_ENTRY(3, FIELD_TYPE_STRING, SQL_FUNC_DERIVEDCOLL,
> +				sql_builtin_trim_three_args, NULL,
> +				true, true),
> +	}},
> +	{"TYPEOF", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, SQL_FUNC_TYPEOF,
> +				sql_builtin_typeof, NULL, true, true),
> +	}},
> +	{"UNICODE", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING, 0,
> +				sql_builtin_unicode, NULL, true, true),
> +	}},
> +	{"UPPER", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_STRING,
> +				SQL_FUNC_DERIVEDCOLL | SQL_FUNC_NEEDCOLL,
> +				sql_builtin_ICUUpper, NULL, true, true),
> +	}},
> +	{"VERSION", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(0, FIELD_TYPE_STRING, 0,
> +				sql_builtin_version, NULL, true, true),
> +	}},
> +	{"ZEROBLOB", 1, {
> +	 BUILTIN_FUNCTION_ENTRY(1, FIELD_TYPE_SCALAR, 0,
> +				sql_builtin_zeroblob, NULL, true, true),
> +	}},
> +	{"_sql_stat_get", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"_sql_stat_init", 1, {BUILTIN_STUB_ENTRY()}},
> +	{"_sql_stat_push", 1, {BUILTIN_STUB_ENTRY()}},
> +};
>  
>  struct func *
>  func_sql_builtin_new(struct func_def *def)
> @@ -2175,26 +2532,88 @@ func_sql_builtin_new(struct func_def *def)
>  			 "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");
> +	/** Binary search for corresponding builtin entry. */
> +	int idx = -1, left = 0, right = nelem(sql_builtins) - 1;
> +	while (left <= right) {
> +		uint32_t mid = (left + right) / 2;
> +		int rc = strcmp(def->name, sql_builtins[mid].name);
> +		if (rc == 0) {
> +			idx = mid;
> +			break;
> +		}
> +		if (rc < 0)
> +			right = mid - 1;
> +		else
> +			left = mid + 1;
> +	}
> +	if (idx == -1) {
> +		diag_set(ClientError, ER_CREATE_FUNCTION, def->name,
> +			 "unknown sql builtin name");
> +		return NULL;
> +	}
> +	int func_cnt = sql_builtins[idx].entries;
> +	struct func_sql_builtin *funcs =
> +		(struct func_sql_builtin *) malloc(sizeof(*funcs) * func_cnt);
> +	if (funcs == NULL) {
> +		diag_set(OutOfMemory, sizeof(*funcs) * func_cnt,
> +			 "malloc", "func");
>  		return NULL;
>  	}
> +	int func_idx = 0;
> +	for (; func_idx < func_cnt; func_idx++) {
> +		struct func_def *curr_def;
> +		if (func_idx == 0) {
> +			curr_def = def;
> +			rlist_create(&funcs[func_idx].base.signature);
> +		} else {
> +			curr_def = func_def_dup(def);
> +			if (curr_def == NULL)
> +				goto error;
> +			rlist_add(&funcs[0].base.signature,
> +				  &funcs[func_idx].base.signature);
> +		}
> +		curr_def->is_deterministic =
> +			sql_builtins[idx].entry[func_idx].is_deterministic;
> +		curr_def->sql_flags =
> +			sql_builtins[idx].entry[func_idx].sql_flags;
> +		curr_def->param_count =
> +			sql_builtins[idx].entry[func_idx].param_count;
> +		curr_def->returns =
> +			sql_builtins[idx].entry[func_idx].returns;
> +		curr_def->aggregate =
> +			sql_builtins[idx].entry[func_idx].aggregate;
> +		curr_def->exports.sql =
> +			sql_builtins[idx].entry[func_idx].is_available;
> +		funcs[func_idx].base.def = curr_def;
> +		funcs[func_idx].user_data =
> +			sql_builtins[idx].entry[func_idx].user_data;
> +		funcs[func_idx].finalize =
> +			sql_builtins[idx].entry[func_idx].finalize;
> +		funcs[func_idx].base.vtab = func_sql_builtin_vtab;
> +		funcs[func_idx].base.vtab.call =
> +			sql_builtins[idx].entry[func_idx].call;
> +	}
>  	/** Don't export SQL builtins in Lua for now. */
>  	def->exports.lua = false;
> -	func->base.vtab = func_sql_builtin_vtab;
> -	return &func->base;
> +	return &funcs[0].base;
> +error:
> +	for (int i = 1; i < func_idx; i++)
> +		free(funcs[func_idx].base.def);
> +	free(funcs);
> +	return NULL;
>  }
>  
>  static void
>  func_sql_builtin_destroy(struct func *base)
>  {
>  	assert(base != NULL && base->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
> +	struct func *func, *tmp;
> +	rlist_foreach_entry_safe(func, &base->signature, signature, tmp)
> +		free(func->def);
>  	free(base);
>  }
>  
>  static struct func_vtab func_sql_builtin_vtab = {
> -	.call = NULL,
> +	.call = sql_builtin_stub,
>  	.destroy = func_sql_builtin_destroy,
>  };
> diff --git a/src/box/sql/global.c b/src/box/sql/global.c
> index 6cadef809..c25b83de1 100644
> --- a/src/box/sql/global.c
> +++ b/src/box/sql/global.c
> @@ -162,13 +162,6 @@ SQL_WSD struct sqlConfig sqlConfig = {
>  	0x7ffffffe		/* iOnceResetThreshold */
>  };
>  
> -/*
> - * Hash table for global functions - functions common to all
> - * database connections.  After initialization, this table is
> - * read-only.
> - */
> -FuncDefHash sqlBuiltinFunctions;
> -
>  /*
>   * The value of the "pending" byte must be 0x40000000 (1 byte past the
>   * 1-gibabyte boundary) in a compatible database.  sql never uses
> diff --git a/src/box/sql/main.c b/src/box/sql/main.c
> index baaf7dcc9..16505ac7f 100644
> --- a/src/box/sql/main.c
> +++ b/src/box/sql/main.c
> @@ -127,9 +127,6 @@ sql_initialize(void)
>  	if (sqlGlobalConfig.isInit == 0
>  	    && sqlGlobalConfig.inProgress == 0) {
>  		sqlGlobalConfig.inProgress = 1;
> -		memset(&sqlBuiltinFunctions, 0,
> -		       sizeof(sqlBuiltinFunctions));
> -		sqlRegisterBuiltinFunctions();
>  		sql_os_init();
>  		sqlGlobalConfig.isInit = 1;
>  		sqlGlobalConfig.inProgress = 0;
> @@ -177,25 +174,6 @@ sqlCloseSavepoints(Vdbe * pVdbe)
>  	pVdbe->anonymous_savepoint = NULL;
>  }
>  
> -/*
> - * Invoke the destructor function associated with FuncDef p, if any. Except,
> - * if this is not the last copy of the function, do not invoke it. Multiple
> - * copies of a single function are created when create_function() is called
> - * with SQL_ANY as the encoding.
> - */
> -static void
> -functionDestroy(sql * db, FuncDef * p)
> -{
> -	FuncDestructor *pDestructor = p->u.pDestructor;
> -	if (pDestructor) {
> -		pDestructor->nRef--;
> -		if (pDestructor->nRef == 0) {
> -			pDestructor->xDestroy(pDestructor->pUserData);
> -			sqlDbFree(db, pDestructor);
> -		}
> -	}
> -}
> -
>  /*
>   * Rollback all database files.  If tripCode is not 0, then
>   * any write cursors are invalidated ("tripped" - as in "tripping a circuit
> @@ -214,118 +192,6 @@ sqlRollbackAll(Vdbe * pVdbe)
>  	}
>  }
>  
> -/*
> - * This function is exactly the same as sql_create_function(), except
> - * that it is designed to be called by internal code. The difference is
> - * that if a malloc() fails in sql_create_function(), an error code
> - * is returned and the mallocFailed flag cleared.
> - */
> -int
> -sqlCreateFunc(struct sql * db, const char *zFunctionName, enum field_type type,
> -	      int nArg, int flags, void *pUserData,
> -	      int (*xSFunc) (struct func *func, struct port *args,
> -			     struct port *ret),
> -	      int (*xStep) (struct func *func, struct port *args,
> -			    struct port *ret),
> -	      int (*xFinal) (struct func *func, struct port *args,
> -			     struct port *ret),
> -	      struct FuncDestructor * pDestructor)
> -{
> -	FuncDef *p;
> -	int extraFlags;
> -
> -	if (zFunctionName == 0 ||
> -	    (xSFunc && (xFinal || xStep)) ||
> -	    (!xSFunc && (xFinal && !xStep)) ||
> -	    (!xSFunc && (!xFinal && xStep)) ||
> -	    (nArg < -1 || nArg > SQL_MAX_FUNCTION_ARG) ||
> -	    (255 < (sqlStrlen30(zFunctionName)))) {
> -		diag_set(ClientError, ER_CREATE_FUNCTION, zFunctionName,
> -			 "wrong function definition");
> -		return -1;
> -	}
> -
> -	assert(SQL_FUNC_CONSTANT == SQL_DETERMINISTIC);
> -	extraFlags = flags & SQL_DETERMINISTIC;
> -
> -
> -	/* Check if an existing function is being overridden or deleted. If so,
> -	 * and there are active VMs, then return an error. If a function
> -	 * is being overridden/deleted but there are no active VMs, allow the
> -	 * operation to continue but invalidate all precompiled statements.
> -	 */
> -	p = sqlFindFunction(db, zFunctionName, nArg, 0);
> -	if (p && p->nArg == nArg) {
> -		if (db->nVdbeActive) {
> -			diag_set(ClientError, ER_CREATE_FUNCTION, zFunctionName,
> -				 "unable to create function due to active "\
> -				 "statements");
> -			return -1;
> -		} else {
> -			sqlExpirePreparedStatements(db);
> -		}
> -	}
> -
> -	p = sqlFindFunction(db, zFunctionName, nArg, 1);
> -	assert(p || db->mallocFailed);
> -	if (p == NULL)
> -		return -1;
> -
> -	/* If an older version of the function with a configured destructor is
> -	 * being replaced invoke the destructor function here.
> -	 */
> -	functionDestroy(db, p);
> -
> -	if (pDestructor) {
> -		pDestructor->nRef++;
> -	}
> -	p->u.pDestructor = pDestructor;
> -	p->funcFlags = extraFlags;
> -	testcase(p->funcFlags & SQL_DETERMINISTIC);
> -	p->xSFunc = xSFunc ? xSFunc : xStep;
> -	p->xFinalize = xFinal;
> -	p->pUserData = pUserData;
> -	p->nArg = (u16) nArg;
> -	p->ret_type = type;
> -	return 0;
> -}
> -
> -
> -int
> -sql_create_function_v2(struct sql * db, const char *zFunc, enum field_type type,
> -		       int nArg, int flags, void *p,
> -		       int (*xSFunc) (struct func *func, struct port *args,
> -				      struct port *ret),
> -		       int (*xStep) (struct func *func, struct port *args,
> -				     struct port *ret),
> -		       int (*xFinal) (struct func *func, struct port *args,
> -				      struct port *ret),
> -		       void (*xDestroy) (void *ctx))
> -{
> -	FuncDestructor *pArg = 0;
> -
> -	if (xDestroy) {
> -		pArg =
> -		    (FuncDestructor *) sqlDbMallocZero(db,
> -							   sizeof
> -							   (FuncDestructor));
> -		if (!pArg) {
> -			xDestroy(p);
> -			return -1;
> -		}
> -		pArg->xDestroy = xDestroy;
> -		pArg->pUserData = p;
> -	}
> -	int rc = sqlCreateFunc(db, zFunc, type, nArg, flags, p, xSFunc, xStep,
> -			       xFinal, pArg);
> -	if (pArg && pArg->nRef == 0) {
> -		assert(rc != 0);
> -		xDestroy(p);
> -		sqlDbFree(db, pArg);
> -	}
> -	return rc;
> -}
> -
>  /*
>   * This array defines hard upper bounds on limit values.  The
>   * initializer must be kept in sync with the SQL_LIMIT_*
> diff --git a/src/box/sql/resolve.c b/src/box/sql/resolve.c
> index 7c81b1980..7c67d5d56 100644
> --- a/src/box/sql/resolve.c
> +++ b/src/box/sql/resolve.c
> @@ -38,6 +38,8 @@
>  #include "sqlInt.h"
>  #include <stdlib.h>
>  #include <string.h>
> +#include "box/func.h"
> +#include "box/field_def.h"
>  
>  /*
>   * Walk the expression tree pExpr and increase the aggregate function
> @@ -576,36 +578,31 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
>  			int no_such_func = 0;	/* True if no such function exists */
>  			int wrong_num_args = 0;	/* True if wrong number of arguments */
>  			int is_agg = 0;	/* True if is an aggregate function */
> -			int nId;	/* Number of characters in function name */
> -			const char *zId;	/* The function name. */
> -			FuncDef *pDef;	/* Information about the function */
> -
>  			assert(!ExprHasProperty(pExpr, EP_xIsSelect));
> -			zId = pExpr->u.zToken;
> -			nId = sqlStrlen30(zId);
> -			pDef = sqlFindFunction(pParse->db, zId, n, 0);
> -			if (pDef == 0) {
> -				pDef =
> -				    sqlFindFunction(pParse->db, zId, -2,0);
> -				if (pDef == 0) {
> +			const char *name = pExpr->u.zToken;
> +			uint32_t name_len = strlen(name);
> +			struct func *func =
> +				sql_func_by_signature(name, name_len, n);
> +			if (func == 0) {
> +				func = sql_func_by_signature(name, name_len, -2);
> +				if (func == NULL) {
>  					no_such_func = 1;
>  				} else {
>  					wrong_num_args = 1;
>  				}
>  			} else {
> -				is_agg = pDef->xFinalize != 0;
> -				pExpr->type = pDef->ret_type;
> -				if (pDef->
> -				    funcFlags & (SQL_FUNC_CONSTANT |
> -						 SQL_FUNC_SLOCHNG)) {
> +				is_agg = func->def->language ==
> +					 FUNC_LANGUAGE_SQL_BUILTIN &&
> +					 func->def->aggregate ==
> +					 FUNC_AGGREGATE_GROUP;
> +				pExpr->type = func->def->returns;
> +				if (func->def->is_deterministic) {
>  					/* For the purposes of the EP_ConstFunc flag, date and time
>  					 * functions and other functions that change slowly are considered
>  					 * constant because they are constant for the duration of one query
>  					 */
>  					ExprSetProperty(pExpr, EP_ConstFunc);
> -				}
> -				if ((pDef->funcFlags & SQL_FUNC_CONSTANT) ==
> -				    0) {
> +				} else {
>  					/* Date/time functions that use 'now', and other functions
>  					 * that might change over time cannot be used
>  					 * in an index.
> @@ -614,22 +611,21 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
>  				}
>  			}
>  			if (is_agg && (pNC->ncFlags & NC_AllowAgg) == 0) {
> -				const char *err =
> -					tt_sprintf("misuse of aggregate "\
> -						   "function %.*s()", nId, zId);
> -				diag_set(ClientError, ER_SQL_PARSER_GENERIC, err);
> +				diag_set(ClientError, ER_SQL_PARSER_GENERIC,
> +					 tt_sprintf("misuse of aggregate "
> +						    "function %s()", name));
>  				pParse->is_aborted = true;
>  				pNC->nErr++;
>  				is_agg = 0;
>  			} else if (no_such_func && pParse->db->init.busy == 0) {
> -				diag_set(ClientError, ER_NO_SUCH_FUNCTION, zId);
> +				diag_set(ClientError, ER_NO_SUCH_FUNCTION,
> +					 name);
>  				pParse->is_aborted = true;
>  				pNC->nErr++;
>  			} else if (wrong_num_args) {
> -				const char *err = "wrong number of arguments "\
> -						  "to function %.*s()";
>  				diag_set(ClientError, ER_SQL_PARSER_GENERIC,
> -					 tt_sprintf(err, nId, zId));
> +					 tt_sprintf("wrong number of arguments "
> +						    "to function %s()", name));
>  				pParse->is_aborted = true;
>  				pNC->nErr++;
>  			}
> @@ -648,7 +644,7 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
>  					pExpr->op2++;
>  					pNC2 = pNC2->pNext;
>  				}
> -				assert(pDef != 0);
> +				assert(func != NULL);
>  				if (pNC2) {
>  					assert(SQL_FUNC_MINMAX ==
>  					       NC_MinMaxAgg);
> @@ -656,8 +652,7 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
>  						  funcFlags &
>  						  SQL_FUNC_MINMAX) != 0);
>  					pNC2->ncFlags |=
> -					    NC_HasAgg | (pDef->
> -							 funcFlags &
> +					    NC_HasAgg | (func->def->sql_flags &
>  							 SQL_FUNC_MINMAX);
>  
>  				}
> diff --git a/src/box/sql/select.c b/src/box/sql/select.c
> index 7c8da251e..70b39ff30 100644
> --- a/src/box/sql/select.c
> +++ b/src/box/sql/select.c
> @@ -4374,7 +4374,7 @@ is_simple_count(struct Select *select, struct AggInfo *agg_info)
>  		return NULL;
>  	if (NEVER(agg_info->nFunc == 0))
>  		return NULL;
> -	if ((agg_info->aFunc[0].pFunc->funcFlags & SQL_FUNC_COUNT) == 0)
> +	if ((agg_info->aFunc[0].func->def->sql_flags & SQL_FUNC_COUNT) == 0)
>  		return NULL;
>  	if (expr->flags & EP_Distinct)
>  		return NULL;
> @@ -5269,7 +5269,7 @@ finalizeAggFunctions(Parse * pParse, AggInfo * pAggInfo)
>  		assert(!ExprHasProperty(pF->pExpr, EP_xIsSelect));
>  		sqlVdbeAddOp2(v, OP_AggFinal, pF->iMem,
>  				  pList ? pList->nExpr : 0);
> -		sqlVdbeAppendP4(v, pF->pFunc, P4_FUNCDEF);
> +		sqlVdbeAppendP4(v, pF->func, P4_FUNC);
>  	}
>  }
>  
> @@ -5310,11 +5310,11 @@ updateAccumulator(Parse * pParse, AggInfo * pAggInfo)
>  			vdbe_insert_distinct(pParse, pF->iDistinct, pF->reg_eph,
>  					     addrNext, 1, regAgg);
>  		}
> -		if (pF->pFunc->funcFlags & SQL_FUNC_NEEDCOLL) {
> +		if ((pF->func->def->sql_flags & SQL_FUNC_NEEDCOLL) != 0) {
>  			struct coll *coll = NULL;
>  			struct ExprList_item *pItem;
>  			int j;
> -			assert(pList != 0);	/* pList!=0 if pF->pFunc has NEEDCOLL */
> +			assert(pList != 0);
>  			bool unused;
>  			uint32_t id;
>  			for (j = 0, pItem = pList->a; coll == NULL && j < nArg;
> @@ -5329,7 +5329,7 @@ updateAccumulator(Parse * pParse, AggInfo * pAggInfo)
>  					  (char *)coll, P4_COLLSEQ);
>  		}
>  		sqlVdbeAddOp3(v, OP_AggStep0, 0, regAgg, pF->iMem);
> -		sqlVdbeAppendP4(v, pF->pFunc, P4_FUNCDEF);
> +		sqlVdbeAppendP4(v, pF->func, P4_FUNC);
>  		sqlVdbeChangeP5(v, (u8) nArg);
>  		sql_expr_type_cache_change(pParse, regAgg, nArg);
>  		sqlReleaseTempRange(pParse, regAgg, nArg);
> diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
> index ce15500e5..dc0a06f5a 100644
> --- a/src/box/sql/sqlInt.h
> +++ b/src/box/sql/sqlInt.h
> @@ -520,19 +520,6 @@ sql_initialize(void);
>  #define SQL_TRACE_ROW        0x04
>  #define SQL_TRACE_CLOSE      0x08
>  
> -#define SQL_DETERMINISTIC    0x800
> -
> -int
> -sql_create_function_v2(struct sql * db, const char *zFunc, enum field_type type,
> -		       int nArg, int flags, void *p,
> -		       int (*xSFunc) (struct func *func, struct port *args,
> -				      struct port *ret),
> -		       int (*xStep) (struct func *func, struct port *args,
> -				     struct port *ret),
> -		       int (*xFinal) (struct func *func, struct port *args,
> -				      struct port *ret),
> -		       void (*xDestroy) (void *ctx));
> -
>  #define SQL_OPEN_READONLY         0x00000001	/* Ok for sql_open_v2() */
>  #define SQL_OPEN_READWRITE        0x00000002	/* Ok for sql_open_v2() */
>  #define SQL_OPEN_CREATE           0x00000004	/* Ok for sql_open_v2() */
> @@ -983,9 +970,6 @@ typedef struct Column Column;
>  typedef struct Expr Expr;
>  typedef struct ExprList ExprList;
>  typedef struct ExprSpan ExprSpan;
> -typedef struct FuncDestructor FuncDestructor;
> -typedef struct FuncDef FuncDef;
> -typedef struct FuncDefHash FuncDefHash;
>  typedef struct IdList IdList;
>  typedef struct KeyClass KeyClass;
>  typedef struct NameContext NameContext;
> @@ -1030,18 +1014,6 @@ typedef int VList;
>   */
>  #define SQL_N_LIMIT (SQL_LIMIT_TRIGGER_DEPTH+1)
>  
> -/*
> - * A hash table for built-in function definitions.  (Application-defined
> - * functions use a regular table table from hash.h.)
> - *
> - * Hash each FuncDef structure into one of the FuncDefHash.a[] slots.
> - * Collisions are on the FuncDef.u.pHash chain.
> - */
> -#define SQL_FUNC_HASH_SZ 23
> -struct FuncDefHash {
> -	FuncDef *a[SQL_FUNC_HASH_SZ];	/* Hash table for functions */
> -};
> -
>  /*
>   * Each database connection is an instance of the following structure.
>   */
> @@ -1152,62 +1124,14 @@ struct type_def {
>  };
>  
>  /*
> - * Each SQL function is defined by an instance of the following
> - * structure.  For global built-in functions (ex: substr(), max(), count())
> - * a pointer to this structure is held in the sqlBuiltinFunctions object.
> - * For per-connection application-defined functions, a pointer to this
> - * structure is held in the db->aHash hash table.
> - *
> - * The u.pHash field is used by the global built-ins.  The u.pDestructor
> - * field is used by per-connection app-def functions.
> - */
> -struct FuncDef {
> -	i8 nArg;		/* Number of arguments.  -1 means unlimited */
> -	u16 funcFlags;		/* Some combination of sql_FUNC_* */
> -	void *pUserData;	/* User data parameter */
> -	FuncDef *pNext;		/* Next function with same name */
> -	int (*xSFunc) (struct func *func, struct port *args, struct port *ret);
> -	int (*xFinalize) (struct func *func, struct port *args, struct port *ret);
> -	const char *zName;	/* SQL name of the function. */
> -	union {
> -		FuncDef *pHash;	/* Next with a different name but the same hash */
> -		FuncDestructor *pDestructor;	/* Reference counted destructor function */
> -	} u;
> -	/* Return type. */
> -	enum field_type ret_type;
> -};
> -
> -/*
> - * This structure encapsulates a user-function destructor callback (as
> - * configured using create_function_v2()) and a reference counter. When
> - * create_function_v2() is called to create a function with a destructor,
> - * a single object of this type is allocated. FuncDestructor.nRef is set to
> - * the number of FuncDef objects created (either 1 or 3, depending on whether
> - * or not the specified encoding is sql_ANY). The FuncDef.pDestructor
> - * member of each of the new FuncDef objects is set to point to the allocated
> - * FuncDestructor.
> - *
> - * Thereafter, when one of the FuncDef objects is deleted, the reference
> - * count on this object is decremented. When it reaches 0, the destructor
> - * is invoked and the FuncDestructor structure freed.
> - */
> -struct FuncDestructor {
> -	int nRef;
> -	void (*xDestroy) (void *);
> -	void *pUserData;
> -};
> -
> -/*
> - * Possible values for FuncDef.flags.  Note that the _LENGTH and _TYPEOF
> - * values must correspond to OPFLAG_LENGTHARG and OPFLAG_TYPEOFARG.  And
> - * sql_FUNC_CONSTANT must be the same as sql_DETERMINISTIC.  There
> - * are assert() statements in the code to verify this.
> + * Possible values for FuncDef.flags.  Note that the _LENGTH and
> + * _TYPEOF values must correspond to OPFLAG_LENGTHARG and
> + * OPFLAG_TYPEOFARG.
>   *
>   * Value constraints (enforced via assert()):
>   *     SQL_FUNC_MINMAX    ==  NC_MinMaxAgg      == SF_MinMaxAgg
>   *     SQL_FUNC_LENGTH    ==  OPFLAG_LENGTHARG
>   *     SQL_FUNC_TYPEOF    ==  OPFLAG_TYPEOFARG
> - *     SQL_FUNC_CONSTANT  ==  sql_DETERMINISTIC from the API
>   */
>  #define SQL_FUNC_LIKE     0x0004	/* Candidate for the LIKE optimization */
>  #define SQL_FUNC_EPHEM    0x0010	/* Ephemeral.  Delete with VDBE */
> @@ -1229,11 +1153,7 @@ struct FuncDestructor {
>   * argument.
>   */
>  #define SQL_FUNC_DERIVEDCOLL 0x0400
> -#define SQL_FUNC_CONSTANT 0x0800	/* Constant inputs give a constant output */
>  #define SQL_FUNC_MINMAX   0x1000	/* True for min() and max() aggregates */
> -#define SQL_FUNC_SLOCHNG  0x2000	/* "Slow Change". Value constant during a
> -					 * single query - might change over time
> -					 */
>  
>  /*
>   * Trim side mask components. TRIM_LEADING means to trim left side
> @@ -1246,72 +1166,6 @@ enum trim_side_mask {
>  	TRIM_BOTH = TRIM_LEADING | TRIM_TRAILING
>  };
>  
> -/*
> - * The following three macros, FUNCTION(), LIKEFUNC() and AGGREGATE() are
> - * used to create the initializers for the FuncDef structures.
> - *
> - *   FUNCTION(zName, nArg, iArg, bNC, xFunc)
> - *     Used to create a scalar function definition of a function zName
> - *     implemented by C function xFunc that accepts nArg arguments. The
> - *     value passed as iArg is cast to a (void*) and made available
> - *     as the user-data (sql_user_data()) for the function. If
> - *     argument bNC is true, then the sql_FUNC_NEEDCOLL flag is set.
> - *
> - *   FUNCTION_COLL
> - *     Like FUNCTION except it assumes that function returns
> - *     STRING which collation should be derived from first
> - *     argument (trim, substr etc).
> - *
> - *   VFUNCTION(zName, nArg, iArg, bNC, xFunc)
> - *     Like FUNCTION except it omits the sql_FUNC_CONSTANT flag.
> - *
> - *   DFUNCTION(zName, nArg, iArg, bNC, xFunc)
> - *     Like FUNCTION except it omits the sql_FUNC_CONSTANT flag and
> - *     adds the sql_FUNC_SLOCHNG flag.  Used for date & time functions,
> - *     but not during a single query.
> - *
> - *   AGGREGATE(zName, nArg, iArg, bNC, xStep, xFinal)
> - *     Used to create an aggregate function definition implemented by
> - *     the C functions xStep and xFinal. The first four parameters
> - *     are interpreted in the same way as the first 4 parameters to
> - *     FUNCTION().
> - *
> - *   LIKEFUNC(zName, nArg, pArg, flags)
> - *     Used to create a scalar function definition of a function zName
> - *     that accepts nArg arguments and is implemented by a call to C
> - *     function likeFunc. Argument pArg is cast to a (void *) and made
> - *     available as the function user-data (sql_user_data()). The
> - *     FuncDef.flags variable is set to the value passed as the flags
> - *     parameter.
> - */
> -#define FUNCTION(zName, nArg, iArg, bNC, xFunc, type) \
> -  {nArg, SQL_FUNC_CONSTANT|(bNC*SQL_FUNC_NEEDCOLL), \
> -   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
> -#define FUNCTION_COLL(zName, nArg, iArg, bNC, xFunc) \
> -  {nArg, SQL_FUNC_CONSTANT|SQL_FUNC_DERIVEDCOLL|(bNC*SQL_FUNC_NEEDCOLL), \
> -   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, FIELD_TYPE_STRING}
> -#define VFUNCTION(zName, nArg, iArg, bNC, xFunc, type) \
> -  {nArg, (bNC*SQL_FUNC_NEEDCOLL), \
> -   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
> -#define DFUNCTION(zName, nArg, iArg, bNC, xFunc, type) \
> -  {nArg, SQL_FUNC_SLOCHNG|(bNC*SQL_FUNC_NEEDCOLL), \
> -   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
> -#define FUNCTION2(zName, nArg, iArg, bNC, xFunc, extraFlags, type) \
> -  {nArg,SQL_FUNC_CONSTANT|(bNC*SQL_FUNC_NEEDCOLL)|extraFlags,\
> -   SQL_INT_TO_PTR(iArg), 0, xFunc, 0, #zName, {0}, type}
> -#define STR_FUNCTION(zName, nArg, pArg, bNC, xFunc) \
> -  {nArg, SQL_FUNC_SLOCHNG|(bNC*SQL_FUNC_NEEDCOLL), \
> -   pArg, 0, xFunc, 0, #zName, {SQL_AFF_STRING, {0}}}
> -#define LIKEFUNC(zName, nArg, arg, flags, type) \
> -  {nArg, SQL_FUNC_CONSTANT|flags, \
> -   (void *)(SQL_INT_TO_PTR(arg)), 0, likeFunc, 0, #zName, {0}, type}
> -#define AGGREGATE(zName, nArg, arg, nc, xStep, xFinal, type) \
> -  {nArg, (nc*SQL_FUNC_NEEDCOLL), \
> -   SQL_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}, type}
> -#define AGGREGATE2(zName, nArg, arg, nc, xStep, xFinal, extraFlags, type) \
> -  {nArg, (nc*SQL_FUNC_NEEDCOLL)|extraFlags, \
> -   SQL_INT_TO_PTR(arg), 0, xStep,xFinal,#zName, {0}, type}
> -
>  /*
>   * All current savepoints are stored in a linked list starting at
>   * sql.pSavepoint. The first element in the list is the most recently
> @@ -1504,7 +1358,8 @@ struct AggInfo {
>  				 */
>  	struct AggInfo_func {	/* For each aggregate function */
>  		Expr *pExpr;	/* Expression encoding the function */
> -		FuncDef *pFunc;	/* The aggregate function implementation */
> +		/** The aggregate function implementation. */
> +		struct func *func;
>  		int iMem;	/* Memory location that acts as accumulator */
>  		int iDistinct;	/* Ephemeral table used to enforce DISTINCT */
>  		/**
> @@ -1654,7 +1509,8 @@ struct Expr {
>  #define EP_Static    0x008000	/* Held in memory not obtained from malloc() */
>  #define EP_MemToken  0x010000	/* Need to sqlDbFree() Expr.zToken */
>  #define EP_NoReduce  0x020000	/* Cannot EXPRDUP_REDUCE this Expr */
> -#define EP_ConstFunc 0x080000	/* A sql_FUNC_CONSTANT or _SLOCHNG function */
> +/** A deterministic function. */
> +#define EP_ConstFunc 0x080000
>  #define EP_CanBeNull 0x100000	/* Can be null despite NOT NULL constraint */
>  #define EP_Subquery  0x200000	/* Tree contains a TK_SELECT operator */
>  #define EP_Alias     0x400000	/* Is an alias for a result set column */
> @@ -3462,10 +3318,6 @@ void sqlSelectSetName(Select *, const char *);
>  #else
>  #define sqlSelectSetName(A,B)
>  #endif
> -void sqlInsertBuiltinFuncs(FuncDef *, int);
> -FuncDef *sqlFindFunction(sql *, const char *, int, u8);
> -void sqlRegisterBuiltinFunctions(void);
> -void sqlRegisterDateTimeFunctions(void);
>  
>  /**
>   * Evaluate a view and store its result in an ephemeral table.
> @@ -4037,7 +3889,6 @@ extern const unsigned char sqlUpperToLower[];
>  extern const unsigned char sqlCtypeMap[];
>  extern const Token sqlIntTokens[];
>  extern SQL_WSD struct sqlConfig sqlConfig;
> -extern FuncDefHash sqlBuiltinFunctions;
>  extern int sqlPendingByte;
>  
>  /**
> @@ -4218,18 +4069,7 @@ sql_key_info_to_key_def(struct sql_key_info *key_info);
>   * @retval 1 if LIKE optimization can be used, 0 otherwise.
>   */
>  int
> -sql_is_like_func(struct sql *db, struct Expr *expr, int *is_like_ci);
> -
> -int
> -sqlCreateFunc(struct sql * db, const char *zFunctionName, enum field_type type,
> -	      int nArg, int flags, void *pUserData,
> -	      int (*xSFunc) (struct func *func, struct port *args,
> -			     struct port *ret),
> -	      int (*xStep) (struct func *func, struct port *args,
> -			    struct port *ret),
> -	      int (*xFinal) (struct func *func, struct port *args,
> -			     struct port *ret),
> -	      struct FuncDestructor * pDestructor);
> +sql_is_like_func(struct Expr *expr, int *is_like_ci);
>  
>  /** Set OOM error flag. */
>  static inline void
> @@ -4456,10 +4296,6 @@ Expr *sqlExprForVectorField(Parse *, Expr *, int);
>   */
>  extern int sqlSubProgramsRemaining;
>  
> -/** Register built-in functions to work with ANALYZE data. */
> -void
> -sql_register_analyze_builtins(void);
> -
>  /**
>   * Generate VDBE code to halt execution with correct error if
>   * the object with specified key is already present (or doesn't
> diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
> index 92c99877a..eb18d4b7a 100644
> --- a/src/box/sql/vdbe.c
> +++ b/src/box/sql/vdbe.c
> @@ -1722,14 +1722,14 @@ case OP_CollSeq: {
>  case OP_Function0: {
>  	sql_context *pCtx;
>  
> -	assert(pOp->p4type==P4_FUNCDEF);
> +	assert(pOp->p4type == P4_FUNC);
>  	assert(pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor));
>  	assert(pOp->p5 == 0 ||
>  	       (pOp->p2 > 0 && pOp->p2 + pOp->p5 <= (p->nMem+1 - p->nCursor)+1));
>  	assert(pOp->p3 < pOp->p2 || pOp->p3>=pOp->p2 + pOp->p5);
>  	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx));
>  	if (pCtx==0) goto no_mem;
> -	pCtx->pFunc = pOp->p4.pFunc;
> +	pCtx->func = pOp->p4.func;
>  	pCtx->iOp = (int)(pOp - aOp);
>  	pCtx->pVdbe = p;
>  	pOp->p4type = P4_FUNCCTX;
> @@ -1764,7 +1764,7 @@ case OP_Function: {
>  	}
>  #endif
>  	MemSetTypeFlag(pOut, MEM_Null);
> -	rc = (*pCtx->pFunc->xSFunc)(NULL, &args, &ret);
> +	rc = func_call(pCtx->func, &args, &ret);
>  	if (rc != 0)
>  		goto abort_due_to_error;
>  	uint32_t size;
> @@ -4969,15 +4969,18 @@ case OP_DecrJumpZero: {      /* jump, in1 */
>  case OP_AggStep0: {
>  	sql_context *pCtx;
>  
> -	assert(pOp->p4type == P4_FUNCDEF);
> +	assert(pOp->p4type == P4_FUNC);
>  	assert(pOp->p3>0 && pOp->p3<=(p->nMem+1 - p->nCursor));
>  	assert(pOp->p5 == 0 ||
>  	       (pOp->p2 > 0 && pOp->p2 + pOp->p5 <= (p->nMem+1 - p->nCursor)+1));
> -	assert(pOp->p3 < pOp->p2 || pOp->p3 >= pOp->p2 + pOp->p5);
> +	assert(pOp->p3<pOp->p2 || pOp->p3>=pOp->p2 + pOp->p5);
> +	assert(pOp->p4.func != NULL &&
> +	       pOp->p4.func->def->language == FUNC_LANGUAGE_SQL_BUILTIN &&
> +	       pOp->p4.func->def->aggregate == FUNC_AGGREGATE_GROUP);
>  	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx));
>  	if (pCtx==0) goto no_mem;
>  	pCtx->pMem = 0;
> -	pCtx->pFunc = pOp->p4.pFunc;
> +	pCtx->func = pOp->p4.func;
>  	pCtx->iOp = (int)(pOp - aOp);
>  	pCtx->pVdbe = p;
>  	pOp->p4type = P4_FUNCCTX;
> @@ -5011,7 +5014,7 @@ case OP_AggStep: {
>  	sqlVdbeMemInit(&t, db, MEM_Null);
>  	pOut = &t;
>  	pCtx->skipFlag = 0;
> -	rc = (*pCtx->pFunc->xSFunc)(NULL, &args, &ret);
> +	rc = func_call(pCtx->func, &args, &ret);
>  	region_truncate(region, region_svp);
>  	if (rc != 0)
>  		goto abort_due_to_error;
> @@ -5042,7 +5045,7 @@ case OP_AggFinal: {
>  	assert(pOp->p1>0 && pOp->p1<=(p->nMem+1 - p->nCursor));
>  	pMem = &aMem[pOp->p1];
>  	assert((pMem->flags & ~(MEM_Null|MEM_Agg))==0);
> -	if (sqlVdbeMemFinalize(pMem, pOp->p4.pFunc) != 0)
> +	if (sql_vdbemem_finilize(pMem, pOp->p4.func) != 0)
>  		goto abort_due_to_error;
>  	UPDATE_MAX_BLOBSIZE(pMem);
>  	if (sqlVdbeMemTooBig(pMem)) {
> diff --git a/src/box/sql/vdbe.h b/src/box/sql/vdbe.h
> index 4f94643cb..31a0a39d7 100644
> --- a/src/box/sql/vdbe.h
> +++ b/src/box/sql/vdbe.h
> @@ -72,7 +72,8 @@ struct VdbeOp {
>  		char *z;	/* Pointer to data for string (char array) types */
>  		i64 *pI64;	/* Used when p4type is P4_INT64 */
>  		double *pReal;	/* Used when p4type is P4_REAL */
> -		FuncDef *pFunc;	/* Used when p4type is P4_FUNCDEF */
> +		/* Used when p4type is P4_FUNC. */
> +		struct func *func;
>  		sql_context *pCtx;	/* Used when p4type is P4_FUNCCTX */
>  		struct coll *pColl;	/* Used when p4type is P4_COLLSEQ */
>  		Mem *pMem;	/* Used when p4type is P4_MEM */
> @@ -122,7 +123,8 @@ struct SubProgram {
>  #define P4_DYNAMIC  (-1)	/* Pointer to a string obtained from sqlMalloc() */
>  #define P4_STATIC   (-2)	/* Pointer to a static string */
>  #define P4_COLLSEQ  (-3)	/* P4 is a pointer to a CollSeq structure */
> -#define P4_FUNCDEF  (-4)	/* P4 is a pointer to a FuncDef structure */
> +/** P4 is a pointer to a function object. */
> +#define P4_FUNC  (-4)
>  #define P4_MEM      (-7)	/* P4 is a pointer to a Mem*    structure */
>  #define P4_TRANSIENT  0		/* P4 is a pointer to a transient string */
>  #define P4_REAL     (-9)	/* P4 is a 64-bit floating point value */
> diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
> index 4c8363a22..0d4acb0eb 100644
> --- a/src/box/sql/vdbeInt.h
> +++ b/src/box/sql/vdbeInt.h
> @@ -46,6 +46,8 @@
>   */
>  typedef struct VdbeOp Op;
>  
> +struct func;
> +
>  /*
>   * Boolean values
>   */
> @@ -167,7 +169,8 @@ struct Mem {
>  		bool b;         /* Boolean value used when MEM_Bool is set in flags */
>  		int nZero;	/* Used when bit MEM_Zero is set in flags */
>  		void *p;	/* Generic pointer */
> -		FuncDef *pDef;	/* Used only when flags==MEM_Agg */
> +		/** Used only when flags==MEM_Agg. */
> +		struct func *func;
>  		VdbeFrame *pFrame;	/* Used when flags==MEM_Frame */
>  	} u;
>  	u32 flags;		/* Some combination of MEM_Null, MEM_Str, MEM_Dyn, etc. */
> @@ -291,7 +294,8 @@ mem_apply_numeric_type(struct Mem *record);
>   * (Mem) which are only defined there.
>   */
>  struct sql_context {
> -	FuncDef *pFunc;		/* Pointer to function information */
> +	/** Pointer to function object. */
> +	struct func *func;
>  	Mem *pMem;		/* Memory cell used to store aggregate context */
>  	Vdbe *pVdbe;		/* The VM that owns this context */
>  	int iOp;		/* Instruction number of OP_Function */
> @@ -472,7 +476,17 @@ int sqlVdbeMemNumerify(Mem *);
>  int sqlVdbeMemCast(Mem *, enum field_type type);
>  int sqlVdbeMemFromBtree(BtCursor *, u32, u32, Mem *);
>  void sqlVdbeMemRelease(Mem * p);
> -int sqlVdbeMemFinalize(Mem *, FuncDef *);
> +
> +/**
> + * Memory cell pMem contains the context of an aggregate function.
> + * This routine calls the finalize method for that function.  The
> + * result of the aggregate is stored back into pMem.
> + *
> + * Returns -1 if the finalizer reports an error. 0 otherwise.
> + */
> +int
> +sql_vdbemem_finilize(struct Mem *mem, struct func *func);
> +
>  const char *sqlOpcodeName(int);
>  int sqlVdbeMemGrow(Mem * pMem, int n, int preserve);
>  int sqlVdbeMemClearAndResize(Mem * pMem, int n);
> diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
> index d014e8258..c59b740a7 100644
> --- a/src/box/sql/vdbeapi.c
> +++ b/src/box/sql/vdbeapi.c
> @@ -370,8 +370,9 @@ sql_step(sql_stmt * pStmt)
>  void *
>  sql_user_data(sql_context * p)
>  {
> -	assert(p && p->pFunc);
> -	return p->pFunc->pUserData;
> +	assert(p != NULL && p->func != NULL &&
> +	       p->func->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
> +	return ((struct func_sql_builtin *)p->func)->user_data;
>  }
>  
>  /*
> @@ -416,7 +417,7 @@ createAggContext(sql_context * p, int nByte)
>  	} else {
>  		sqlVdbeMemClearAndResize(pMem, nByte);
>  		pMem->flags = MEM_Agg;
> -		pMem->u.pDef = p->pFunc;
> +		pMem->u.func = p->func;
>  		if (pMem->z) {
>  			memset(pMem->z, 0, nByte);
>  		}
> @@ -432,7 +433,9 @@ createAggContext(sql_context * p, int nByte)
>  void *
>  sql_aggregate_context(sql_context * p, int nByte)
>  {
> -	assert(p && p->pFunc && p->pFunc->xFinalize);
> +	assert(p != NULL && p->func != NULL &&
> +	       p->func->def->language == FUNC_LANGUAGE_SQL_BUILTIN &&
> +	       p->func->def->aggregate == FUNC_AGGREGATE_GROUP);
>  	testcase(nByte < 0);
>  	if ((p->pMem->flags & MEM_Agg) == 0) {
>  		return createAggContext(p, nByte);
> diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
> index baeeb4624..2c6f5b279 100644
> --- a/src/box/sql/vdbeaux.c
> +++ b/src/box/sql/vdbeaux.c
> @@ -39,6 +39,8 @@
>  #include "box/schema.h"
>  #include "box/tuple_format.h"
>  #include "box/txn.h"
> +#include "box/func.h"
> +#include "box/func_def.h"
>  #include "msgpuck/msgpuck.h"
>  #include "sqlInt.h"
>  #include "vdbeInt.h"
> @@ -650,24 +652,11 @@ sqlVdbeJumpHere(Vdbe * p, int addr)
>  	sqlVdbeChangeP2(p, addr, p->nOp);
>  }
>  
> -/*
> - * If the input FuncDef structure is ephemeral, then free it.  If
> - * the FuncDef is not ephermal, then do nothing.
> - */
> -static void
> -freeEphemeralFunction(sql * db, FuncDef * pDef)
> -{
> -	if ((pDef->funcFlags & SQL_FUNC_EPHEM) != 0) {
> -		sqlDbFree(db, pDef);
> -	}
> -}
> -
>  static void vdbeFreeOpArray(sql *, Op *, int);
>  
>  static SQL_NOINLINE void
>  freeP4FuncCtx(sql * db, sql_context * p)
>  {
> -	freeEphemeralFunction(db, p->pFunc);
>  	sqlDbFree(db, p);
>  }
>  
> @@ -690,10 +679,6 @@ freeP4(sql * db, int p4type, void *p4)
>  	case P4_KEYINFO:
>  		sql_key_info_unref(p4);
>  		break;
> -	case P4_FUNCDEF:{
> -			freeEphemeralFunction(db, (FuncDef *) p4);
> -			break;
> -		}
>  	case P4_MEM:
>  		sqlValueFree((sql_value *) p4);
>  		break;
> @@ -1150,15 +1135,17 @@ displayP4(Op * pOp, char *zTemp, int nTemp)
>  				sqlXPrintf(&x, "(binary)");
>  			break;
>  		}
> -	case P4_FUNCDEF:{
> -			FuncDef *pDef = pOp->p4.pFunc;
> -			sqlXPrintf(&x, "%s(%d)", pDef->zName, pDef->nArg);
> +	case P4_FUNC:{
> +			struct func *func = pOp->p4.func;
> +			sqlXPrintf(&x, "%s(%d)", func->def->name,
> +				   func->def->param_count);
>  			break;
>  		}
>  #if defined(SQL_DEBUG) || defined(VDBE_PROFILE)
>  	case P4_FUNCCTX:{
> -			FuncDef *pDef = pOp->p4.pCtx->pFunc;
> -			sqlXPrintf(&x, "%s(%d)", pDef->zName, pDef->nArg);
> +			struct func *func = pOp->p4.pCtx->func;
> +			sqlXPrintf(&x, "%s(%d)", func->def->name,
> +				   func->def->param_count);
>  			break;
>  		}
>  #endif
> diff --git a/src/box/sql/vdbemem.c b/src/box/sql/vdbemem.c
> index 7bd5cdd71..b785739a6 100644
> --- a/src/box/sql/vdbemem.c
> +++ b/src/box/sql/vdbemem.c
> @@ -39,6 +39,8 @@
>  #include "sqlInt.h"
>  #include "vdbeInt.h"
>  #include "tarantoolInt.h"
> +#include "box/func.h"
> +#include "box/func_def.h"
>  #include "box/schema.h"
>  #include "box/tuple.h"
>  #include "box/port.h"
> @@ -306,40 +308,30 @@ sqlVdbeMemStringify(Mem * pMem, u8 bForce)
>  	return 0;
>  }
>  
> -/*
> - * Memory cell pMem contains the context of an aggregate function.
> - * This routine calls the finalize method for that function.  The
> - * result of the aggregate is stored back into pMem.
> - *
> - * Return -1 if the finalizer reports an error. 0 otherwise.
> - */
>  int
> -sqlVdbeMemFinalize(Mem * pMem, FuncDef * pFunc)
> +sql_vdbemem_finilize(struct Mem *mem, struct func *func)
>  {
>  	int rc = 0;
> -	if (ALWAYS(pFunc && pFunc->xFinalize)) {
> -		assert((pMem->flags & MEM_Null) != 0 || pFunc == pMem->u.pDef);
> -		struct sql_context ctx;
> -		memset(&ctx, 0, sizeof(ctx));
> -		ctx.pMem = pMem;
> -		ctx.pFunc = pFunc;
> -		struct region *region = &fiber()->gc;
> -		size_t region_svp = region_used(region);
> -		struct port args, ret;
> -		port_vdbemem_create(&args, NULL, 0, &ctx);
> -		rc = pFunc->xFinalize(NULL, &args, &ret);
> -		assert((pMem->flags & MEM_Dyn) == 0);
> -		if (pMem->szMalloc > 0)
> -			sqlDbFree(pMem->db, pMem->zMalloc);
> -		uint32_t size;
> -		struct Mem *mem = (struct Mem *)port_get_vdbemem(&ret, &size);
> -		if (mem != NULL) {
> -			*pMem = mem[0];
> -		} else {
> -			rc = -1;
> -		}
> -		region_truncate(region, region_svp);
> -	}
> +	assert(func != NULL && func->def->aggregate == FUNC_AGGREGATE_GROUP);
> +	struct sql_context ctx;
> +	memset(&ctx, 0, sizeof(ctx));
> +	ctx.pMem = mem;
> +	ctx.func = func;
> +	struct region *region = &fiber()->gc;
> +	size_t region_svp = region_used(region);
> +	struct port args, ret;
> +	port_vdbemem_create(&args, NULL, 0, &ctx);
> +	rc = ((struct func_sql_builtin *)func)->finalize(func, &args, &ret);
> +	assert((mem->flags & MEM_Dyn) == 0);
> +	if (mem->szMalloc > 0)
> +		sqlDbFree(mem->db, mem->zMalloc);
> +	uint32_t size;
> +	struct Mem *val = (struct Mem *)port_get_vdbemem(&ret, &size);
> +	if (val != NULL)
> +		*mem = val[0];
> +	else
> +		rc = -1;
> +	region_truncate(region, region_svp);
>  	return rc;
>  }
>  
> @@ -357,7 +349,7 @@ vdbeMemClearExternAndSetNull(Mem * p)
>  {
>  	assert(VdbeMemDynamic(p));
>  	if (p->flags & MEM_Agg) {
> -		sqlVdbeMemFinalize(p, p->u.pDef);
> +		sql_vdbemem_finilize(p, p->u.func);
>  		assert((p->flags & MEM_Agg) == 0);
>  		testcase(p->flags & MEM_Dyn);
>  	}
> @@ -1222,8 +1214,8 @@ valueNew(sql * db, struct ValueNewStat4Ctx *p)
>   * to be a scalar SQL function. If
>   *
>   *   * all function arguments are SQL literals,
> - *   * one of the SQL_FUNC_CONSTANT or _SLOCHNG function flags is set, and
> - *   * the SQL_FUNC_NEEDCOLL function flag is not set,
> + *   * the function is deterministic and the SQL_FUNC_NEEDCOLL
> + *     flag is not set,
>   *
>   * then this routine attempts to invoke the SQL function. Assuming no
>   * error occurs, output parameter (*ppVal) is set to point to a value
> @@ -1247,7 +1239,6 @@ valueFromFunction(sql * db,	/* The database connection */
>  {
>  	sql_value **apVal = 0;	/* Function arguments */
>  	int nVal = 0;		/* Size of apVal[] array */
> -	FuncDef *pFunc = 0;	/* Function definition */
>  	sql_value *pVal = 0;	/* New value */
>  	int rc = 0;	/* Return code */
>  	ExprList *pList = 0;	/* Function arguments */
> @@ -1258,13 +1249,11 @@ valueFromFunction(sql * db,	/* The database connection */
>  	pList = p->x.pList;
>  	if (pList)
>  		nVal = pList->nExpr;
> -	pFunc = sqlFindFunction(db, p->u.zToken, nVal, 0);
> -	assert(pFunc);
> -	if ((pFunc->funcFlags & (SQL_FUNC_CONSTANT | SQL_FUNC_SLOCHNG)) ==
> -	    0 || (pFunc->funcFlags & SQL_FUNC_NEEDCOLL)
> -	    ) {
> +	struct func *func =
> +		sql_func_by_signature(p->u.zToken, strlen(p->u.zToken), nVal);
> +	assert(func != NULL);
> +	if ((func->def->sql_flags & SQL_FUNC_NEEDCOLL) != 0)
>  		return 0;
> -	}
>  	struct region *region = &fiber()->gc;
>  	size_t region_svp = region_used(region);
>  
> @@ -1295,7 +1284,7 @@ valueFromFunction(sql * db,	/* The database connection */
>  
>  	struct port args, ret;
>  	port_vdbemem_create(&args, (struct sql_value *)apVal, nVal, NULL);
> -	if ((*pFunc->xSFunc)(NULL, &args, &ret) != 0) {
> +	if (func_call(func, &args, &ret) != 0) {
>  		rc = -1;
>  		goto value_from_function_out;
>  	}
> diff --git a/src/box/sql/whereexpr.c b/src/box/sql/whereexpr.c
> index befe4ce73..5736a4a0b 100644
> --- a/src/box/sql/whereexpr.c
> +++ b/src/box/sql/whereexpr.c
> @@ -259,9 +259,9 @@ like_optimization_is_valid(Parse *pParse, Expr *pExpr, Expr **ppPrefix,
>  	/* Result code to return. */
>  	int rc;
>  
> -	if (!sql_is_like_func(db, pExpr, pnoCase)) {
> +	if (!sql_is_like_func(pExpr, pnoCase))
>  		return 0;
> -	}
> +
>  	pList = pExpr->x.pList;
>  	pLeft = pList->a[1].pExpr;
>  	/* Value might be numeric */
> diff --git a/src/lib/coll/coll.c b/src/lib/coll/coll.c
> index fa27bf34e..275b1cfd7 100644
> --- a/src/lib/coll/coll.c
> +++ b/src/lib/coll/coll.c
> @@ -36,6 +36,7 @@
>  #include <unicode/ucol.h>
>  #include <unicode/ucnv.h>
>  #include <unicode/ucasemap.h>
> +#include <unicode/ucnv.h>
>  #include "tt_static.h"
>  
>  struct UCaseMap *icu_ucase_default_map = NULL;
> diff --git a/test/box/function1.result b/test/box/function1.result
> index c434b0067..7c3a93577 100644
> --- a/test/box/function1.result
> +++ b/test/box/function1.result
> @@ -365,6 +365,125 @@ test_run:cmd("setopt delimiter ''");
>  c:close()
>  ---
>  ...
> +--
> +-- gh-2233: Invoke Lua functions created outside SQL.
> +--
> +box.schema.func.create("function1.divide", {language = 'C', returns = 'number', is_deterministic = true, exports = {'LUA'}})
> +---
> +...
> +box.execute('SELECT "function1.divide"()')
> +---
> +- error: wrong number of arguments to function function1.divide()
> +...
> +box.func["function1.divide"]:drop()
> +---
> +...
> +box.schema.func.create("function1.divide", {language = 'C', returns = 'number', is_deterministic = true, exports = {'LUA', 'SQL'}})
> +---
> +...
> +box.execute('SELECT "function1.divide"()')
> +---
> +- error: invalid argument
> +...
> +box.execute('SELECT "function1.divide"(6)')
> +---
> +- error: wrong number of arguments to function function1.divide()
> +...
> +box.execute('SELECT "function1.divide"(6, 3)')
> +---
> +- error: wrong number of arguments to function function1.divide()
> +...
> +box.func["function1.divide"]:drop()
> +---
> +...
> +box.schema.func.create("function1.divide", {language = 'C', returns = 'number', param_list = {'number', 'number'}, is_deterministic = true, exports = {'LUA', 'SQL'}})
> +---
> +...
> +box.execute('SELECT "function1.divide"()')
> +---
> +- error: wrong number of arguments to function function1.divide()
> +...
> +box.execute('SELECT "function1.divide"(6)')
> +---
> +- error: wrong number of arguments to function function1.divide()
> +...
> +box.execute('SELECT "function1.divide"(6, 3, 3)')
> +---
> +- error: wrong number of arguments to function function1.divide()
> +...
> +box.execute('SELECT "function1.divide"(6, 3)')
> +---
> +- metadata:
> +  - name: '"function1.divide"(6, 3)'
> +    type: number
> +  rows:
> +  - [2]
> +...
> +box.execute('SELECT "function1.divide"(5, 2)')
> +---
> +- metadata:
> +  - name: '"function1.divide"(5, 2)'
> +    type: number
> +  rows:
> +  - [2.5]
> +...
> +box.func["function1.divide"]:drop()
> +---
> +...
> +function SUMMARIZE(a, b) return a + b end
> +---
> +...
> +box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number', is_deterministic = true, param_list = {'number', 'number'}, exports = {'LUA', 'SQL'}})
> +---
> +...
> +box.execute('SELECT summarize(1, 2)')
> +---
> +- metadata:
> +  - name: summarize(1, 2)
> +    type: number
> +  rows:
> +  - [3]
> +...
> +box.func.SUMMARIZE:drop()
> +---
> +...
> +box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number', body = 'function (a, b) return a + b end', is_deterministic = true, param_list = {'number', 'number'}, exports = {'LUA', 'SQL'}})
> +---
> +...
> +box.execute('SELECT summarize(1, 2)')
> +---
> +- metadata:
> +  - name: summarize(1, 2)
> +    type: number
> +  rows:
> +  - [3]
> +...
> +box.func.SUMMARIZE:drop()
> +---
> +...
> +--
> +-- gh-4113: Valid method to use Lua from SQL
> +--
> +box.execute('SELECT lua(\'return 1 + 1\')')
> +---
> +- metadata:
> +  - name: lua('return 1 + 1')
> +    type: any
> +  rows:
> +  - [2]
> +...
> +box.execute('SELECT lua(\'return box.cfg\')')
> +---
> +- error: 'Failed to execute SQL statement: Unsupported type passed from Lua'
> +...
> +box.execute('SELECT lua(\'return box.cfg.memtx_memory\')')
> +---
> +- metadata:
> +  - name: lua('return box.cfg.memtx_memory')
> +    type: any
> +  rows:
> +  - [107374182]
> +...
>  -- Test registered functions interface.
>  function divide(a, b) return a / b end
>  ---
> diff --git a/test/box/function1.test.lua b/test/box/function1.test.lua
> index dbbdcf8be..69c2de685 100644
> --- a/test/box/function1.test.lua
> +++ b/test/box/function1.test.lua
> @@ -128,6 +128,41 @@ identifier.run_test(
>  test_run:cmd("setopt delimiter ''");
>  c:close()
>  
> +--
> +-- gh-2233: Invoke Lua functions created outside SQL.
> +--
> +box.schema.func.create("function1.divide", {language = 'C', returns = 'number', is_deterministic = true, exports = {'LUA'}})
> +box.execute('SELECT "function1.divide"()')
> +box.func["function1.divide"]:drop()
> +box.schema.func.create("function1.divide", {language = 'C', returns = 'number', is_deterministic = true, exports = {'LUA', 'SQL'}})
> +box.execute('SELECT "function1.divide"()')
> +box.execute('SELECT "function1.divide"(6)')
> +box.execute('SELECT "function1.divide"(6, 3)')
> +box.func["function1.divide"]:drop()
> +box.schema.func.create("function1.divide", {language = 'C', returns = 'number', param_list = {'number', 'number'}, is_deterministic = true, exports = {'LUA', 'SQL'}})
> +box.execute('SELECT "function1.divide"()')
> +box.execute('SELECT "function1.divide"(6)')
> +box.execute('SELECT "function1.divide"(6, 3, 3)')
> +box.execute('SELECT "function1.divide"(6, 3)')
> +box.execute('SELECT "function1.divide"(5, 2)')
> +box.func["function1.divide"]:drop()
> +
> +function SUMMARIZE(a, b) return a + b end
> +box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number', is_deterministic = true, param_list = {'number', 'number'}, exports = {'LUA', 'SQL'}})
> +box.execute('SELECT summarize(1, 2)')
> +box.func.SUMMARIZE:drop()
> +
> +box.schema.func.create("SUMMARIZE", {language = 'LUA', returns = 'number', body = 'function (a, b) return a + b end', is_deterministic = true, param_list = {'number', 'number'}, exports = {'LUA', 'SQL'}})
> +box.execute('SELECT summarize(1, 2)')
> +box.func.SUMMARIZE:drop()
> +
> +--
> +-- gh-4113: Valid method to use Lua from SQL
> +--
> +box.execute('SELECT lua(\'return 1 + 1\')')
> +box.execute('SELECT lua(\'return box.cfg\')')
> +box.execute('SELECT lua(\'return box.cfg.memtx_memory\')')
> +
>  -- Test registered functions interface.
>  function divide(a, b) return a / b end
>  box.schema.func.create("divide", {comment = 'Divide two values'})
> diff --git a/test/sql-tap/alias.test.lua b/test/sql-tap/alias.test.lua
> index f9e6fc9fa..75391b305 100755
> --- a/test/sql-tap/alias.test.lua
> +++ b/test/sql-tap/alias.test.lua
> @@ -25,14 +25,13 @@ test:plan(9)
>  --
>  
>  counter = 0
> -sequence = function()
> -    counter = counter + 1
> -    return counter
> -end
>  
>  -- Function is declared as deterministic deliberately.
>  -- Otherwise it would be called as much as it occurs in a query.
> -box.internal.sql_create_function("sequence", "INT", sequence, 0, true)
> +box.schema.func.create('SEQUENCE', {language = 'Lua', is_deterministic = true,
> +                       returns = 'unsigned',
> +                       body = 'function() counter = counter + 1 return counter end',
> +                       exports = {'LUA', 'SQL'}})
>  
>  test:do_test(
>      "alias-1.1",
> @@ -220,6 +219,6 @@ test:do_test(
>  --         -- </alias-3.1>
>  --     })
>  
> -
> +box.func.SEQUENCE:drop()
>  
>  test:finish_test()
> diff --git a/test/sql-tap/check.test.lua b/test/sql-tap/check.test.lua
> index 98f906cf4..d83aafe2c 100755
> --- a/test/sql-tap/check.test.lua
> +++ b/test/sql-tap/check.test.lua
> @@ -679,10 +679,12 @@ test:do_execsql_test(
>  -- cannot be tested).
>  --
>  --reset_db()
> -local function myfunc(x)
> -    return x < 10
> -end
> -box.internal.sql_create_function("myfunc", "INT", myfunc)
> +
> +box.schema.func.create('MYFUNC', {language = 'Lua',
> +                   is_deterministic = true,
> +                   body = 'function(x) return x < 10 end',
> +                   returns = 'boolean', param_list = {'number'},
> +                   exports = {'LUA', 'SQL'}})
>  
>  test:do_execsql_test(
>      7.1,
> @@ -807,5 +809,8 @@ test:do_catchsql_test(
>          -- </9.3>
>      })
>  
> +
> +box.func.MYFUNC:drop()
> +
>  test:finish_test()
>  
> diff --git a/test/sql-tap/func5.test.lua b/test/sql-tap/func5.test.lua
> index f3706e631..4134d4036 100755
> --- a/test/sql-tap/func5.test.lua
> +++ b/test/sql-tap/func5.test.lua
> @@ -71,13 +71,25 @@ test:do_execsql_test(
>  
>  global_counter = 0
>  
> -counter = function(str)
> -    global_counter = global_counter + 1
> -    return global_counter
> -end
> -
> -box.internal.sql_create_function("counter1", "INT", counter, -1, false)
> -box.internal.sql_create_function("counter2", "INT", counter, -1, true)
> +box.schema.func.create('COUNTER1', {language = 'Lua', is_deterministic = false,
> +            param_list = {'any'}, returns = 'integer',
> +            exports = {'SQL', 'LUA'},
> +            body = [[
> +                function(str)
> +                    global_counter = global_counter + 1
> +                    return global_counter
> +                end
> +            ]]})
> +
> +box.schema.func.create('COUNTER2', {language = 'Lua', is_deterministic = true,
> +                param_list = {'any'}, returns = 'integer',
> +                exports = {'SQL', 'LUA'},
> +                body = [[
> +                    function(str)
> +                            global_counter = global_counter + 1
> +                            return global_counter
> +                        end
> +                ]]})
>  
>  test:do_execsql_test(
>      "func5-2.2",
> @@ -229,4 +241,7 @@ test:do_catchsql_test(
>      }
>  )
>  
> +box.func.COUNTER1:drop()
> +box.func.COUNTER2:drop()
> +
>  test:finish_test()
> diff --git a/test/sql-tap/lua_sql.test.lua b/test/sql-tap/lua_sql.test.lua
> index b0913e7f4..6451a2cf5 100755
> --- a/test/sql-tap/lua_sql.test.lua
> +++ b/test/sql-tap/lua_sql.test.lua
> @@ -1,19 +1,16 @@
>  #!/usr/bin/env tarantool
>  test = require("sqltester")
>  NULL = require('msgpack').NULL
> -test:plan(24)
> -
> -local function func1(a)
> -    return a
> -end
> -local function allways_2(a)
> -    return 2
> -end
> +test:plan(22)
>  
>  test:do_test(
>      "lua_sql-1.0",
>      function ()
> -        box.internal.sql_create_function("func1", "INT", allways_2)
> +        box.schema.func.create('FUNC1', {language = 'Lua',
> +                        is_deterministic = true,
> +                        body = 'function(a) return 2 end',
> +                        param_list = {'any'}, returns = 'integer',
> +                        exports = {'LUA', 'SQL'}})
>          return test:execsql("select func1(1)")
>      end,
>      {2})
> @@ -22,34 +19,16 @@ test:do_test(
>  test:do_test(
>      "lua_sql-1.1",
>      function ()
> -        box.internal.sql_create_function("func1", "INT", func1)
> +        box.func.FUNC1:drop()
> +        box.schema.func.create('FUNC1', {language = 'Lua',
> +                        is_deterministic = true,
> +                        body = 'function(a) return a end',
> +                        param_list = {'scalar'}, returns = 'integer',
> +                        exports = {'LUA', 'SQL'}})
>          return test:execsql("select func1(1)")
>      end,
>      {1})
>  
> --- try to loose memory
> -test:do_test(
> -    "lua_sql-1.2",
> -    function ()
> -        for i = 1, 1000000, 1 do
> -            box.internal.sql_create_function("func1", "INT", func1)
> -        end
> -        return test:execsql("select func1(1)")
> -    end,
> -    {1})
> -
> --- check sql polymorphism
> -test:do_test(
> -    "lua_sql-1.3",
> -    function ()
> -        box.internal.sql_create_function("allways_2", "INT", allways_2, 1) -- specify 1 arg
> -        box.internal.sql_create_function("allways_2", "INT", func1)
> -        box.internal.sql_create_function("allways_2", "INT", func1, 2)
> -        box.internal.sql_create_function("allways_2", "INT", func1, 3)
> -        return test:execsql("select allways_2(1)")
> -    end,
> -    {2})
> -
>  test:do_catchsql_test(
>      "lua_sql-1.0",
>      "select func3(1)",
> @@ -72,7 +51,7 @@ for _, val in ipairs({
>          {result})
>  end
>  
> -local from_sql_to_lua = {
> +from_sql_to_lua = {
>      [1] = {1, 1},
>      [2] = {"1", 1},
>      [3] = {"1.5", 1.5},
> @@ -81,14 +60,19 @@ local from_sql_to_lua = {
>      [6] = {"x'0500'", "\u{0005}\u{0000}"},
>      [7] = {"123123123123123", 123123123123123LL},
>  }
> -local json = require("json")
> -local function check_from_sql_to_lua(i, arg)
> -    if from_sql_to_lua[i][2] == arg then
> -        return 1
> -    end
> -    return 0
> -end
> -box.internal.sql_create_function("check_from_sql_to_lua", "INT", check_from_sql_to_lua)
> +
> +box.schema.func.create('CHECK_FROM_SQL_TO_LUA', {language = 'Lua',
> +                is_deterministic = true,
> +                body = [[
> +                    function(i, arg)
> +                        if from_sql_to_lua[i][2] == arg then
> +                            return 1
> +                        end
> +                        return 0
> +                    end
> +                ]],
> +                param_list = {'integer', 'scalar'}, returns = 'integer',
> +                exports = {'LUA', 'SQL'}})
>  
>  -- check for different types
>  for i = 1, #from_sql_to_lua, 1 do
> @@ -98,17 +82,23 @@ for i = 1, #from_sql_to_lua, 1 do
>          {1})
>  end
>  
> -local from_lua_to_sql = {
> +from_lua_to_sql = {
>      [1] = {1, 1},
>      [2] = {"1.5", 1.5},
>      [3] = {"'1'", "1"},
>      [4] = {"true", true},
>      [5] = {"false", false},
>  }
> -local function check_from_lua_to_sql(i)
> -    return from_lua_to_sql[i][2]
> -end
> -box.internal.sql_create_function("check_from_lua_to_sql", "BLOB", check_from_lua_to_sql)
> +
> +box.schema.func.create('CHECK_FROM_LUA_TO_SQL', {language = 'Lua',
> +                is_deterministic = true,
> +                body = [[
> +                    function(i)
> +                        return from_lua_to_sql[i][2]
> +                    end
> +                ]],
> +                param_list = {'integer'}, returns = 'scalar',
> +                exports = {'LUA', 'SQL'}})
>  
>  -- check for different types
>  for i = 1, #from_lua_to_sql, 1 do
> @@ -118,14 +108,20 @@ for i = 1, #from_lua_to_sql, 1 do
>          {1})
>  end
>  
> -local from_lua_to_sql_bad = {
> +from_lua_to_sql_bad = {
>      [1] = NULL,
>      [2] = 12LL, -- it is possible to support this type
>  }
> -local function check_from_lua_to_sql_bad(i)
> -    return from_lua_to_sql_bad[i]
> -end
> -box.internal.sql_create_function("check_from_lua_to_sql_bad", "BLOB", check_from_lua_to_sql_bad)
> +
> +box.schema.func.create('CHECK_FROM_LUA_TO_SQL_BAD', {language = 'Lua',
> +                is_deterministic = true,
> +                body = [[
> +                    function(i)
> +                        return from_lua_to_sql_bad[i]
> +                    end
> +                ]],
> +                param_list = {'integer'}, returns = 'scalar',
> +                exports = {'LUA', 'SQL'}})
>  
>  for i = 1, #from_lua_to_sql_bad, 1 do
>      test:do_catchsql_test(
> @@ -134,16 +130,26 @@ for i = 1, #from_lua_to_sql_bad, 1 do
>          {1, "/Unsupported/"})
>  end
>  
> -local function allways_error()
> -    error("my_error123")
> -    return 1
> -end
> -box.internal.sql_create_function("allways_error", "INT", allways_error)
> +box.schema.func.create('ALLWAYS_ERROR', {language = 'Lua',
> +                is_deterministic = true,
> +                body = [[
> +                    function()
> +                        error("my_error123")
> +                        return 1
> +                    end
> +                ]],
> +                param_list = {}, returns = 'integer',
> +                exports = {'LUA', 'SQL'}})
>  
>  test:do_catchsql_test(
>      "lua_sql-2.6",
>      "select allways_error()",
>      {1, "/my_error123/"})
>  
> +box.func.FUNC1:drop()
> +box.func.CHECK_FROM_SQL_TO_LUA:drop()
> +box.func.CHECK_FROM_LUA_TO_SQL:drop()
> +box.func.CHECK_FROM_LUA_TO_SQL_BAD:drop()
> +box.func.ALLWAYS_ERROR:drop()
>  
>  test:finish_test()
> diff --git a/test/sql-tap/subquery.test.lua b/test/sql-tap/subquery.test.lua
> index 7a3e270dc..743b7141e 100755
> --- a/test/sql-tap/subquery.test.lua
> +++ b/test/sql-tap/subquery.test.lua
> @@ -710,17 +710,20 @@ test:do_execsql_test(
>  -- for a matching column name did not cause an otherwise static subquery
>  -- to become a dynamic (correlated) subquery.
>  --
> -local callcnt = 0
> +callcnt = 0
>  test:do_test(
>      "subquery-5.1",
>      function()
> -        local function callcntproc(n)
> -            callcnt = callcnt + 1
> -            return n
> -        end
> -
> -        callcnt = 0
> -        box.internal.sql_create_function("callcnt", "INT", callcntproc)
> +        box.schema.func.create('CALLCNT', {language = 'Lua',
> +                   is_deterministic = true,
> +                   param_list = {'integer'}, returns = 'integer',
> +                   exports = {'LUA', 'SQL'},
> +                   body = [[
> +                       function(n)
> +                            callcnt = callcnt + 1
> +                            return n
> +                       end
> +                   ]]})
>          return test:execsql [[
>              CREATE TABLE t4(x TEXT,y  INT PRIMARY KEY);
>              INSERT INTO t4 VALUES('one',1);
> @@ -791,6 +794,8 @@ test:do_test(
>          return callcnt
>      end, 1)
>  
> +box.func.CALLCNT:drop()
> +
>  --############  was disable until we get #2652 fixed
>  -- Ticket #2652.  Allow aggregate functions of outer queries inside
>  -- a non-aggregate subquery.
> diff --git a/test/sql-tap/trigger9.test.lua b/test/sql-tap/trigger9.test.lua
> index e7e170b3d..64dcaff33 100755
> --- a/test/sql-tap/trigger9.test.lua
> +++ b/test/sql-tap/trigger9.test.lua
> @@ -46,7 +46,10 @@ local function has_rowdata(sql)
>  --     X(41, "X!cmd", [=[["expr","[lsearch [execsql \"explain $sql\"] RowData]>=0"]]=])
>  end
>  
> -box.internal.sql_create_function('randstr', 'TEXT', test.randstr, 1)
> +box.schema.func.create('RANDSTR', {language = 'Lua',
> +                   body = 'function(n) return test.randstr(n) end',
> +                   param_list = {'integer'}, returns = 'string',
> +                   exports = {'LUA', 'SQL'}})
>  
>  -- MUST_WORK_TEST
>  test:do_execsql_test(
> @@ -450,5 +453,6 @@ test:do_execsql_test(
>          -- </4.3>
>      })
>  
> +box.func.RANDSTR:drop()
>  
>  test:finish_test()
> diff --git a/test/sql-tap/where2.test.lua b/test/sql-tap/where2.test.lua
> index 4116ca913..f267be8e6 100755
> --- a/test/sql-tap/where2.test.lua
> +++ b/test/sql-tap/where2.test.lua
> @@ -231,7 +231,7 @@ test:do_execsql_test(
>          EXPLAIN SELECT * FROM x1, x2 WHERE x=1 ORDER BY random();
>      ]], {
>          -- <where2-2.5>
> -        "/random/"
> +        "/RANDOM/"
>          -- </where2-2.5>
>      })
>  
> @@ -254,7 +254,7 @@ test:do_execsql_test(
>          EXPLAIN SELECT * FROM x1, x2 WHERE x=1 ORDER BY abs(5);
>      ]], {
>          -- <where2-2.6>
> -        "~/abs/"
> +        "~/ABS/"
>          -- </where2-2.6>
>      })
>  
> diff --git a/test/sql/errinj.result b/test/sql/errinj.result
> index 8846e5ee8..b4b44dda4 100644
> --- a/test/sql/errinj.result
> +++ b/test/sql/errinj.result
> @@ -439,31 +439,6 @@ errinj.set("ERRINJ_WAL_DELAY", false)
>  - ok
>  ...
>  --
> --- gh-3931: Store regular identifiers in case-normal form
> ---
> -errinj = box.error.injection
> ----
> -...
> -errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", true)
> ----
> -- ok
> -...
> -box.execute("CREATE TABLE hello (id INT primary key,x INT,y INT);")
> ----
> -- error: Failed to allocate 6 bytes in sqlDbMallocRawNN for res
> -...
> -dummy_f = function(int) return 1 end
> ----
> -...
> -box.internal.sql_create_function("counter1", "INT", dummy_f, -1, false)
> ----
> -- error: Failed to allocate 9 bytes in region_alloc for res
> -...
> -errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false)
> ----
> -- ok
> -...
> ---
>  -- Tests which are aimed at verifying work of commit/rollback
>  -- triggers on _ck_constraint space.
>  --
> diff --git a/test/sql/errinj.test.lua b/test/sql/errinj.test.lua
> index 48b80a443..1250bf34b 100644
> --- a/test/sql/errinj.test.lua
> +++ b/test/sql/errinj.test.lua
> @@ -140,16 +140,6 @@ box.execute("UPDATE t SET id = 2;")
>  -- Finish drop space.
>  errinj.set("ERRINJ_WAL_DELAY", false)
>  
> ---
> --- gh-3931: Store regular identifiers in case-normal form
> ---
> -errinj = box.error.injection
> -errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", true)
> -box.execute("CREATE TABLE hello (id INT primary key,x INT,y INT);")
> -dummy_f = function(int) return 1 end
> -box.internal.sql_create_function("counter1", "INT", dummy_f, -1, false)
> -errinj.set("ERRINJ_SQL_NAME_NORMALIZATION", false)
> -
>  --
>  -- Tests which are aimed at verifying work of commit/rollback
>  -- triggers on _ck_constraint space.
> diff --git a/test/sql/func-recreate.result b/test/sql/func-recreate.result
> index 73fb03cc4..da6d6e770 100644
> --- a/test/sql/func-recreate.result
> +++ b/test/sql/func-recreate.result
> @@ -12,7 +12,15 @@ box.execute('pragma sql_default_engine=\''..engine..'\'')
>  fiber = require('fiber')
>  ---
>  ...
> -box.internal.sql_create_function('WAITFOR', 'INT', function (n) fiber.sleep(n) return n end)
> +test_run:cmd("setopt delimiter ';'")
> +---
> +- true
> +...
> +box.schema.func.create('WAITFOR', {language = 'Lua',
> +                   body = 'function (n) fiber.sleep(n) return n end',
> +                   param_list = {'integer'}, returns = 'integer',
> +                   exports = {'LUA', 'SQL'}})
> +test_run:cmd("setopt delimiter ''");
>  ---
>  ...
>  ch = fiber.channel(1)
> @@ -24,10 +32,19 @@ _ = fiber.create(function () ch:put(box.execute('select WAITFOR(0.2)')) end)
>  fiber.sleep(0.1)
>  ---
>  ...
> -box.internal.sql_create_function('WAITFOR', 'INT', function (n) require('fiber').sleep(n) return n end)
> +box.func.WAITFOR:drop()
> +---
> +...
> +test_run:cmd("setopt delimiter ';'")
> +---
> +- true
> +...
> +box.schema.func.create('WAITFOR', {language = 'Lua',
> +                   body = 'function (n) fiber.sleep(n) return n end',
> +                   param_list = {'integer'}, returns = 'integer',
> +                   exports = {'LUA', 'SQL'}})
> +test_run:cmd("setopt delimiter ''");
>  ---
> -- error: 'Failed to create function ''WAITFOR'': unable to create function due to
> -    active statements'
>  ...
>  ch:get()
>  ---
> @@ -37,6 +54,20 @@ ch:get()
>    rows:
>    - [0.2]
>  ...
> -box.internal.sql_create_function('WAITFOR', 'INT', function (n) require('fiber').sleep(n) return n end)
> +box.func.WAITFOR:drop()
> +---
> +...
> +test_run:cmd("setopt delimiter ';'")
> +---
> +- true
> +...
> +box.schema.func.create('WAITFOR', {language = 'Lua',
> +                   body = 'function (n) fiber.sleep(n) return n end',
> +                   param_list = {'integer'}, returns = 'integer',
> +                   exports = {'LUA', 'SQL'}})
> +test_run:cmd("setopt delimiter ''");
> +---
> +...
> +box.func.WAITFOR:drop()
>  ---
>  ...
> diff --git a/test/sql/func-recreate.test.lua b/test/sql/func-recreate.test.lua
> index 753e9ca4d..1b6e870f3 100644
> --- a/test/sql/func-recreate.test.lua
> +++ b/test/sql/func-recreate.test.lua
> @@ -4,14 +4,36 @@ box.execute('pragma sql_default_engine=\''..engine..'\'')
>  
>  -- Check errors during function create process
>  fiber = require('fiber')
> -box.internal.sql_create_function('WAITFOR', 'INT', function (n) fiber.sleep(n) return n end)
> +
> +test_run:cmd("setopt delimiter ';'")
> +box.schema.func.create('WAITFOR', {language = 'Lua',
> +                   body = 'function (n) fiber.sleep(n) return n end',
> +                   param_list = {'integer'}, returns = 'integer',
> +                   exports = {'LUA', 'SQL'}})
> +test_run:cmd("setopt delimiter ''");
>  
>  ch = fiber.channel(1)
>  
>  _ = fiber.create(function () ch:put(box.execute('select WAITFOR(0.2)')) end)
>  fiber.sleep(0.1)
>  
> -box.internal.sql_create_function('WAITFOR', 'INT', function (n) require('fiber').sleep(n) return n end)
> +box.func.WAITFOR:drop()
> +
> +test_run:cmd("setopt delimiter ';'")
> +box.schema.func.create('WAITFOR', {language = 'Lua',
> +                   body = 'function (n) fiber.sleep(n) return n end',
> +                   param_list = {'integer'}, returns = 'integer',
> +                   exports = {'LUA', 'SQL'}})
> +test_run:cmd("setopt delimiter ''");
> +
>  ch:get()
> -box.internal.sql_create_function('WAITFOR', 'INT', function (n) require('fiber').sleep(n) return n end)
> +box.func.WAITFOR:drop()
> +
> +test_run:cmd("setopt delimiter ';'")
> +box.schema.func.create('WAITFOR', {language = 'Lua',
> +                   body = 'function (n) fiber.sleep(n) return n end',
> +                   param_list = {'integer'}, returns = 'integer',
> +                   exports = {'LUA', 'SQL'}})
> +test_run:cmd("setopt delimiter ''");
>  
> +box.func.WAITFOR:drop()
> -- 
> 2.21.0
> 

-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 10/12] sql: refactor builtins signatures with port
  2019-07-10 18:47   ` [tarantool-patches] " Konstantin Osipov
@ 2019-07-11  7:33     ` Kirill Shcherbatov
  0 siblings, 0 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-11  7:33 UTC (permalink / raw)
  To: tarantool-patches, Kostya Osipov, Nikita Pettik

> I don't know why you ignored my comment on telegram: I think this
> is a non-goal and SQL functions don't have to use _func api for
> calls. It is a performance issue as well, since obstructs possible JITting
> of expressions. Did you discuss this with Nikita as I asked you on
> telegram?
We've decided to postpone the rest of this patchset [10-12 commits] to the next milestone,
so we'll discuss all of this in details a bit later. 

The main objection is necessity to distinguish SQL and no-SQL functions that is ugly.

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

* [tarantool-patches] Re: [PATCH v2 02/12] sql: get rid of LIKELY, UNLIKELY and LIKEHOOD
  2019-07-10 19:02   ` [tarantool-patches] " Konstantin Osipov
@ 2019-07-11  7:38     ` Kirill Shcherbatov
  0 siblings, 0 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-11  7:38 UTC (permalink / raw)
  To: tarantool-patches, Nikita Pettik, Konstantin Osipov

> Ugh, I don't mind removing them, but for a non-reason?
> 
> There exist for a reason, especially in absence of statistics,
> they allow the optimizer to guess the amount of rows a WHERE
> clause returns.
> 
> Why did you get angry at them?

I've asked Nikita about it (about SOUDEX also).
Moreover he is ok about this changes (previous patchset version)

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

* [tarantool-patches] Re: [PATCH v2 08/12] sql: rfc for SQL and Lua functions
  2019-07-10 19:18     ` Konstantin Osipov
@ 2019-07-11  7:40       ` Kirill Shcherbatov
  0 siblings, 0 replies; 35+ messages in thread
From: Kirill Shcherbatov @ 2019-07-11  7:40 UTC (permalink / raw)
  To: tarantool-patches, Kostya Osipov, Nikita Pettik

>> The spec generally looks good to me, but I don't see how you
>> addressed the case sensitivity issue. Please add name_ucase field
>> to the _func definition, and add a unique index over it, to
>> prevent duplicates with a differently cased name.
> 
> Sorry, forget this. Hit "send" too hastily. I noticed that you
> changed the unique index collation on "name" to case insensitive.
> Please mention this in the spec, I missed that at first. After
> that the spec is OK to push.

There was a string:
"We must forbid such names for `box.schema.func.create` endpoint to prevent a mess in SQL code.
To solve this problem, we may set `collation = 'unicode_ci'` for ``_func.name`` field and put all built-ins in bootstrap image."

in RFC already.

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

* [tarantool-patches] Re: [PATCH v2 08/12] sql: rfc for SQL and Lua functions
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 08/12] sql: rfc for SQL and Lua functions Kirill Shcherbatov
  2019-07-10 19:17   ` [tarantool-patches] " Konstantin Osipov
@ 2019-07-11 13:59   ` Kirill Yukhin
  1 sibling, 0 replies; 35+ messages in thread
From: Kirill Yukhin @ 2019-07-11 13:59 UTC (permalink / raw)
  To: tarantool-patches; +Cc: korablev, kostja, Kirill Shcherbatov

Hello,

On 10 Jul 14:01, Kirill Shcherbatov wrote:
> Part of #4182
> ---
>  doc/rfc/4182-persistent-lua-functions.md | 214 +++++++++++++++++++++++
>  1 file changed, 214 insertions(+)
>  create mode 100644 doc/rfc/4182-persistent-lua-functions.md

The patch was checked into master.

--
Regards, Kirill Yukhin

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

* [tarantool-patches] Re: [PATCH v2 01/12] sql: get rid of SOUNDEX, MATCH
  2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 01/12] sql: get rid of SOUNDEX, MATCH Kirill Shcherbatov
  2019-07-10 18:45   ` [tarantool-patches] " Konstantin Osipov
@ 2019-07-12  8:44   ` Kirill Yukhin
  1 sibling, 0 replies; 35+ messages in thread
From: Kirill Yukhin @ 2019-07-12  8:44 UTC (permalink / raw)
  To: tarantool-patches; +Cc: korablev, kostja, Kirill Shcherbatov

Hello,

On 10 Jul 14:00, Kirill Shcherbatov wrote:
> In relation with FuncDef cache rework we need to clean-up
> builtins list. The SOUNDEX function is not in use while MATCH
> fucntion is a stub that raises an error, so they could be
> dropped.
> 
> Needed for #4182


After private discussion, patch which enables SOUNDEX was checked
into master.

Hash: 873a3b301bbe10e8839457b2177ce142e72d97f8

--
Regards, Kirill Yukhin

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

* [tarantool-patches] Re: [PATCH v2 03/12] sql: put analyze helpers to FuncDef cache
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 03/12] sql: put analyze helpers to FuncDef cache Kirill Shcherbatov
  2019-07-10 19:04   ` [tarantool-patches] " Konstantin Osipov
@ 2019-07-12  8:47   ` Kirill Yukhin
  1 sibling, 0 replies; 35+ messages in thread
From: Kirill Yukhin @ 2019-07-12  8:47 UTC (permalink / raw)
  To: tarantool-patches; +Cc: korablev, kostja, Kirill Shcherbatov

Hello,

On 10 Jul 14:01, Kirill Shcherbatov wrote:
> Previously analyze functions refer to statically defined
> service FuncDef context. We need to change this approach due we
> going to rework the builtins functions machinery in following
> patches.
> 
> Needed for #4113, #2200, #2233

Patch was committed to master.

--
Regards, Kirill Yukhin

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

* [tarantool-patches] Re: [PATCH v2 05/12] sql: replace bool is_derived_coll marker with flag
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 05/12] sql: replace bool is_derived_coll marker with flag Kirill Shcherbatov
  2019-07-10 19:10   ` [tarantool-patches] " Konstantin Osipov
@ 2019-07-12  8:48   ` Kirill Yukhin
  1 sibling, 0 replies; 35+ messages in thread
From: Kirill Yukhin @ 2019-07-12  8:48 UTC (permalink / raw)
  To: tarantool-patches; +Cc: korablev, kostja, Kirill Shcherbatov

Hello,

On 10 Jul 14:01, Kirill Shcherbatov wrote:
> Introduce a new flag SQL_FUNC_DERIVEDCOLL for function that may
> require collation to be applied on its result instead of separate
> boolean variable. This is required to get rid of FuncDef in
> further patches.
> 
> Needed for #4113, #2200, #2233

The patch was committed to master.

--
Regards, Kirill Yukhin

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

* [tarantool-patches] Re: [PATCH v2 07/12] sql: move LIKE UConverter object to collation library
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 07/12] sql: move LIKE UConverter object to collation library Kirill Shcherbatov
@ 2019-07-12  8:49   ` Kirill Yukhin
  0 siblings, 0 replies; 35+ messages in thread
From: Kirill Yukhin @ 2019-07-12  8:49 UTC (permalink / raw)
  To: tarantool-patches; +Cc: korablev, kostja, Kirill Shcherbatov

Hello,

On 10 Jul 14:01, Kirill Shcherbatov wrote:
> Moved UConverter object to collation library. This is required
> to get rid of sqlRegisterBuiltinFunctions function in further
> patches.
> 
> Needed for #4113, #2200, #2233

The patch was committed to master.

--
Regards, Kirill Yukhin

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

* [tarantool-patches] Re: [PATCH v2 09/12] box: introduce Lua persistent functions
  2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 09/12] box: introduce Lua persistent functions Kirill Shcherbatov
  2019-07-10 19:26   ` [tarantool-patches] " Konstantin Osipov
@ 2019-07-12 21:49   ` Konstantin Osipov
  2019-07-13 13:55     ` Kirill Yukhin
  1 sibling, 1 reply; 35+ messages in thread
From: Konstantin Osipov @ 2019-07-12 21:49 UTC (permalink / raw)
  To: Kirill Shcherbatov; +Cc: tarantool-patches, korablev

* Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:

I pushed this patch with several edits.

-- 
Konstantin Osipov, Moscow, Russia

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

* [tarantool-patches] Re: [PATCH v2 09/12] box: introduce Lua persistent functions
  2019-07-12 21:49   ` Konstantin Osipov
@ 2019-07-13 13:55     ` Kirill Yukhin
  2019-07-13 14:17       ` Kirill Yukhin
  0 siblings, 1 reply; 35+ messages in thread
From: Kirill Yukhin @ 2019-07-13 13:55 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Kirill Shcherbatov, korablev

Hello,

On 13 Jul 00:49, Konstantin Osipov wrote:
> * Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> 
> I pushed this patch with several edits.

Your changes broke the build (in release mode). Please, see,
for example [1].

We appriciate fixing broken builds.

[1] - https://travis-ci.org/tarantool/tarantool/jobs/558057993

--
Regards, Kirill Yukhin

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

* [tarantool-patches] Re: [PATCH v2 09/12] box: introduce Lua persistent functions
  2019-07-13 13:55     ` Kirill Yukhin
@ 2019-07-13 14:17       ` Kirill Yukhin
  0 siblings, 0 replies; 35+ messages in thread
From: Kirill Yukhin @ 2019-07-13 14:17 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Kirill Shcherbatov, korablev

Hello,

On 13 Jul 16:55, Kirill Yukhin wrote:
> Hello,
> 
> On 13 Jul 00:49, Konstantin Osipov wrote:
> > * Kirill Shcherbatov <kshcherbatov@tarantool.org> [19/07/10 14:02]:
> > 
> > I pushed this patch with several edits.
> 
> Your changes broke the build (in release mode). Please, see,
> for example [1].

I've checked trivial fix into master.

--
Regards, Kirill Yukhin

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

end of thread, other threads:[~2019-07-13 14:17 UTC | newest]

Thread overview: 35+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-07-10 11:00 [tarantool-patches] [PATCH v2 00/12] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 01/12] sql: get rid of SOUNDEX, MATCH Kirill Shcherbatov
2019-07-10 18:45   ` [tarantool-patches] " Konstantin Osipov
2019-07-12  8:44   ` Kirill Yukhin
2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 10/12] sql: refactor builtins signatures with port Kirill Shcherbatov
2019-07-10 18:47   ` [tarantool-patches] " Konstantin Osipov
2019-07-11  7:33     ` Kirill Shcherbatov
2019-07-10 11:00 ` [tarantool-patches] [PATCH v2 11/12] box: use own vtab per each function object Kirill Shcherbatov
2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 12/12] sql: use schema's func hash instead of FuncDef hash Kirill Shcherbatov
2019-07-10 20:22   ` [tarantool-patches] " Konstantin Osipov
2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 02/12] sql: get rid of LIKELY, UNLIKELY and LIKEHOOD Kirill Shcherbatov
2019-07-10 19:02   ` [tarantool-patches] " Konstantin Osipov
2019-07-11  7:38     ` Kirill Shcherbatov
2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 03/12] sql: put analyze helpers to FuncDef cache Kirill Shcherbatov
2019-07-10 19:04   ` [tarantool-patches] " Konstantin Osipov
2019-07-12  8:47   ` Kirill Yukhin
2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 04/12] sql: rework LIKE case-insensitive mode Kirill Shcherbatov
2019-07-10 19:09   ` [tarantool-patches] " Konstantin Osipov
2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 05/12] sql: replace bool is_derived_coll marker with flag Kirill Shcherbatov
2019-07-10 19:10   ` [tarantool-patches] " Konstantin Osipov
2019-07-12  8:48   ` Kirill Yukhin
2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 06/12] sql: remove SQL_PreferBuiltin flag Kirill Shcherbatov
2019-07-10 19:11   ` [tarantool-patches] " Konstantin Osipov
2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 07/12] sql: move LIKE UConverter object to collation library Kirill Shcherbatov
2019-07-12  8:49   ` [tarantool-patches] " Kirill Yukhin
2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 08/12] sql: rfc for SQL and Lua functions Kirill Shcherbatov
2019-07-10 19:17   ` [tarantool-patches] " Konstantin Osipov
2019-07-10 19:18     ` Konstantin Osipov
2019-07-11  7:40       ` Kirill Shcherbatov
2019-07-11 13:59   ` Kirill Yukhin
2019-07-10 11:01 ` [tarantool-patches] [PATCH v2 09/12] box: introduce Lua persistent functions Kirill Shcherbatov
2019-07-10 19:26   ` [tarantool-patches] " Konstantin Osipov
2019-07-12 21:49   ` Konstantin Osipov
2019-07-13 13:55     ` Kirill Yukhin
2019-07-13 14:17       ` Kirill Yukhin

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