Tarantool development patches archive
 help / color / mirror / Atom feed
From: Kirill Shcherbatov <kshcherbatov@tarantool.org>
To: tarantool-patches@freelists.org, korablev@tarantool.org
Cc: Kirill Shcherbatov <kshcherbatov@tarantool.org>
Subject: [tarantool-patches] [PATCH v4 3/4] sql: get rid of FuncDef function hash
Date: Wed, 21 Aug 2019 18:28:08 +0300	[thread overview]
Message-ID: <154ee8ea11ae260254b71d32e1299f1272438af1.1566400979.git.kshcherbatov@tarantool.org> (raw)
In-Reply-To: <cover.1566400979.git.kshcherbatov@tarantool.org>

Now it is possible to move all SQL builtin functions to
Tarantool's function hash. An existent FuncDef function
representation was replaced with func_sql_builtin class.
It has a sql-specific method :call and :finalize, while
port API call is not supported and protected with stubs.

This patch removes FuncDef hash and sql_function_create endpoint,
but doesn't introduce something instead. Therefore few affected
tests are disabled. A required functionality would be fixed in
the next patch.

Following tests using sql_create_function are broken now.
They are going to be fixed in the next commit:
 sql-tap/alias.test.lua sql-tap/check.test.lua
 sql-tap/func5.test.lua sql-tap/lua_sql.test.lua
 sql-tap/subquery.test.lua sql-tap/trigger9.test.lua
 sql/errinj.result sql/errinj.test.lua
 sql/func-recreate.test.lua

Part of #2200, #4113, #2233
---
 src/box/errcode.h               |   1 +
 src/box/lua/lua_sql.h           |  39 --
 src/box/sql.h                   |   1 +
 src/box/sql/sqlInt.h            | 222 +++-------
 src/box/sql/vdbe.h              |   9 +-
 src/box/sql/vdbeInt.h           |  23 +-
 src/box/func.c                  |  33 +-
 src/box/lua/call.c              |   2 -
 src/box/lua/lua_sql.c           | 205 ---------
 src/box/sql/analyze.c           |  34 +-
 src/box/sql/callback.c          | 204 ---------
 src/box/sql/date.c              |  28 --
 src/box/sql/expr.c              |  83 ++--
 src/box/sql/func.c              | 760 +++++++++++++++++++++++++++-----
 src/box/sql/global.c            |   7 -
 src/box/sql/main.c              | 137 ------
 src/box/sql/resolve.c           |  71 +--
 src/box/sql/select.c            |  10 +-
 src/box/sql/vdbe.c              |  20 +-
 src/box/sql/vdbeapi.c           |  17 +-
 src/box/sql/vdbeaux.c           |  31 +-
 src/box/sql/vdbemem.c           |  65 ++-
 src/box/sql/whereexpr.c         |   2 +-
 src/box/CMakeLists.txt          |   1 -
 src/box/alter.cc                |   6 +
 test/box/function1.result       |  20 +
 test/box/function1.test.lua     |   7 +
 test/box/misc.result            |   1 +
 test/sql-tap/func.test.lua      |  22 +-
 test/sql-tap/func2.test.lua     |  18 +-
 test/sql-tap/limit.test.lua     |   4 +-
 test/sql-tap/select1.test.lua   |  12 +-
 test/sql-tap/where2.test.lua    |   4 +-
 test/sql/icu-upper-lower.result |   4 +-
 34 files changed, 952 insertions(+), 1151 deletions(-)
 delete mode 100644 src/box/lua/lua_sql.h
 delete mode 100644 src/box/lua/lua_sql.c

diff --git a/src/box/errcode.h b/src/box/errcode.h
index 817275b97..2c19046fc 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -253,6 +253,7 @@ struct errcode_record {
 	/*198 */_(ER_FUNC_INDEX_FUNC,		"Failed to build a key for functional index '%s' of space '%s': %s") \
 	/*199 */_(ER_FUNC_INDEX_FORMAT,		"Key format doesn't match one defined in functional index '%s' of space '%s': %s") \
 	/*200 */_(ER_FUNC_INDEX_PARTS,		"Wrong functional index definition: %s") \
+	/*200 */_(ER_FUNC_WRONG_ARG_COUNT,	"Wrong number of arguments is passed to %s(): expected %s, got %d")
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
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/sql.h b/src/box/sql.h
index 7644051b4..b29f5aab5 100644
--- a/src/box/sql.h
+++ b/src/box/sql.h
@@ -68,6 +68,7 @@ struct Expr;
 struct Parse;
 struct Select;
 struct Table;
+struct func_def;
 struct sql_trigger;
 struct space_def;
 
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index d71fd06d7..d08321408 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -70,6 +70,8 @@
 #include "box/column_mask.h"
 #include "parse_def.h"
 #include "box/field_def.h"
+#include "box/func.h"
+#include "box/func_def.h"
 #include "box/sql.h"
 #include "box/txn.h"
 #include "trivia/util.h"
@@ -541,9 +543,6 @@ void
 sql_row_count(struct sql_context *context, MAYBE_UNUSED int unused1,
 	      MAYBE_UNUSED sql_value **unused2);
 
-void *
-sql_user_data(sql_context *);
-
 void *
 sql_aggregate_context(sql_context *,
 			  int nBytes);
@@ -566,26 +565,6 @@ sql_initialize(void);
 #define SQL_TRACE_ROW        0x04
 #define SQL_TRACE_CLOSE      0x08
 
-#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 *)
-	);
-
 #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() */
@@ -1039,9 +1018,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 Lookaside Lookaside;
@@ -1123,18 +1099,6 @@ struct LookasideSlot {
 	LookasideSlot *pNext;	/* Next buffer in the list of free buffers */
 };
 
-/*
- * 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.
  */
@@ -1244,65 +1208,12 @@ 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 */
-	void (*xSFunc) (sql_context *, int, sql_value **);	/* func or agg-step */
-	void (*xFinalize) (sql_context *);	/* Agg finalizer */
-	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.
- *
  * Value constraints (enforced via assert()):
  *     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 */
 #define SQL_FUNC_NEEDCOLL 0x0020	/* sqlGetFuncCollSeq() might be called.
 					 * The flag is set when the collation
 					 * of function arguments should be
@@ -1316,11 +1227,10 @@ struct FuncDestructor {
 #define SQL_FUNC_COUNT    0x0100
 #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 */
 /** Built-in min() or least() function. */
 #define SQL_FUNC_MIN      0x1000
 /** Built-in max() or greatest() function. */
-#define SQL_FUNC_MAX      0x2000
+#define SQL_FUNC_MAX   0x2000
 /**
  * If function returns string, it may require collation to be
  * applied on its result. For instance, result of substr()
@@ -1340,61 +1250,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.
- *
- *   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 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 LIKEFUNC(zName, nArg, arg, flags, type) \
-  {nArg, SQL_FUNC_NEEDCOLL|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
@@ -1587,7 +1442,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 */
 		/**
@@ -3535,10 +3391,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.
@@ -4111,7 +3963,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;
 
 /**
@@ -4247,21 +4098,13 @@ sql_key_info_to_key_def(struct sql_key_info *key_info);
  * Check if the function implements LIKE-style comparison & if it
  * is appropriate to apply a LIKE query optimization.
  *
- * @param db database structure.
  * @param pExpr pointer to a function-implementing expression.
  * @param[out] is_like_ci true if LIKE is case insensitive.
  *
  * @retval 1 if LIKE optimization can be used, 0 otherwise.
  */
 int
-sql_is_like_func(struct sql *db, struct Expr *expr);
-
-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);
+sql_is_like_func(struct Expr *expr);
 
 /** Set OOM error flag. */
 static inline void
@@ -4298,7 +4141,6 @@ sql_expr_new_column(struct sql *db, struct SrcList *src_list, int src_idx,
 
 int sqlExprCheckIN(Parse *, Expr *);
 
-void sqlAnalyzeFunctions(void);
 int sqlStat4ProbeSetValue(Parse *, struct index_def *, UnpackedRecord **, Expr *, int,
 			      int, int *);
 int sqlStat4ValueFromExpr(Parse *, Expr *, enum field_type type,
@@ -4489,9 +4331,55 @@ Expr *sqlExprForVectorField(Parse *, Expr *, int);
  */
 extern int sqlSubProgramsRemaining;
 
-/** Register built-in functions to work with ANALYZE data. */
-void
-sql_register_analyze_builtins(void);
+struct func_sql_builtin {
+	/** Function object base class. */
+	struct func base;
+	/** A bitmask of SQL flags. */
+	uint16_t flags;
+	/**
+	 * A VDBE-memory-compatible call method.
+	 * SQL built-ins don't use func base class "call"
+	 * method to provide a best performance for SQL requests.
+	 * Access checks are redundant, because all SQL built-ins
+	 * are predefined and are executed on SQL privilege level.
+	 */
+	void (*call)(sql_context *ctx, int argc, sql_value **argv);
+	/**
+	 * A VDBE-memory-compatible finalize method
+	 * (is valid only for aggregate function).
+	 */
+	void (*finalize)(sql_context *ctx);
+};
+
+/**
+ * Test whether SQL-specific flag is set for given function.
+ * Currently only SQL Builtin Functions have such hint flags,
+ * so function returns false for other functions. Such approach
+ * decreases code complexity and allows do not distinguish
+ * functions by implementation details where it is unnecessary.
+ *
+ * Returns true when given flag is set for a given function and
+ * false otherwise.
+ */
+static inline bool
+sql_func_flag_is_set(struct func *func, uint16_t flag)
+{
+	if (func->def->language != FUNC_LANGUAGE_SQL_BUILTIN)
+		return false;
+	return (((struct func_sql_builtin *)func)->flags & flag) != 0;
+}
+
+/**
+ * A SQL method to find a function in a hash by its name and
+ * count of arguments. Only functions that have 'SQL' engine
+ * export field set true and have exactly the same signature
+ * are returned.
+ *
+ * Returns not NULL function pointer when a valid and exported
+ * to SQL engine function is found and NULL otherwise.
+ */
+struct func *
+sql_func_by_signature(const char *name, int argc);
 
 /**
  * Generate VDBE code to halt execution with correct error if
diff --git a/src/box/sql/vdbe.h b/src/box/sql/vdbe.h
index e5a85fe0e..f8dbd0fa3 100644
--- a/src/box/sql/vdbe.h
+++ b/src/box/sql/vdbe.h
@@ -72,7 +72,11 @@ struct VdbeOp {
 		char *z;	/* Pointer to data for string (char array) types */
 		i64 *pI64;	/* Used when p4type is P4_INT64/UINT64 */
 		double *pReal;	/* Used when p4type is P4_REAL */
-		FuncDef *pFunc;	/* Used when p4type is P4_FUNCDEF */
+		/**
+		 * A pointer to function implementation.
+		 * 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 +126,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 func structure. */
+#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 f77c019fb..af55f4cdc 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
  */
@@ -168,7 +170,11 @@ 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 */
+		/**
+		 * A pointer to function implementation.
+		 * 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. */
@@ -309,7 +315,8 @@ mem_apply_numeric_type(struct Mem *record);
  */
 struct sql_context {
 	Mem *pOut;		/* The return value is stored here */
-	FuncDef *pFunc;		/* Pointer to function information */
+	/* A pointer to function implementation. */
+	struct func *func;
 	Mem *pMem;		/* Memory cell used to store aggregate context */
 	Vdbe *pVdbe;		/* The VM that owns this context */
 	/** Instruction number of OP_BuiltinFunction0. */
@@ -516,7 +523,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 mem 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 mem.
+ *
+ * Returns -1 if the finalizer reports an error. 0 otherwise.
+ */
+int
+sql_vdbemem_finalize(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/func.c b/src/box/func.c
index b35d05dca..e8cc99081 100644
--- a/src/box/func.c
+++ b/src/box/func.c
@@ -381,38 +381,9 @@ restore:
 static struct func *
 func_c_new(struct func_def *def);
 
-/** A stub object for SQL builtins to avoid name clash with UDF. */
-static struct func_vtab func_sql_builtin_vtab;
-
 /** Construct a SQL builtin function object. */
-struct func *
-func_sql_builtin_new(struct func_def *def)
-{
-	assert(def->language == FUNC_LANGUAGE_SQL_BUILTIN);
-	struct func *func =
-		(struct func *) 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->vtab = &func_sql_builtin_vtab;
-	return func;
-}
-
-static void
-func_sql_builtin_destroy(struct func *func)
-{
-	assert(func->vtab == &func_sql_builtin_vtab);
-	assert(func->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
-	free(func);
-}
-
-static struct func_vtab func_sql_builtin_vtab = {
-	.call = NULL,
-	.destroy = func_sql_builtin_destroy,
-};
+extern struct func *
+func_sql_builtin_new(struct func_def *def);
 
 struct func *
 func_new(struct func_def *def)
diff --git a/src/box/lua/call.c b/src/box/lua/call.c
index 0ac2eb7a6..001578b5a 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"
 
@@ -968,7 +967,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 67a51a82c..000000000
--- a/src/box/lua/lua_sql.c
+++ /dev/null
@@ -1,205 +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/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 void
-lua_sql_call(sql_context *pCtx, int nVal, sql_value **apVal) {
-	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);
-
-	lua_rawgeti(L, LUA_REGISTRYINDEX, func_info->func_ref);
-	for (int i = 0; i < nVal; i++) {
-		sql_value *param = apVal[i];
-		switch (sql_value_type(param)) {
-		case MP_INT:
-			luaL_pushint64(L, sql_value_int64(param));
-			break;
-		case MP_UINT:
-			luaL_pushuint64(L, sql_value_uint64(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");
-			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));
-		break;
-	case LUA_TNUMBER:
-		sql_result_double(pCtx, lua_tonumber(L, -1));
-		break;
-	case LUA_TSTRING:
-		sql_result_text(pCtx, lua_tostring(L, -1), -1,
-				    SQL_TRANSIENT);
-		break;
-	case LUA_TNIL:
-		sql_result_null(pCtx);
-		break;
-	default:
-		diag_set(ClientError, ER_SQL_EXECUTE, "Unsupported type "\
-			 "passed from Lua");
-		pCtx->is_aborted = true;
-		goto error;
-	}
-error:
-	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, coro_ref);
-	return;
-}
-
-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, "NUMBER") == 0)
-		type = FIELD_TYPE_NUMBER;
-	else if (strcmp(type_arg, "VARBINARY") == 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/sql/analyze.c b/src/box/sql/analyze.c
index bd52d12df..b9858c8d6 100644
--- a/src/box/sql/analyze.c
+++ b/src/box/sql/analyze.c
@@ -263,7 +263,7 @@ stat4Destructor(void *pOld)
  * return value is BLOB, but it is really just a pointer to the Stat4Accum
  * object.
  */
-static void
+MAYBE_UNUSED static void
 statInit(sql_context * context, int argc, sql_value ** argv)
 {
 	Stat4Accum *p;
@@ -535,7 +535,7 @@ samplePushPrevious(Stat4Accum * p, int iChng)
  *
  * The R parameter is only used for STAT4
  */
-static void
+MAYBE_UNUSED static void
 statPush(sql_context * context, int argc, sql_value ** argv)
 {
 	int i;
@@ -608,7 +608,7 @@ 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
+MAYBE_UNUSED static void
 statGet(sql_context * context, int argc, sql_value ** argv)
 {
 	Stat4Accum *p = (Stat4Accum *) sql_value_blob(argv[0]);
@@ -715,11 +715,10 @@ 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);
+	struct func *func = sql_func_by_signature("_sql_stat_get", 2);
 	assert(func != NULL);
 	sqlVdbeAddOp4(v, OP_BuiltinFunction0, 0, regStat4, regOut,
-		      (char *)func, P4_FUNCDEF);
+		      (char *)func, P4_FUNC);
 	sqlVdbeChangeP5(v, 2);
 }
 
@@ -855,11 +854,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);
+		struct func *init_func =
+			sql_func_by_signature("_sql_stat_init", 3);
 		assert(init_func != NULL);
 		sqlVdbeAddOp4(v, OP_BuiltinFunction0, 0, stat4_reg + 1,
-			      stat4_reg, (char *)init_func, P4_FUNCDEF);
+			      stat4_reg, (char *)init_func, P4_FUNC);
 		sqlVdbeChangeP5(v, 3);
 		/*
 		 * Implementation of the following:
@@ -956,11 +955,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);
+		struct func *push_func =
+			sql_func_by_signature("_sql_stat_push", 3);
 		assert(push_func != NULL);
 		sqlVdbeAddOp4(v, OP_BuiltinFunction0, 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. */
@@ -1746,14 +1745,3 @@ 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/callback.c b/src/box/sql/callback.c
index 6c272de71..290363db6 100644
--- a/src/box/sql/callback.c
+++ b/src/box/sql/callback.c
@@ -56,207 +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.
-	 *
-	 * 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 1f9d91705..9055a0770 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -328,12 +328,11 @@ 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, arg_count);
 			if (func == NULL)
 				break;
-			if ((func->funcFlags & SQL_FUNC_DERIVEDCOLL) != 0) {
+			if (sql_func_flag_is_set(func, SQL_FUNC_DERIVEDCOLL)) {
 				/*
 				 * Now we use quite straightforward
 				 * approach assuming that resulting
@@ -342,7 +341,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;
 			}
@@ -3975,11 +3974,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));
@@ -3991,8 +3988,8 @@ 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, nFarg);
+			if (func == NULL) {
 				diag_set(ClientError, ER_NO_SUCH_FUNCTION,
 					 zId);
 				pParse->is_aborted = true;
@@ -4002,9 +3999,16 @@ 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 (sql_func_flag_is_set(func, SQL_FUNC_COALESCE)) {
 				int endCoalesce = sqlVdbeMakeLabel(v);
-				assert(nFarg >= 2);
+				if (nFarg < 2) {
+					diag_set(ClientError,
+						 ER_FUNC_WRONG_ARG_COUNT,
+						 func->def->name,
+						 ">= 2", nFarg);
+					pParse->is_aborted = true;
+					break;
+				}
 				sqlExprCode(pParse, pFarg->a[0].pExpr,
 						target);
 				for (i = 1; i < nFarg; i++) {
@@ -4026,8 +4030,15 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 			/* 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);
+			if (sql_func_flag_is_set(func, SQL_FUNC_UNLIKELY)) {
+				if (nFarg < 1) {
+					diag_set(ClientError,
+						 ER_FUNC_WRONG_ARG_COUNT,
+						 func->def->name,
+						 ">= 1", nFarg);
+					pParse->is_aborted = true;
+					break;
+				}
 				return sqlExprCodeTarget(pParse,
 							     pFarg->a[0].pExpr,
 							     target);
@@ -4049,7 +4060,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 (sql_func_flag_is_set(func, SQL_FUNC_NEEDCOLL)) {
 				struct coll *unused = NULL;
 				uint32_t curr_id = COLL_NONE;
 				bool is_curr_forced = false;
@@ -4096,9 +4107,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 (sql_func_flag_is_set(func, SQL_FUNC_LENGTH |
+							       SQL_FUNC_TYPEOF)) {
 					u8 exprOp;
 					assert(nFarg == 1);
 					assert(pFarg->a[0].pExpr != 0);
@@ -4109,14 +4119,7 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 						       OPFLAG_LENGTHARG);
 						assert(SQL_FUNC_TYPEOF ==
 						       OPFLAG_TYPEOFARG);
-						testcase(pDef->
-							 funcFlags &
-							 OPFLAG_LENGTHARG);
-						pFarg->a[0].pExpr->op2 =
-						    pDef->
-						    funcFlags &
-						    (OPFLAG_LENGTHARG |
-						     OPFLAG_TYPEOFARG);
+						pFarg->a[0].pExpr->op2 = true;
 					}
 				}
 
@@ -4128,12 +4131,15 @@ sqlExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 			} else {
 				r1 = 0;
 			}
-			if (pDef->funcFlags & SQL_FUNC_NEEDCOLL) {
+			if (sql_func_flag_is_set(func, SQL_FUNC_NEEDCOLL)) {
 				sqlVdbeAddOp4(v, OP_CollSeq, 0, 0, 0,
 						  (char *)coll, P4_COLLSEQ);
 			}
-			sqlVdbeAddOp4(v, OP_BuiltinFunction0, constMask, r1,
-					  target, (char *)pDef, P4_FUNCDEF);
+			assert(func->def->language ==
+			       FUNC_LANGUAGE_SQL_BUILTIN);
+			int op = OP_BuiltinFunction0;
+			sqlVdbeAddOp4(v, op, constMask, r1, target,
+				      (char *)func, P4_FUNC);
 			sqlVdbeChangeP5(v, (u8) nFarg);
 			if (nFarg && constMask == 0) {
 				sqlReleaseTempRange(pParse, r1, nFarg);
@@ -5441,12 +5447,21 @@ analyzeAggregate(Walker * pWalker, Expr * pExpr)
 						pItem->iMem = ++pParse->nMem;
 						assert(!ExprHasProperty
 						       (pExpr, EP_IntValue));
-						pItem->pFunc = sqlFindFunction(
-							pParse->db,
-							pExpr->u.zToken,
-							pExpr->x.pList ?
-							pExpr->x.pList->nExpr : 0,
-							0);
+						const char *name =
+							pExpr->u.zToken;
+						uint32_t argc =
+							pExpr->x.pList != NULL ?
+							pExpr->x.pList->nExpr : 0;
+						pItem->func =
+							sql_func_by_signature(
+								name, argc);
+						assert(pItem->func != NULL);
+						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 a46df60ed..041183b8a 100644
--- a/src/box/sql/func.c
+++ b/src/box/sql/func.c
@@ -45,6 +45,8 @@
 #include <unicode/uchar.h>
 #include <unicode/ucol.h>
 #include "box/coll_id_cache.h"
+#include "box/schema.h"
+#include "box/func.h"
 
 /*
  * Return the collating function associated with a function.
@@ -79,10 +81,11 @@ minmaxFunc(sql_context * context, int argc, sql_value ** argv)
 	int i;
 	int iBest;
 	struct coll *pColl;
-	struct FuncDef *func = context->pFunc;
+	struct func_sql_builtin *func =
+		(struct func_sql_builtin *)context->func;
 
 	assert(argc > 1);
-	int mask = (func->funcFlags & SQL_FUNC_MAX) != 0 ? -1 : 0;
+	int mask = (func->flags & SQL_FUNC_MAX) != 0 ? -1 : 0;
 	pColl = sqlGetFuncCollSeq(context);
 	assert(mask == -1 || mask == 0);
 	iBest = 0;
@@ -410,7 +413,12 @@ substrFunc(sql_context * context, int argc, sql_value ** argv)
 	i64 p1, p2;
 	int negP2 = 0;
 
-	assert(argc == 3 || argc == 2);
+	if (argc != 2 && argc != 3) {
+		diag_set(ClientError, ER_FUNC_WRONG_ARG_COUNT, "SUBSTR",
+			 "1 or 2", argc);
+		context->is_aborted = true;
+		return;
+	}
 	if (sql_value_is_null(argv[1])
 	    || (argc == 3 && sql_value_is_null(argv[2]))
 	    ) {
@@ -506,7 +514,12 @@ roundFunc(sql_context * context, int argc, sql_value ** argv)
 {
 	int n = 0;
 	double r;
-	assert(argc == 1 || argc == 2);
+	if (argc != 1 && argc != 2) {
+		diag_set(ClientError, ER_FUNC_WRONG_ARG_COUNT, "ROUND",
+			 "1 or 2", argc);
+		context->is_aborted = true;
+		return;
+	}
 	if (argc == 2) {
 		if (sql_value_is_null(argv[1]))
 			return;
@@ -898,6 +911,12 @@ likeFunc(sql_context *context, int argc, sql_value **argv)
 {
 	u32 escape = SQL_END_OF_STRING;
 	int nPat;
+	if (argc != 2 && argc != 3) {
+		diag_set(ClientError, ER_FUNC_WRONG_ARG_COUNT,
+			 "LIKE", "2 or 3", argc);
+		context->is_aborted = true;
+		return;
+	}
 	sql *db = sql_context_db_handle(context);
 	int rhs_type = sql_value_type(argv[0]);
 	int lhs_type = sql_value_type(argv[1]);
@@ -1510,7 +1529,9 @@ trim_func(struct sql_context *context, int argc, sql_value **argv)
 		trim_func_three_args(context, argv[0], argv[1], argv[2]);
 		break;
 	default:
-		unreachable();
+		diag_set(ClientError, ER_FUNC_WRONG_ARG_COUNT, "TRIM",
+			 "1 or 2 or 3", argc);
+		context->is_aborted = true;
 	}
 }
 
@@ -1689,6 +1710,12 @@ static void
 countStep(sql_context * context, int argc, sql_value ** argv)
 {
 	CountCtx *p;
+	if (argc != 0 && argc != 1) {
+		diag_set(ClientError, ER_FUNC_WRONG_ARG_COUNT,
+			 "COUNT", "0 or 1", argc);
+		context->is_aborted = true;
+		return;
+	}
 	p = sql_aggregate_context(context, sizeof(*p));
 	if ((argc == 0 || ! sql_value_is_null(argv[0])) && p) {
 		p->n++;
@@ -1713,6 +1740,8 @@ minmaxStep(sql_context * context, int NotUsed, sql_value ** argv)
 	Mem *pBest;
 	UNUSED_PARAMETER(NotUsed);
 
+	struct func_sql_builtin *func =
+		(struct func_sql_builtin *)context->func;
 	pBest = (Mem *) sql_aggregate_context(context, sizeof(*pBest));
 	if (!pBest)
 		return;
@@ -1729,7 +1758,7 @@ minmaxStep(sql_context * context, int NotUsed, sql_value ** argv)
 		 * between the two being that the sense of the
 		 * comparison is inverted.
 		 */
-		bool is_max = (context->pFunc->funcFlags & SQL_FUNC_MAX) != 0;
+		bool is_max = (func->flags & SQL_FUNC_MAX) != 0;
 		cmp = sqlMemCompare(pBest, pArg, pColl);
 		if ((is_max && cmp < 0) || (!is_max && cmp > 0)) {
 			sqlVdbeMemCopy(pBest, pArg);
@@ -1765,7 +1794,12 @@ groupConcatStep(sql_context * context, int argc, sql_value ** argv)
 	StrAccum *pAccum;
 	const char *zSep;
 	int nVal, nSep;
-	assert(argc == 1 || argc == 2);
+	if (argc != 1 && argc != 2) {
+		diag_set(ClientError, ER_FUNC_WRONG_ARG_COUNT,
+			 "GROUP_CONCAT", "1 or 2", argc);
+		context->is_aborted = true;
+		return;
+	}
 	if (sql_value_is_null(argv[0]))
 		return;
 	pAccum =
@@ -1814,129 +1848,621 @@ groupConcatFinalize(sql_context * context)
 }
 
 int
-sql_is_like_func(struct sql *db, struct Expr *expr)
+sql_is_like_func(struct Expr *expr)
 {
 	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);
-	assert(func != NULL);
-	if ((func->funcFlags & SQL_FUNC_LIKE) == 0)
+	struct func *func = sql_func_by_signature(expr->u.zToken, 2);
+	if (func == NULL || !sql_func_flag_is_set(func, SQL_FUNC_LIKE))
 		return 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
+struct func *
+sql_func_by_signature(const char *name, int argc)
+{
+	struct func *base = func_by_name(name, strlen(name));
+	if (base == NULL || !base->def->exports.sql)
+		return NULL;
+
+	if (base->def->param_count != -1 && base->def->param_count != argc)
+		return NULL;
+	return base;
+}
+
+static int
+func_sql_builtin_call_stub(struct func *func, struct port *args,
+			   struct port *ret)
+{
+	(void) func; (void) args; (void) ret;
+	diag_set(ClientError, ER_UNSUPPORTED,
+		 "sql builtin function", "Lua frontend");
+	return -1;
+}
+
+static void
+sql_builtin_stub(sql_context *ctx, int argc, sql_value **argv)
+{
+	(void) argc; (void) argv;
+	diag_set(ClientError, ER_SQL_EXECUTE,
+		 tt_sprintf("function '%s' is not implemented",
+			    ctx->func->def->name));
+	ctx->is_aborted = true;
+}
+
+/**
+ * A sequence of SQL builtins definitions in
+ * lexicographic order.
  */
-void
-sqlRegisterBuiltinFunctions(void)
-{
-	/*
-	 * 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 struct {
+	/**
+	 * Name is used to find corresponding entry in array
+	 * sql_builtins applying binary search.
 	 */
-	static FuncDef aBuiltinFunc[] = {
-		FUNCTION(soundex, 1, 0, 0, soundexFunc, FIELD_TYPE_STRING),
-		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),
-		FUNCTION_COLL(trim, 2, 3, 0, trim_func),
-		FUNCTION_COLL(trim, 3, 3, 0, trim_func),
-		FUNCTION2(least, -1, 0, 1, minmaxFunc, SQL_FUNC_MIN,
-			  FIELD_TYPE_SCALAR),
-		AGGREGATE2(min, 1, 0, 1, minmaxStep, minMaxFinalize,
-			   SQL_FUNC_MIN, FIELD_TYPE_SCALAR),
-		FUNCTION2(greatest, -1, 1, 1, minmaxFunc, SQL_FUNC_MAX,
-			  FIELD_TYPE_SCALAR),
-		AGGREGATE2(max, 1, 1, 1, minmaxStep, minMaxFinalize,
-			   SQL_FUNC_MAX, FIELD_TYPE_SCALAR),
-		FUNCTION2(typeof, 1, 0, 0, typeofFunc, SQL_FUNC_TYPEOF,
-			  FIELD_TYPE_STRING),
-		FUNCTION2(length, 1, 0, 0, lengthFunc, SQL_FUNC_LENGTH,
-			  FIELD_TYPE_INTEGER),
-		FUNCTION(char_length, 1, 0, 0, lengthFunc, FIELD_TYPE_INTEGER),
-		FUNCTION(character_length, 1, 0, 0, lengthFunc,
-			 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,
-			  FIELD_TYPE_INTEGER),
-		VFUNCTION(random, 0, 0, 0, randomFunc, FIELD_TYPE_INTEGER),
-		VFUNCTION(randomblob, 1, 0, 0, randomBlob, FIELD_TYPE_VARBINARY),
-		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_VARBINARY),
-		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),
-		AGGREGATE2(count, 1, 0, 0, countStep, countFinalize,
-			   SQL_FUNC_COUNT, 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),
+	const char *name;
+	/** Members below are related to struct func_sql_builtin. */
+	uint16_t flags;
+	void (*call)(sql_context *ctx, int argc, sql_value **argv);
+	void (*finalize)(sql_context *ctx);
+	/** Members below are related to struct func_def. */
+	bool is_deterministic;
+	int param_count;
+	enum field_type returns;
+	enum func_aggregate aggregate;
+	bool export_to_sql;
+} sql_builtins[] = {
+	{.name = "ABS",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_NUMBER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = absFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "AVG",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_NUMBER,
+	 .is_deterministic = false,
+	 .aggregate = FUNC_AGGREGATE_GROUP,
+	 .flags = 0,
+	 .call = sum_step,
+	 .finalize = avgFinalize,
+	 .export_to_sql = true,
+	}, {
+	 .name = "CEIL",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "CEILING",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "CHAR",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_STRING,
+	 .is_deterministic = true,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .flags = 0,
+	 .call = charFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	 }, {
+	 .name = "CHARACTER_LENGTH",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = lengthFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "CHAR_LENGTH",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = lengthFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "COALESCE",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_SCALAR,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_COALESCE,
+	 .call = sql_builtin_stub,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "COUNT",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_GROUP,
+	 .is_deterministic = false,
+	 .flags = 0,
+	 .call = countStep,
+	 .finalize = countFinalize,
+	 .export_to_sql = true,
+	}, {
+	 .name = "CURRENT_DATE",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "CURRENT_TIME",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "CURRENT_TIMESTAMP",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "DATE",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "DATETIME",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "EVERY",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "EXISTS",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "EXP",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "EXTRACT",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "FLOOR",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "GREATER",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "GREATEST",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_SCALAR,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_NEEDCOLL | SQL_FUNC_MAX,
+	 .call = minmaxFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "GROUP_CONCAT",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_GROUP,
+	 .is_deterministic = false,
+	 .flags = 0,
+	 .call = groupConcatStep,
+	 .finalize = groupConcatFinalize,
+	 .export_to_sql = true,
+	}, {
+	 .name = "HEX",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = hexFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "IFNULL",
+	 .param_count = 2,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_COALESCE,
+	 .call = sql_builtin_stub,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "JULIANDAY",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "LEAST",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_SCALAR,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_NEEDCOLL | SQL_FUNC_MIN,
+	 .call = minmaxFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "LENGTH",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_LENGTH,
+	 .call = lengthFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "LESSER",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "LIKE",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_NEEDCOLL | SQL_FUNC_LIKE,
+	 .call = likeFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "LIKELIHOOD",
+	 .param_count = 2,
+	 .returns = FIELD_TYPE_BOOLEAN,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_UNLIKELY,
+	 .call = sql_builtin_stub,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "LIKELY",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_BOOLEAN,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_UNLIKELY,
+	 .call = sql_builtin_stub,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "LN",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "LOWER",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_DERIVEDCOLL | SQL_FUNC_NEEDCOLL,
+	 .call = LowerICUFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "MAX",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_SCALAR,
+	 .aggregate = FUNC_AGGREGATE_GROUP,
+	 .is_deterministic = false,
+	 .flags = SQL_FUNC_NEEDCOLL | SQL_FUNC_MAX,
+	 .call = minmaxStep,
+	 .finalize = minMaxFinalize,
+	 .export_to_sql = true,
+	}, {
+	 .name = "MIN",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_SCALAR,
+	 .aggregate = FUNC_AGGREGATE_GROUP,
+	 .is_deterministic = false,
+	 .flags = SQL_FUNC_NEEDCOLL | SQL_FUNC_MIN,
+	 .call = minmaxStep,
+	 .finalize = minMaxFinalize,
+	 .export_to_sql = true,
+	}, {
+	 .name = "MOD",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "NULLIF",
+	 .param_count = 2,
+	 .returns = FIELD_TYPE_SCALAR,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_NEEDCOLL,
+	 .call = nullifFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "OCTET_LENGTH",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "POSITION",
+	 .param_count = 2,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_NEEDCOLL,
+	 .call = position_func,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "POWER",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "PRINTF",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = printfFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "QUOTE",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = quoteFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "RANDOM",
+	 .param_count = 0,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = false,
+	 .flags = 0,
+	 .call = randomFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "RANDOMBLOB",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_VARBINARY,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = false,
+	 .flags = 0,
+	 .call = randomBlob,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "REPLACE",
+	 .param_count = 3,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_DERIVEDCOLL,
+	 .call = replaceFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "ROUND",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = roundFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "ROW_COUNT",
+	 .param_count = 0,
+	 .returns = FIELD_TYPE_INTEGER,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = sql_row_count,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "SOME",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "SOUNDEX",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = soundexFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "SQRT",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "STRFTIME",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "SUBSTR",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_DERIVEDCOLL,
+	 .call = substrFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "SUM",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_NUMBER,
+	 .aggregate = FUNC_AGGREGATE_GROUP,
+	 .is_deterministic = false,
+	 .flags = 0,
+	 .call = sum_step,
+	 .finalize = sumFinalize,
+	 .export_to_sql = true,
+	}, {
+	 .name = "TIME",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "TOTAL",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_NUMBER,
+	 .aggregate = FUNC_AGGREGATE_GROUP,
+	 .is_deterministic = false,
+	 .flags = 0,
+	 .call = sum_step,
+	 .finalize = totalFinalize,
+	 .export_to_sql = true,
+	}, {
+	 .name = "TRIM",
+	 .param_count = -1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_DERIVEDCOLL,
+	 .call = trim_func,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "TYPEOF",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_TYPEOF,
+	 .call = typeofFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "UNICODE",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = unicodeFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "UNLIKELY",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_BOOLEAN,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_UNLIKELY,
+	 .call = sql_builtin_stub,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "UPPER",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = SQL_FUNC_DERIVEDCOLL | SQL_FUNC_NEEDCOLL,
+	 .call = UpperICUFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "VERSION",
+	 .param_count = 0,
+	 .returns = FIELD_TYPE_STRING,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = sql_func_version,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "ZEROBLOB",
+	 .param_count = 1,
+	 .returns = FIELD_TYPE_VARBINARY,
+	 .aggregate = FUNC_AGGREGATE_NONE,
+	 .is_deterministic = true,
+	 .flags = 0,
+	 .call = zeroblobFunc,
+	 .finalize = NULL,
+	 .export_to_sql = true,
+	}, {
+	 .name = "_sql_stat_get",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "_sql_stat_init",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	}, {
+	 .name = "_sql_stat_push",
+	 .call = sql_builtin_stub,
+	 .export_to_sql = false,
+	},
+};
 
-		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),
-	};
-	sql_register_analyze_builtins();
-	sqlRegisterDateTimeFunctions();
-	sqlInsertBuiltinFuncs(aBuiltinFunc, ArraySize(aBuiltinFunc));
+static struct func_vtab func_sql_builtin_vtab;
 
-#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 *
+func_sql_builtin_new(struct func_def *def)
+{
+	assert(def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	/** 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;
 	}
-#endif
+	/* Some builtins are not implemented yet. */
+	if (idx == -1) {
+		diag_set(ClientError, ER_CREATE_FUNCTION, def->name,
+			 "given built-in is not predefined");
+		return NULL;
+	}
+	struct func_sql_builtin *func =
+		(struct func_sql_builtin *) calloc(1, sizeof(*func));
+	if (func == NULL) {
+		diag_set(OutOfMemory, sizeof(*func), "malloc", "func");
+		return NULL;
+	}
+	func->base.def = def;
+	func->base.vtab = &func_sql_builtin_vtab;
+	func->flags = sql_builtins[idx].flags;
+	func->call = sql_builtins[idx].call;
+	func->finalize = sql_builtins[idx].finalize;
+	def->param_count = sql_builtins[idx].param_count;
+	def->is_deterministic = sql_builtins[idx].is_deterministic;
+	def->returns = sql_builtins[idx].returns;
+	def->aggregate = sql_builtins[idx].aggregate;
+	def->exports.sql = sql_builtins[idx].export_to_sql;
+	return &func->base;
+}
+
+static void
+func_sql_builtin_destroy(struct func *func)
+{
+	assert(func->vtab == &func_sql_builtin_vtab);
+	assert(func->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	free(func);
 }
+
+static struct func_vtab func_sql_builtin_vtab = {
+	.call = func_sql_builtin_call_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 748dc1b8e..0b20f2132 100644
--- a/src/box/sql/main.c
+++ b/src/box/sql/main.c
@@ -131,9 +131,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;
@@ -242,25 +239,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
@@ -279,121 +257,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(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)
-{
-	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(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 *))
-{
-	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 83dff47b6..231c670f5 100644
--- a/src/box/sql/resolve.c
+++ b/src/box/sql/resolve.c
@@ -38,6 +38,7 @@
 #include "sqlInt.h"
 #include <stdlib.h>
 #include <string.h>
+#include "box/schema.h"
 
 /*
  * Walk the expression tree pExpr and increase the aggregate function
@@ -591,32 +592,46 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 	case TK_FUNCTION:{
 			ExprList *pList = pExpr->x.pList;	/* The argument list */
 			int n = pList ? pList->nExpr : 0;	/* Number of arguments */
-			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) {
-					no_such_func = 1;
+			struct func *func = sql_func_by_signature(zId, n);
+			if (func == NULL) {
+				func = func_by_name(zId, nId);
+				if (func == NULL) {
+					diag_set(ClientError,
+						 ER_NO_SUCH_FUNCTION, zId);
+				} else if (!func->def->exports.sql) {
+					diag_set(ClientError,
+						 ER_SQL_PARSER_GENERIC,
+						 tt_sprintf("function %.*s() "
+							    "is not available "
+							    "in SQL", nId, zId));
 				} else {
-					wrong_num_args = 1;
+					uint32_t argc = func->def->param_count;
+					const char *err = tt_sprintf("%d", argc);
+					diag_set(ClientError,
+						 ER_FUNC_WRONG_ARG_COUNT,
+						 func->def->name, err, n);
 				}
+				pParse->is_aborted = true;
+				pNC->nErr++;
 			} else {
-				is_agg = pDef->xFinalize != 0;
-				pExpr->type = pDef->ret_type;
+				is_agg = func->def->aggregate ==
+					 FUNC_AGGREGATE_GROUP;
+				assert(!is_agg ||
+				       func->def->language ==
+				       FUNC_LANGUAGE_SQL_BUILTIN);
+				pExpr->type = func->def->returns;
 				const char *err =
 					"second argument to likelihood() must "\
 					"be a constant between 0.0 and 1.0";
-				if (pDef->funcFlags & SQL_FUNC_UNLIKELY) {
+				if (sql_func_flag_is_set(func,
+							 SQL_FUNC_UNLIKELY)) {
 					ExprSetProperty(pExpr,
 							EP_Unlikely | EP_Skip);
 					if (n == 2) {
@@ -643,19 +658,17 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 						 */
 						/* TUNING: unlikely() probability is 0.0625.  likely() is 0.9375 */
 						pExpr->iTable =
-						    pDef->zName[0] ==
-						    'u' ? 8388608 : 125829120;
+						    func->def->name[0] == 'u' ?
+						    8388608 : 125829120;
 					}
 				}
-				if ((pDef->funcFlags & SQL_FUNC_CONSTANT) != 0) {
+				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.
@@ -671,17 +684,6 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 				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);
-				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));
-				pParse->is_aborted = true;
-				pNC->nErr++;
 			}
 			if (is_agg)
 				pNC->ncFlags &= ~NC_AllowAgg;
@@ -698,13 +700,12 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 					pExpr->op2++;
 					pNC2 = pNC2->pNext;
 				}
-				assert(pDef != 0);
+				assert(func != NULL);
 				if (pNC2) {
 					pNC2->ncFlags |= NC_HasAgg;
-					if ((pDef->funcFlags &
-					    SQL_FUNC_MIN) != 0 ||
-					    (pDef->funcFlags &
-					    SQL_FUNC_MAX) != 0)
+					if (sql_func_flag_is_set(func,
+							         SQL_FUNC_MIN |
+								 SQL_FUNC_MAX))
 						pNC2->ncFlags |= NC_MinMaxAgg;
 				}
 				pNC->ncFlags |= NC_AllowAgg;
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index ddb5de87e..cdcdbaf2b 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -4381,7 +4381,9 @@ is_simple_count(struct Select *select, struct AggInfo *agg_info)
 		return NULL;
 	if (NEVER(agg_info->nFunc == 0))
 		return NULL;
-	if ((agg_info->aFunc->pFunc->funcFlags & SQL_FUNC_COUNT) == 0 ||
+	assert(agg_info->aFunc->func->def->language ==
+	       FUNC_LANGUAGE_SQL_BUILTIN);
+	if (sql_func_flag_is_set(agg_info->aFunc->func, SQL_FUNC_COUNT) ||
 	    (agg_info->aFunc->pExpr->x.pList != NULL &&
 	     agg_info->aFunc->pExpr->x.pList->nExpr > 0))
 		return NULL;
@@ -5278,7 +5280,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);
 	}
 }
 
@@ -5319,7 +5321,7 @@ 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 (sql_func_flag_is_set(pF->func, SQL_FUNC_NEEDCOLL)) {
 			struct coll *coll = NULL;
 			struct ExprList_item *pItem;
 			int j;
@@ -5338,7 +5340,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/vdbe.c b/src/box/sql/vdbe.c
index a2f78607a..c5d91ceee 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -41,6 +41,8 @@
  */
 #include "box/box.h"
 #include "box/error.h"
+#include "box/func.h"
+#include "box/func_def.h"
 #include "box/fk_constraint.h"
 #include "box/txn.h"
 #include "box/tuple.h"
@@ -1720,7 +1722,7 @@ case OP_BuiltinFunction0: {
 	int n;
 	sql_context *pCtx;
 
-	assert(pOp->p4type==P4_FUNCDEF);
+	assert(pOp->p4type == P4_FUNC);
 	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));
@@ -1728,7 +1730,7 @@ case OP_BuiltinFunction0: {
 	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx) + (n-1)*sizeof(sql_value*));
 	if (pCtx==0) goto no_mem;
 	pCtx->pOut = 0;
-	pCtx->pFunc = pOp->p4.pFunc;
+	pCtx->func = pOp->p4.func;
 	pCtx->iOp = (int)(pOp - aOp);
 	pCtx->pVdbe = p;
 	pCtx->argc = n;
@@ -1763,7 +1765,9 @@ case OP_BuiltinFunction: {
 	}
 #endif
 	pCtx->is_aborted = false;
-	(*pCtx->pFunc->xSFunc)(pCtx, pCtx->argc, pCtx->argv);/* IMP: R-24505-23230 */
+	assert(pCtx->func->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	struct func_sql_builtin *func = (struct func_sql_builtin *)pCtx->func;
+	func->call(pCtx, pCtx->argc, pCtx->argv);
 
 	/* If the function returned an error, throw an exception */
 	if (pCtx->is_aborted)
@@ -5021,7 +5025,7 @@ case OP_AggStep0: {
 	int n;
 	sql_context *pCtx;
 
-	assert(pOp->p4type==P4_FUNCDEF);
+	assert(pOp->p4type == P4_FUNC);
 	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));
@@ -5029,7 +5033,7 @@ case OP_AggStep0: {
 	pCtx = sqlDbMallocRawNN(db, sizeof(*pCtx) + (n-1)*sizeof(sql_value*));
 	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;
 	pCtx->argc = n;
@@ -5071,7 +5075,9 @@ case OP_AggStep: {
 	pCtx->pOut = &t;
 	pCtx->is_aborted = false;
 	pCtx->skipFlag = 0;
-	(pCtx->pFunc->xSFunc)(pCtx,pCtx->argc,pCtx->argv); /* IMP: R-24505-23230 */
+	assert(pCtx->func->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	struct func_sql_builtin *func = (struct func_sql_builtin *)pCtx->func;
+	func->call(pCtx, pCtx->argc, pCtx->argv);
 	if (pCtx->is_aborted) {
 		sqlVdbeMemRelease(&t);
 		goto abort_due_to_error;
@@ -5103,7 +5109,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_finalize(pMem, pOp->p4.func) != 0)
 		goto abort_due_to_error;
 	UPDATE_MAX_BLOBSIZE(pMem);
 	if (sqlVdbeMemTooBig(pMem)) {
diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
index dfaff9365..bf40b44fa 100644
--- a/src/box/sql/vdbeapi.c
+++ b/src/box/sql/vdbeapi.c
@@ -477,17 +477,6 @@ sql_step(sql_stmt * pStmt)
 	return sqlStep(v);
 }
 
-/*
- * Extract the user data from a sql_context structure and return a
- * pointer to it.
- */
-void *
-sql_user_data(sql_context * p)
-{
-	assert(p && p->pFunc);
-	return p->pFunc->pUserData;
-}
-
 /*
  * Extract the user data from a sql_context structure and return a
  * pointer to it.
@@ -547,7 +536,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);
 		}
@@ -563,7 +552,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);
+	assert(p->func->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	assert(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 5bcf2ccd8..3bd4ff433 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -648,24 +648,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);
 }
 
@@ -689,13 +676,11 @@ 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;
+	default:
+		break;
 	}
 }
 
@@ -1149,15 +1134,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.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 5516d7fb1..ac0dfa333 100644
--- a/src/box/sql/vdbemem.c
+++ b/src/box/sql/vdbemem.c
@@ -312,37 +312,29 @@ sqlVdbeMemStringify(Mem * pMem)
 	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_finalize(struct Mem *mem, struct func *func)
 {
-	if (ALWAYS(pFunc && pFunc->xFinalize)) {
-		sql_context ctx;
-		Mem t;
-		assert((pMem->flags & MEM_Null) != 0 || pFunc == pMem->u.pDef);
-		memset(&ctx, 0, sizeof(ctx));
-		memset(&t, 0, sizeof(t));
-		t.flags = MEM_Null;
-		t.db = pMem->db;
-		t.field_type = field_type_MAX;
-		ctx.pOut = &t;
-		ctx.pMem = pMem;
-		ctx.pFunc = pFunc;
-		pFunc->xFinalize(&ctx);	/* IMP: R-24505-23230 */
-		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;
-	}
-	return 0;
+	assert(func != NULL);
+	assert(func->def->language == FUNC_LANGUAGE_SQL_BUILTIN);
+	assert(func->def->aggregate == FUNC_AGGREGATE_GROUP);
+	assert((mem->flags & MEM_Null) != 0 || func == mem->u.func);
+	sql_context ctx;
+	memset(&ctx, 0, sizeof(ctx));
+	Mem t;
+	memset(&t, 0, sizeof(t));
+	t.flags = MEM_Null;
+	t.db = mem->db;
+	t.field_type = field_type_MAX;
+	ctx.pOut = &t;
+	ctx.pMem = mem;
+	ctx.func = func;
+	((struct func_sql_builtin *)func)->finalize(&ctx);
+	assert((mem->flags & MEM_Dyn) == 0);
+	if (mem->szMalloc > 0)
+		sqlDbFree(mem->db, mem->zMalloc);
+	memcpy(mem, &t, sizeof(t));
+	return ctx.is_aborted ? -1 : 0;
 }
 
 /*
@@ -359,7 +351,7 @@ vdbeMemClearExternAndSetNull(Mem * p)
 {
 	assert(VdbeMemDynamic(p));
 	if (p->flags & MEM_Agg) {
-		sqlVdbeMemFinalize(p, p->u.pDef);
+		sql_vdbemem_finalize(p, p->u.func);
 		assert((p->flags & MEM_Agg) == 0);
 		testcase(p->flags & MEM_Dyn);
 	}
@@ -1289,7 +1281,6 @@ valueFromFunction(sql * db,	/* The database connection */
 	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 */
 	sql_value *pVal = 0;	/* New value */
 	int rc = 0;	/* Return code */
 	ExprList *pList = 0;	/* Function arguments */
@@ -1300,10 +1291,10 @@ 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) == 0 ||
-	    (pFunc->funcFlags & SQL_FUNC_NEEDCOLL))
+	struct func *func = sql_func_by_signature(p->u.zToken, nVal);
+	if (func == NULL || func->def->language != FUNC_LANGUAGE_SQL_BUILTIN ||
+	    !func->def->is_deterministic ||
+	    sql_func_flag_is_set(func, SQL_FUNC_NEEDCOLL))
 		return 0;
 
 	if (pList) {
@@ -1332,8 +1323,8 @@ 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);
+	ctx.func = func;
+	((struct func_sql_builtin *)func)->call(&ctx, nVal, apVal);
 	assert(!ctx.is_aborted);
 	sql_value_apply_type(pVal, type);
 	assert(rc == 0);
diff --git a/src/box/sql/whereexpr.c b/src/box/sql/whereexpr.c
index 8adf6a5f1..98615b118 100644
--- a/src/box/sql/whereexpr.c
+++ b/src/box/sql/whereexpr.c
@@ -273,7 +273,7 @@ like_optimization_is_valid(Parse *pParse, Expr *pExpr, Expr **ppPrefix,
 	/* Result code to return. */
 	int rc;
 
-	if (!sql_is_like_func(db, pExpr)) {
+	if (!sql_is_like_func(pExpr)) {
 		return 0;
 	}
 	pList = pExpr->x.pList;
diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 9bba37bcb..9d2fcea4b 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -130,7 +130,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 4f2e34bf0..c0780d6da 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -2926,6 +2926,12 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
 				  (unsigned) old_func->def->uid,
 				  "function has references");
 		}
+		/* Can't' drop a builtin function. */
+		if (old_func->def->language == FUNC_LANGUAGE_SQL_BUILTIN) {
+			tnt_raise(ClientError, ER_DROP_FUNCTION,
+				  (unsigned) old_func->def->uid,
+				  "function is SQL built-in");
+		}
 		struct trigger *on_commit =
 			txn_alter_trigger_new(on_drop_func_commit, old_func);
 		struct trigger *on_rollback =
diff --git a/test/box/function1.result b/test/box/function1.result
index 5b091f72b..0ece5448a 100644
--- a/test/box/function1.result
+++ b/test/box/function1.result
@@ -825,6 +825,26 @@ box.func.LUA:call({"return 1 + 1"})
 ---
 - 2
 ...
+box.schema.user.grant('guest', 'execute', 'function', 'SUM')
+---
+...
+c = net.connect(box.cfg.listen)
+---
+...
+c:call("SUM")
+---
+- error: sql builtin function does not support Lua frontend
+...
+c:close()
+---
+...
+box.schema.user.revoke('guest', 'execute', 'function', 'SUM')
+---
+...
+box.schema.func.drop("SUM")
+---
+- error: 'Can''t drop function 1: function is SQL built-in'
+...
 -- Introduce function options
 box.schema.func.create('test', {body = "function(tuple) return tuple end", is_deterministic = true, opts = {is_multikey = true}})
 ---
diff --git a/test/box/function1.test.lua b/test/box/function1.test.lua
index f894472f8..665972eda 100644
--- a/test/box/function1.test.lua
+++ b/test/box/function1.test.lua
@@ -294,6 +294,13 @@ ok == true
 
 box.func.LUA:call({"return 1 + 1"})
 
+box.schema.user.grant('guest', 'execute', 'function', 'SUM')
+c = net.connect(box.cfg.listen)
+c:call("SUM")
+c:close()
+box.schema.user.revoke('guest', 'execute', 'function', 'SUM')
+box.schema.func.drop("SUM")
+
 -- Introduce function options
 box.schema.func.create('test', {body = "function(tuple) return tuple end", is_deterministic = true, opts = {is_multikey = true}})
 box.func['test'].is_multikey == true
diff --git a/test/box/misc.result b/test/box/misc.result
index 7a15dabf0..feb72f69c 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -529,6 +529,7 @@ t;
   198: box.error.FUNC_INDEX_FUNC
   199: box.error.FUNC_INDEX_FORMAT
   200: box.error.FUNC_INDEX_PARTS
+  201: box.error.FUNC_WRONG_ARG_COUNT
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/sql-tap/func.test.lua b/test/sql-tap/func.test.lua
index ec06c903d..b70fac373 100755
--- a/test/sql-tap/func.test.lua
+++ b/test/sql-tap/func.test.lua
@@ -69,7 +69,7 @@ test:do_catchsql_test(
         SELECT length(*) FROM tbl1 ORDER BY t1
     ]], {
         -- <func-1.1>
-        1, "wrong number of arguments to function LENGTH()"
+        1, "Wrong number of arguments is passed to LENGTH(): expected 1, got 0"
         -- </func-1.1>
     })
 
@@ -79,7 +79,7 @@ test:do_catchsql_test(
         SELECT length(t1,5) FROM tbl1 ORDER BY t1
     ]], {
         -- <func-1.2>
-        1, "wrong number of arguments to function LENGTH()"
+        1, "Wrong number of arguments is passed to LENGTH(): expected 1, got 2"
         -- </func-1.2>
     })
 
@@ -365,7 +365,7 @@ test:do_test(
         return test:catchsql("SELECT abs(a,b) FROM t1")
     end, {
         -- <func-4.1>
-        1, "wrong number of arguments to function ABS()"
+        1, "Wrong number of arguments is passed to ABS(): expected 1, got 2"
         -- </func-4.1>
     })
 
@@ -377,7 +377,7 @@ test:do_catchsql_test(
         SELECT abs() FROM t1
     ]], {
         -- <func-4.2>
-        1, "wrong number of arguments to function ABS()"
+        1, "Wrong number of arguments is passed to ABS(): expected 1, got 0"
         -- </func-4.2>
     })
 
@@ -428,7 +428,7 @@ test:do_catchsql_test(
         SELECT round(a,b,c) FROM t1
     ]], {
         -- <func-4.5>
-        1, "wrong number of arguments to function ROUND()"
+        1, "Wrong number of arguments is passed to ROUND(): expected 1 or 2, got 3"
         -- </func-4.5>
     })
 
@@ -488,7 +488,7 @@ test:do_catchsql_test(
         SELECT round() FROM t1 ORDER BY a
     ]], {
         -- <func-4.11>
-        1, "wrong number of arguments to function ROUND()"
+        1, "Wrong number of arguments is passed to ROUND(): expected 1 or 2, got 0"
         -- </func-4.11>
     })
 
@@ -778,7 +778,7 @@ test:do_catchsql_test(
         SELECT upper(*) FROM t2
     ]], {
         -- <func-5.5>
-        1, "wrong number of arguments to function UPPER()"
+        1, "Wrong number of arguments is passed to UPPER(): expected 1, got 0"
         -- </func-5.5>
     })
 
@@ -1782,7 +1782,7 @@ test:do_catchsql_test(
         SELECT replace(1,2);
     ]], {
         -- <func-21.1>
-        1, "wrong number of arguments to function REPLACE()"
+        1, "Wrong number of arguments is passed to REPLACE(): expected 3, got 2"
         -- </func-21.1>
     })
 
@@ -1792,7 +1792,7 @@ test:do_catchsql_test(
         SELECT replace(1,2,3,4);
     ]], {
         -- <func-21.2>
-        1, "wrong number of arguments to function REPLACE()"
+        1, "Wrong number of arguments is passed to REPLACE(): expected 3, got 4"
         -- </func-21.2>
     })
 
@@ -2540,7 +2540,7 @@ test:do_catchsql_test(
         SELECT coalesce()
     ]], {
         -- <func-27.1>
-        1, "wrong number of arguments to function COALESCE()"
+        1, "Wrong number of arguments is passed to COALESCE(): expected >= 2, got 0"
         -- </func-27.1>
     })
 
@@ -2550,7 +2550,7 @@ test:do_catchsql_test(
         SELECT coalesce(1)
     ]], {
         -- <func-27.2>
-        1, "wrong number of arguments to function COALESCE()"
+        1, "Wrong number of arguments is passed to COALESCE(): expected >= 2, got 1"
         -- </func-27.2>
     })
 
diff --git a/test/sql-tap/func2.test.lua b/test/sql-tap/func2.test.lua
index b70197d09..c9bd3665f 100755
--- a/test/sql-tap/func2.test.lua
+++ b/test/sql-tap/func2.test.lua
@@ -50,7 +50,7 @@ test:do_catchsql_test(
         SELECT SUBSTR()
     ]], {
         -- <func2-1.2.1>
-        1, "wrong number of arguments to function SUBSTR()"
+        1, "Wrong number of arguments is passed to SUBSTR(): expected 1 or 2, got 0"
         -- </func2-1.2.1>
     })
 
@@ -60,7 +60,7 @@ test:do_catchsql_test(
         SELECT SUBSTR('Supercalifragilisticexpialidocious')
     ]], {
         -- <func2-1.2.2>
-        1, "wrong number of arguments to function SUBSTR()"
+        1, "Wrong number of arguments is passed to SUBSTR(): expected 1 or 2, got 1"
         -- </func2-1.2.2>
     })
 
@@ -70,7 +70,7 @@ test:do_catchsql_test(
         SELECT SUBSTR('Supercalifragilisticexpialidocious', 1,1,1)
     ]], {
         -- <func2-1.2.3>
-        1, "wrong number of arguments to function SUBSTR()"
+        1, "Wrong number of arguments is passed to SUBSTR(): expected 1 or 2, got 4"
         -- </func2-1.2.3>
     })
 
@@ -673,7 +673,7 @@ if ("ሴ" ~= "u1234")
             SELECT SUBSTR()
         ]], {
             -- <func2-2.1.2>
-            1, "wrong number of arguments to function SUBSTR()"
+            1, "Wrong number of arguments is passed to SUBSTR(): expected 1 or 2, got 0"
             -- </func2-2.1.2>
         })
 
@@ -683,7 +683,7 @@ if ("ሴ" ~= "u1234")
             SELECT SUBSTR('hiሴho')
         ]], {
             -- <func2-2.1.3>
-            1, "wrong number of arguments to function SUBSTR()"
+            1, "Wrong number of arguments is passed to SUBSTR(): expected 1 or 2, got 1"
             -- </func2-2.1.3>
         })
 
@@ -693,7 +693,7 @@ if ("ሴ" ~= "u1234")
             SELECT SUBSTR('hiሴho', 1,1,1)
         ]], {
             -- <func2-2.1.4>
-            1, "wrong number of arguments to function SUBSTR()"
+            1, "Wrong number of arguments is passed to SUBSTR(): expected 1 or 2, got 4"
             -- </func2-2.1.4>
         })
 
@@ -1038,7 +1038,7 @@ test:do_catchsql_test(
         SELECT SUBSTR()
     ]], {
         -- <func2-3.1.2>
-        1, "wrong number of arguments to function SUBSTR()"
+        1, "Wrong number of arguments is passed to SUBSTR(): expected 1 or 2, got 0"
         -- </func2-3.1.2>
     })
 
@@ -1048,7 +1048,7 @@ test:do_catchsql_test(
         SELECT SUBSTR(x'1234')
     ]], {
         -- <func2-3.1.3>
-        1, "wrong number of arguments to function SUBSTR()"
+        1, "Wrong number of arguments is passed to SUBSTR(): expected 1 or 2, got 1"
         -- </func2-3.1.3>
     })
 
@@ -1058,7 +1058,7 @@ test:do_catchsql_test(
         SELECT SUBSTR(x'1234', 1,1,1)
     ]], {
         -- <func2-3.1.4>
-        1, "wrong number of arguments to function SUBSTR()"
+        1, "Wrong number of arguments is passed to SUBSTR(): expected 1 or 2, got 4"
         -- </func2-3.1.4>
     })
 
diff --git a/test/sql-tap/limit.test.lua b/test/sql-tap/limit.test.lua
index 8445ab18e..870233942 100755
--- a/test/sql-tap/limit.test.lua
+++ b/test/sql-tap/limit.test.lua
@@ -771,7 +771,7 @@ test:do_catchsql_test(
         SELECT * FROM t1 LIMIT replace(1)
     ]], {
         -- <limit-12.1>
-        1, "wrong number of arguments to function REPLACE()"
+        1, "Wrong number of arguments is passed to REPLACE(): expected 3, got 1"
         -- </limit-12.1>
     })
 
@@ -781,7 +781,7 @@ test:do_catchsql_test(
         SELECT * FROM t1 LIMIT 5 OFFSET replace(1)
     ]], {
         -- <limit-12.2>
-        1, 'wrong number of arguments to function REPLACE()'
+        1, 'Wrong number of arguments is passed to REPLACE(): expected 3, got 1'
         -- </limit-12.2>
     })
 
diff --git a/test/sql-tap/select1.test.lua b/test/sql-tap/select1.test.lua
index 924c0ccb1..4bbfbd67b 100755
--- a/test/sql-tap/select1.test.lua
+++ b/test/sql-tap/select1.test.lua
@@ -244,7 +244,7 @@ test:do_catchsql_test(
         SELECT count(f1,f2) FROM test1
     ]], {
         -- <select1-2.1>
-        1, "wrong number of arguments to function COUNT()"
+        1, "Wrong number of arguments is passed to COUNT(): expected 0 or 1, got 2"
         -- </select1-2.1>
     })
 
@@ -324,7 +324,7 @@ test:do_catchsql_test(
         SELECT min(*) FROM test1
     ]], {
         -- <select1-2.6>
-        1, "wrong number of arguments to function MIN()"
+        1, "Wrong number of arguments is passed to MIN(): expected 1, got 0"
         -- </select1-2.6>
     })
 
@@ -389,7 +389,7 @@ test:do_catchsql_test(
         SELECT MAX(*) FROM test1
     ]], {
         -- <select1-2.9>
-        1, "wrong number of arguments to function MAX()"
+        1, "Wrong number of arguments is passed to MAX(): expected 1, got 0"
         -- </select1-2.9>
     })
 
@@ -469,7 +469,7 @@ test:do_catchsql_test(
         SELECT SUM(*) FROM test1
     ]], {
         -- <select1-2.14>
-        1, "wrong number of arguments to function SUM()"
+        1, "Wrong number of arguments is passed to SUM(): expected 1, got 0"
         -- </select1-2.14>
     })
 
@@ -489,7 +489,7 @@ test:do_catchsql_test(
         SELECT sum(f1,f2) FROM test1
     ]], {
         -- <select1-2.16>
-        1, "wrong number of arguments to function SUM()"
+        1, "Wrong number of arguments is passed to SUM(): expected 1, got 2"
         -- </select1-2.16>
     })
 
@@ -691,7 +691,7 @@ test:do_catchsql_test(
         SELECT f1 FROM test1 WHERE count(f1,f2)!=11
     ]], {
         -- <select1-3.9>
-        1, "wrong number of arguments to function COUNT()"
+        1, "misuse of aggregate function COUNT()"
         -- </select1-3.9>
     })
 
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/icu-upper-lower.result b/test/sql/icu-upper-lower.result
index 88266c8c5..8ff7528f3 100644
--- a/test/sql/icu-upper-lower.result
+++ b/test/sql/icu-upper-lower.result
@@ -277,7 +277,7 @@ test_run:cmd("setopt delimiter ''");
 box.execute("select upper('1', 2)")
 ---
 - null
-- wrong number of arguments to function UPPER()
+- 'Wrong number of arguments is passed to UPPER(): expected 1, got 2'
 ...
 box.execute("select upper(\"1\")")
 ---
@@ -287,5 +287,5 @@ box.execute("select upper(\"1\")")
 box.execute("select upper()")
 ---
 - null
-- wrong number of arguments to function UPPER()
+- 'Wrong number of arguments is passed to UPPER(): expected 1, got 0'
 ...
-- 
2.22.1

  parent reply	other threads:[~2019-08-21 15:28 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-08-21 15:28 [tarantool-patches] [PATCH v4 0/4] sql: uniform SQL and Lua functions subsystem Kirill Shcherbatov
2019-08-21 15:28 ` [tarantool-patches] [PATCH v4 1/4] sql: rename sql_vdbe_mem_alloc_region helper Kirill Shcherbatov
2019-08-22 13:04   ` [tarantool-patches] " n.pettik
2019-08-23 15:02     ` Kirill Shcherbatov
2019-08-21 15:28 ` [tarantool-patches] [PATCH v4 2/4] sql: replace flag MINMAX with flags MIN and MAX Kirill Shcherbatov
2019-08-22 13:30   ` [tarantool-patches] " n.pettik
2019-08-21 15:28 ` Kirill Shcherbatov [this message]
2019-08-22 14:37   ` [tarantool-patches] Re: [PATCH v4 3/4] sql: get rid of FuncDef function hash n.pettik
2019-08-23 15:02     ` [tarantool-patches] [PATCH v4 4/5] " Kirill Shcherbatov
2019-08-23 15:02     ` [tarantool-patches] [PATCH v4 3/5] sql: remove name overloading for SQL builtins Kirill Shcherbatov
2019-08-28 15:05       ` [tarantool-patches] " Nikita Pettik
2019-08-23 15:02     ` [tarantool-patches] Re: [PATCH v4 3/4] sql: get rid of FuncDef function hash Kirill Shcherbatov
2019-08-21 15:28 ` [tarantool-patches] [PATCH v4 4/4] sql: support user-defined functions in SQL Kirill Shcherbatov
2019-08-22 15:23   ` [tarantool-patches] " n.pettik
2019-08-23 15:02     ` Kirill Shcherbatov
2019-08-29 15:09 ` [tarantool-patches] Re: [PATCH v4 0/4] sql: uniform SQL and Lua functions subsystem Nikita Pettik
2019-08-29 17:12 ` Kirill Yukhin

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=154ee8ea11ae260254b71d32e1299f1272438af1.1566400979.git.kshcherbatov@tarantool.org \
    --to=kshcherbatov@tarantool.org \
    --cc=korablev@tarantool.org \
    --cc=tarantool-patches@freelists.org \
    --subject='Re: [tarantool-patches] [PATCH v4 3/4] sql: get rid of FuncDef function hash' \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

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