[Tarantool-patches] [PATCH v3 18/20] box: introduce prepared statements
Sergey Ostanevich
sergos at tarantool.org
Wed Dec 25 18:23:54 MSK 2019
Hi!
Thanks for the patch, LGTM with just 2 nits below.
Sergos
On 20 Dec 15:47, Nikita Pettik wrote:
> This patch introduces local prepared statements. Support of prepared
> statements in IProto protocol and netbox is added in the next patch.
>
> Prepared statement is an opaque instance of SQL Virtual Machine. It can
> be executed several times without necessity of query recompilation. To
> achieve this one can use box.prepare(...) function. It takes string of
> SQL query to be prepared; returns extended set of meta-information
> including statement's ID, parameter's types and names, types and names
> of columns of the resulting set, count of parameters to be bound. Lua
> object representing result of :prepare() invocation also features two
> methods - :execute() and :unprepare(). They correspond to
> box.execute(stmt.stmt_id) and box.unprepare(stmt.stmt_id), i.e.
> automatically substitute string of prepared statement to be executed.
> Statements are held in prepared statement cache - for details see
> previous commit. After schema changes all prepared statement located in
> cache are considered to be expired - they must be re-prepared by
> separate :prepare() call (or be invalidated with :unrepare()).
>
> Two sessions can share one prepared statements. But in current
> implementation if statement is executed by one session, another one is
> not able to use it and will compile it from scratch and than execute.
It would be nice to mention plans on what should/will be done for
resolution of this. Also, my previous question on DDL during the
execution of the statement is valid - one session can ruin execution
of a prepared statement in another session with a DDL. Should there
be some guard for DDL until all executions of prep statements are finished?
>
> SQL cache memory limit is regulated by box{sql_cache_size} which can be
> set dynamically. However, it can be set to the value which is less than
> the size of current free space in cache (since otherwise some statements
> can disappear from cache).
>
> Part of #2592
> ---
> src/box/errcode.h | 1 +
> src/box/execute.c | 114 ++++++++
> src/box/execute.h | 16 +-
> src/box/lua/execute.c | 213 +++++++++++++-
> src/box/lua/execute.h | 2 +-
> src/box/lua/init.c | 2 +-
> src/box/sql/prepare.c | 9 -
> test/box/misc.result | 3 +
> test/sql/engine.cfg | 3 +
> test/sql/prepared.result | 687 +++++++++++++++++++++++++++++++++++++++++++++
> test/sql/prepared.test.lua | 240 ++++++++++++++++
> 11 files changed, 1267 insertions(+), 23 deletions(-)
> create mode 100644 test/sql/prepared.result
> create mode 100644 test/sql/prepared.test.lua
>
> diff --git a/src/box/errcode.h b/src/box/errcode.h
> index ee44f61b3..9e12f3a31 100644
> --- a/src/box/errcode.h
> +++ b/src/box/errcode.h
> @@ -259,6 +259,7 @@ struct errcode_record {
> /*204 */_(ER_SQL_FUNC_WRONG_RET_COUNT, "SQL expects exactly one argument returned from %s, got %d")\
> /*205 */_(ER_FUNC_INVALID_RETURN_TYPE, "Function '%s' returned value of invalid type: expected %s got %s") \
> /*206 */_(ER_SQL_PREPARE, "Failed to prepare SQL statement: %s") \
> + /*207 */_(ER_WRONG_QUERY_ID, "Prepared statement with id %u does not exist") \
>
> /*
> * !IMPORTANT! Please follow instructions at start of the file
> diff --git a/src/box/execute.c b/src/box/execute.c
> index 3bc4988b7..09224c23a 100644
> --- a/src/box/execute.c
> +++ b/src/box/execute.c
> @@ -30,6 +30,7 @@
> */
> #include "execute.h"
>
> +#include "assoc.h"
> #include "bind.h"
> #include "iproto_constants.h"
> #include "sql/sqlInt.h"
> @@ -45,6 +46,8 @@
> #include "tuple.h"
> #include "sql/vdbe.h"
> #include "box/lua/execute.h"
> +#include "box/sql_stmt_cache.h"
> +#include "session.h"
>
> const char *sql_info_key_strs[] = {
> "row_count",
> @@ -413,6 +416,81 @@ port_sql_dump_msgpack(struct port *port, struct obuf *out)
> return 0;
> }
>
> +static bool
> +sql_stmt_check_schema_version(struct sql_stmt *stmt)
The naming could be better - since you state something as true or false,
it should be definitive, like sql_stmt_schema_version_is_valid()
> +{
> + return sql_stmt_schema_version(stmt) == box_schema_version();
> +}
> +
> +/**
> + * Re-compile statement and refresh global prepared statement
> + * cache with the newest value.
> + */
> +static int
> +sql_reprepare(struct sql_stmt **stmt)
> +{
> + const char *sql_str = sql_stmt_query_str(*stmt);
> + struct sql_stmt *new_stmt;
> + if (sql_stmt_compile(sql_str, strlen(sql_str), NULL,
> + &new_stmt, NULL) != 0)
> + return -1;
> + if (sql_stmt_cache_update(*stmt, new_stmt) != 0)
> + return -1;
> + *stmt = new_stmt;
> + return 0;
> +}
> +
> +/**
> + * Compile statement and save it to the global holder;
> + * update session hash with prepared statement ID (if
> + * it's not already there).
> + */
> +int
> +sql_prepare(const char *sql, int len, struct port *port)
> +{
> + uint32_t stmt_id = sql_stmt_calculate_id(sql, len);
> + struct sql_stmt *stmt = sql_stmt_cache_find(stmt_id);
> + if (stmt == NULL) {
> + if (sql_stmt_compile(sql, len, NULL, &stmt, NULL) != 0)
> + return -1;
> + if (sql_stmt_cache_insert(stmt) != 0) {
> + sql_stmt_finalize(stmt);
> + return -1;
> + }
> + } else {
> + if (! sql_stmt_check_schema_version(stmt)) {
The unaries should not be space-delimited as per C style $3.1
https://www.tarantool.io/en/doc/2.2/dev_guide/c_style_guide/
> + if (sql_reprepare(&stmt) != 0)
> + return -1;
> + }
> + }
> + assert(stmt != NULL);
> + /* Add id to the list of available statements in session. */
> + if (!session_check_stmt_id(current_session(), stmt_id))
> + session_add_stmt_id(current_session(), stmt_id);
> + enum sql_serialization_format format = sql_column_count(stmt) > 0 ?
> + DQL_PREPARE : DML_PREPARE;
> + port_sql_create(port, stmt, format, false);
> +
> + return 0;
> +}
> +
> +/**
> + * Deallocate prepared statement from current session:
> + * remove its ID from session-local hash and unref entry
> + * in global holder.
> + */
> +int
> +sql_unprepare(uint32_t stmt_id)
> +{
> + if (!session_check_stmt_id(current_session(), stmt_id)) {
> + diag_set(ClientError, ER_WRONG_QUERY_ID, stmt_id);
> + return -1;
> + }
> + session_remove_stmt_id(current_session(), stmt_id);
> + sql_stmt_unref(stmt_id);
> + return 0;
> +}
> +
> /**
> * Execute prepared SQL statement.
> *
> @@ -450,6 +528,42 @@ sql_execute(struct sql_stmt *stmt, struct port *port, struct region *region)
> return 0;
> }
>
> +int
> +sql_execute_prepared(uint32_t stmt_id, const struct sql_bind *bind,
> + uint32_t bind_count, struct port *port,
> + struct region *region)
> +{
> +
> + if (!session_check_stmt_id(current_session(), stmt_id)) {
> + diag_set(ClientError, ER_WRONG_QUERY_ID, stmt_id);
> + return -1;
> + }
> + struct sql_stmt *stmt = sql_stmt_cache_find(stmt_id);
> + assert(stmt != NULL);
> + if (! sql_stmt_check_schema_version(stmt)) {
> + diag_set(ClientError, ER_SQL_EXECUTE, "statement has expired");
> + return -1;
> + }
> + if (sql_stmt_busy(stmt)) {
> + const char *sql_str = sql_stmt_query_str(stmt);
> + return sql_prepare_and_execute(sql_str, strlen(sql_str), bind,
> + bind_count, port, region);
> + }
> + if (sql_bind(stmt, bind, bind_count) != 0)
> + return -1;
> + enum sql_serialization_format format = sql_column_count(stmt) > 0 ?
> + DQL_EXECUTE : DML_EXECUTE;
> + port_sql_create(port, stmt, format, false);
> + if (sql_execute(stmt, port, region) != 0) {
> + port_destroy(port);
> + sql_stmt_reset(stmt);
> + return -1;
> + }
> + sql_stmt_reset(stmt);
> +
> + return 0;
> +}
> +
> int
> sql_prepare_and_execute(const char *sql, int len, const struct sql_bind *bind,
> uint32_t bind_count, struct port *port,
> diff --git a/src/box/execute.h b/src/box/execute.h
> index 61c8e0281..5e2327f4a 100644
> --- a/src/box/execute.h
> +++ b/src/box/execute.h
> @@ -62,6 +62,14 @@ extern const char *sql_info_key_strs[];
> struct region;
> struct sql_bind;
>
> +int
> +sql_unprepare(uint32_t stmt_id);
> +
> +int
> +sql_execute_prepared(uint32_t query_id, const struct sql_bind *bind,
> + uint32_t bind_count, struct port *port,
> + struct region *region);
> +
> /**
> * Prepare and execute an SQL statement.
> * @param sql SQL statement.
> @@ -135,13 +143,11 @@ sql_stmt_busy(const struct sql_stmt *stmt);
> * Prepare (compile into VDBE byte-code) statement.
> *
> * @param sql UTF-8 encoded SQL statement.
> - * @param length Length of @param sql in bytes.
> - * @param[out] stmt A pointer to the prepared statement.
> - * @param[out] sql_tail End of parsed string.
> + * @param len Length of @param sql in bytes.
> + * @param port Port to store request response.
> */
> int
> -sql_prepare(const char *sql, int length, struct sql_stmt **stmt,
> - const char **sql_tail);
> +sql_prepare(const char *sql, int len, struct port *port);
>
> #if defined(__cplusplus)
> } /* extern "C" { */
> diff --git a/src/box/lua/execute.c b/src/box/lua/execute.c
> index b164ffcaf..6cb1f5db9 100644
> --- a/src/box/lua/execute.c
> +++ b/src/box/lua/execute.c
> @@ -5,6 +5,8 @@
> #include "box/port.h"
> #include "box/execute.h"
> #include "box/bind.h"
> +#include "box/sql_stmt_cache.h"
> +#include "box/schema.h"
>
> /**
> * Serialize a description of the prepared statement.
> @@ -38,6 +40,101 @@ lua_sql_get_metadata(struct sql_stmt *stmt, struct lua_State *L,
> }
> }
>
> +static inline void
> +lua_sql_get_params_metadata(struct sql_stmt *stmt, struct lua_State *L)
> +{
> + int bind_count = sql_bind_parameter_count(stmt);
> + lua_createtable(L, bind_count, 0);
> + for (int i = 0; i < bind_count; ++i) {
> + lua_createtable(L, 0, 2);
> + const char *name = sql_bind_parameter_name(stmt, i);
> + if (name == NULL)
> + name = "?";
> + const char *type = "ANY";
> + lua_pushstring(L, name);
> + lua_setfield(L, -2, "name");
> + lua_pushstring(L, type);
> + lua_setfield(L, -2, "type");
> + lua_rawseti(L, -2, i + 1);
> + }
> +}
> +
> +/** Forward declaration to avoid code movement. */
> +static int
> +lbox_execute(struct lua_State *L);
> +
> +/**
> + * Prepare SQL statement: compile it and save to the cache.
> + * In fact it is wrapper around box.execute() which unfolds
> + * it to box.execute(stmt.query_id).
> + */
> +static int
> +lbox_execute_prepared(struct lua_State *L)
> +{
> + int top = lua_gettop(L);
> +
> + if ((top != 1 && top != 2) || ! lua_istable(L, 1))
> + return luaL_error(L, "Usage: statement:execute([, params])");
> + lua_getfield(L, 1, "stmt_id");
> + if (!lua_isnumber(L, -1))
> + return luaL_error(L, "Query id is expected to be numeric");
> + lua_remove(L, 1);
> + if (top == 2) {
> + /*
> + * Stack state (before remove operation):
> + * 1 Prepared statement object (Lua table)
> + * 2 Bindings (Lua table)
> + * 3 Statement ID(fetched from PS table) - top of stack
> + *
> + * We should make it suitable to pass arguments to
> + * lbox_execute(), i.e. after manipulations stack
> + * should look like:
> + * 1 Statement ID
> + * 2 Bindings - top of stack
> + * Since there's no swap operation, we firstly remove
> + * PS object, then copy table of values to be bound to
> + * the top of stack (push), and finally remove original
> + * bindings from stack.
> + */
> + lua_pushvalue(L, 1);
> + lua_remove(L, 1);
> + }
> + return lbox_execute(L);
> +}
> +
> +/**
> + * Unprepare statement: remove it from prepared statements cache.
> + * This function can be called in two ways: as member of prepared
> + * statement handle (stmt:unprepare()) or as box.unprepare(stmt_id).
> + */
> +static int
> +lbox_unprepare(struct lua_State *L)
> +{
> + int top = lua_gettop(L);
> +
> + if (top != 1 || (! lua_istable(L, 1) && ! lua_isnumber(L, 1))) {
> + return luaL_error(L, "Usage: statement:unprepare() or "\
> + "box.unprepare(stmt_id)");
> + }
> + lua_Integer stmt_id;
> + if (lua_istable(L, 1)) {
> + lua_getfield(L, -1, "stmt_id");
> + if (! lua_isnumber(L, -1)) {
> + return luaL_error(L, "Statement id is expected "\
> + "to be numeric");
> + }
> + stmt_id = lua_tointeger(L, -1);
> + lua_pop(L, 1);
> + } else {
> + stmt_id = lua_tonumber(L, 1);
> + }
> + if (stmt_id < 0)
> + return luaL_error(L, "Statement id can't be negative");
> + if (sql_unprepare((uint32_t) stmt_id) != 0)
> + return luaT_push_nil_and_error(L);
> + return 0;
> +}
> +
> void
> port_sql_dump_lua(struct port *port, struct lua_State *L, bool is_flat)
> {
> @@ -82,7 +179,66 @@ port_sql_dump_lua(struct port *port, struct lua_State *L, bool is_flat)
> }
> break;
> }
> - default: {
> + case DQL_PREPARE: {
> + /* Format is following:
> + * stmt_id,
> + * param_count,
> + * params {name, type},
> + * metadata {name, type}
> + * execute(), unprepare()
> + */
> + lua_createtable(L, 0, 6);
> + /* query_id */
> + const char *sql_str = sql_stmt_query_str(port_sql->stmt);
> + luaL_pushuint64(L, sql_stmt_calculate_id(sql_str,
> + strlen(sql_str)));
> + lua_setfield(L, -2, "stmt_id");
> + /* param_count */
> + luaL_pushuint64(L, sql_bind_parameter_count(stmt));
> + lua_setfield(L, -2, "param_count");
> + /* params map */
> + lua_sql_get_params_metadata(stmt, L);
> + lua_setfield(L, -2, "params");
> + /* metadata */
> + lua_sql_get_metadata(stmt, L, sql_column_count(stmt));
> + lua_setfield(L, -2, "metadata");
> + /* execute function */
> + lua_pushcfunction(L, lbox_execute_prepared);
> + lua_setfield(L, -2, "execute");
> + /* unprepare function */
> + lua_pushcfunction(L, lbox_unprepare);
> + lua_setfield(L, -2, "unprepare");
> + break;
> + }
> + case DML_PREPARE : {
> + assert(((struct port_tuple *) port)->size == 0);
> + /* Format is following:
> + * stmt_id,
> + * param_count,
> + * params {name, type},
> + * execute(), unprepare()
> + */
> + lua_createtable(L, 0, 5);
> + /* query_id */
> + const char *sql_str = sql_stmt_query_str(port_sql->stmt);
> + luaL_pushuint64(L, sql_stmt_calculate_id(sql_str,
> + strlen(sql_str)));
> + lua_setfield(L, -2, "stmt_id");
> + /* param_count */
> + luaL_pushuint64(L, sql_bind_parameter_count(stmt));
> + lua_setfield(L, -2, "param_count");
> + /* params map */
> + lua_sql_get_params_metadata(stmt, L);
> + lua_setfield(L, -2, "params");
> + /* execute function */
> + lua_pushcfunction(L, lbox_execute_prepared);
> + lua_setfield(L, -2, "execute");
> + /* unprepare function */
> + lua_pushcfunction(L, lbox_unprepare);
> + lua_setfield(L, -2, "unprepare");
> + break;
> + }
> + default:{
> unreachable();
> }
> }
> @@ -253,9 +409,8 @@ lbox_execute(struct lua_State *L)
> int top = lua_gettop(L);
>
> if ((top != 1 && top != 2) || ! lua_isstring(L, 1))
> - return luaL_error(L, "Usage: box.execute(sqlstring[, params])");
> -
> - const char *sql = lua_tolstring(L, 1, &length);
> + return luaL_error(L, "Usage: box.execute(sqlstring[, params]) "
> + "or box.execute(stmt_id[, params])");
>
> if (top == 2) {
> if (! lua_istable(L, 2))
> @@ -264,9 +419,44 @@ lbox_execute(struct lua_State *L)
> if (bind_count < 0)
> return luaT_push_nil_and_error(L);
> }
> + /*
> + * lua_isstring() returns true for numeric values as well,
> + * so test explicit type instead.
> + */
> + if (lua_type(L, 1) == LUA_TSTRING) {
> + const char *sql = lua_tolstring(L, 1, &length);
> + if (sql_prepare_and_execute(sql, length, bind, bind_count, &port,
> + &fiber()->gc) != 0)
> + return luaT_push_nil_and_error(L);
> + } else {
> + assert(lua_type(L, 1) == LUA_TNUMBER);
> + lua_Integer query_id = lua_tointeger(L, 1);
> + if (query_id < 0)
> + return luaL_error(L, "Statement id can't be negative");
> + if (sql_execute_prepared(query_id, bind, bind_count, &port,
> + &fiber()->gc) != 0)
> + return luaT_push_nil_and_error(L);
> + }
> + port_dump_lua(&port, L, false);
> + port_destroy(&port);
> + return 1;
> +}
>
> - if (sql_prepare_and_execute(sql, length, bind, bind_count, &port,
> - &fiber()->gc) != 0)
> +/**
> + * Prepare SQL statement: compile it and save to the cache.
> + */
> +static int
> +lbox_prepare(struct lua_State *L)
> +{
> + size_t length;
> + struct port port;
> + int top = lua_gettop(L);
> +
> + if ((top != 1 && top != 2) || ! lua_isstring(L, 1))
> + return luaL_error(L, "Usage: box.prepare(sqlstring)");
> +
> + const char *sql = lua_tolstring(L, 1, &length);
> + if (sql_prepare(sql, length, &port) != 0)
> return luaT_push_nil_and_error(L);
> port_dump_lua(&port, L, false);
> port_destroy(&port);
> @@ -274,11 +464,20 @@ lbox_execute(struct lua_State *L)
> }
>
> void
> -box_lua_execute_init(struct lua_State *L)
> +box_lua_sql_init(struct lua_State *L)
> {
> lua_getfield(L, LUA_GLOBALSINDEX, "box");
> lua_pushstring(L, "execute");
> lua_pushcfunction(L, lbox_execute);
> lua_settable(L, -3);
> +
> + lua_pushstring(L, "prepare");
> + lua_pushcfunction(L, lbox_prepare);
> + lua_settable(L, -3);
> +
> + lua_pushstring(L, "unprepare");
> + lua_pushcfunction(L, lbox_unprepare);
> + lua_settable(L, -3);
> +
> lua_pop(L, 1);
> }
> diff --git a/src/box/lua/execute.h b/src/box/lua/execute.h
> index 23e193fa4..bafd67615 100644
> --- a/src/box/lua/execute.h
> +++ b/src/box/lua/execute.h
> @@ -66,6 +66,6 @@ lua_sql_bind_list_decode(struct lua_State *L, struct sql_bind **out_bind,
> int idx);
>
> void
> -box_lua_execute_init(struct lua_State *L);
> +box_lua_sql_init(struct lua_State *L);
>
> #endif /* INCLUDES_TARANTOOL_LUA_EXECUTE_H */
> diff --git a/src/box/lua/init.c b/src/box/lua/init.c
> index 7ffed409d..7be520e09 100644
> --- a/src/box/lua/init.c
> +++ b/src/box/lua/init.c
> @@ -314,7 +314,7 @@ box_lua_init(struct lua_State *L)
> box_lua_ctl_init(L);
> box_lua_session_init(L);
> box_lua_xlog_init(L);
> - box_lua_execute_init(L);
> + box_lua_sql_init(L);
> luaopen_net_box(L);
> lua_pop(L, 1);
> tarantool_lua_console_init(L);
> diff --git a/src/box/sql/prepare.c b/src/box/sql/prepare.c
> index 73d6866a4..5b1c6c581 100644
> --- a/src/box/sql/prepare.c
> +++ b/src/box/sql/prepare.c
> @@ -193,15 +193,6 @@ sqlReprepare(Vdbe * p)
> return 0;
> }
>
> -int
> -sql_prepare(const char *sql, int length, struct sql_stmt **stmt,
> - const char **sql_tail)
> -{
> - int rc = sql_stmt_compile(sql, length, 0, stmt, sql_tail);
> - assert(rc == 0 || stmt == NULL || *stmt == NULL);
> - return rc;
> -}
> -
> void
> sql_parser_create(struct Parse *parser, struct sql *db, uint32_t sql_flags)
> {
> diff --git a/test/box/misc.result b/test/box/misc.result
> index 7e5d28b70..90923f28e 100644
> --- a/test/box/misc.result
> +++ b/test/box/misc.result
> @@ -73,6 +73,7 @@ t
> - on_commit
> - on_rollback
> - once
> + - prepare
> - priv
> - rollback
> - rollback_to_savepoint
> @@ -86,6 +87,7 @@ t
> - space
> - stat
> - tuple
> + - unprepare
> ...
> t = nil
> ---
> @@ -555,6 +557,7 @@ t;
> 204: box.error.SQL_FUNC_WRONG_RET_COUNT
> 205: box.error.FUNC_INVALID_RETURN_TYPE
> 206: box.error.SQL_PREPARE
> + 207: box.error.WRONG_QUERY_ID
> ...
> test_run:cmd("setopt delimiter ''");
> ---
> diff --git a/test/sql/engine.cfg b/test/sql/engine.cfg
> index 284c42082..a1b4b0fc5 100644
> --- a/test/sql/engine.cfg
> +++ b/test/sql/engine.cfg
> @@ -9,6 +9,9 @@
> "remote": {"remote": "true"},
> "local": {"remote": "false"}
> },
> + "prepared.test.lua": {
> + "local": {"remote": "false"}
> + },
> "*": {
> "memtx": {"engine": "memtx"},
> "vinyl": {"engine": "vinyl"}
> diff --git a/test/sql/prepared.result b/test/sql/prepared.result
> new file mode 100644
> index 000000000..bd37cfdd7
> --- /dev/null
> +++ b/test/sql/prepared.result
> @@ -0,0 +1,687 @@
> +-- test-run result file version 2
> +remote = require('net.box')
> + | ---
> + | ...
> +test_run = require('test_run').new()
> + | ---
> + | ...
> +fiber = require('fiber')
> + | ---
> + | ...
> +
> +-- Wrappers to make remote and local execution interface return
> +-- same result pattern.
> +--
> +test_run:cmd("setopt delimiter ';'")
> + | ---
> + | - true
> + | ...
> +execute = function(...)
> + local res, err = box.execute(...)
> + if err ~= nil then
> + error(err)
> + end
> + return res
> +end;
> + | ---
> + | ...
> +prepare = function(...)
> + local res, err = box.prepare(...)
> + if err ~= nil then
> + error(err)
> + end
> + return res
> +end;
> + | ---
> + | ...
> +unprepare = function(...)
> + local res, err = box.unprepare(...)
> + if err ~= nil then
> + error(err)
> + end
> + return res
> +end;
> + | ---
> + | ...
> +
> +test_run:cmd("setopt delimiter ''");
> + | ---
> + | - true
> + | ...
> +
> +-- Test local interface and basic capabilities of prepared statements.
> +--
> +execute('CREATE TABLE test (id INT PRIMARY KEY, a NUMBER, b TEXT)')
> + | ---
> + | - row_count: 1
> + | ...
> +space = box.space.TEST
> + | ---
> + | ...
> +space:replace{1, 2, '3'}
> + | ---
> + | - [1, 2, '3']
> + | ...
> +space:replace{4, 5, '6'}
> + | ---
> + | - [4, 5, '6']
> + | ...
> +space:replace{7, 8.5, '9'}
> + | ---
> + | - [7, 8.5, '9']
> + | ...
> +s, e = prepare("SELECT * FROM test WHERE id = ? AND a = ?;")
> + | ---
> + | ...
> +assert(e == nil)
> + | ---
> + | - true
> + | ...
> +assert(s ~= nil)
> + | ---
> + | - true
> + | ...
> +s.stmt_id
> + | ---
> + | - 3603193623
> + | ...
> +s.metadata
> + | ---
> + | - - name: ID
> + | type: integer
> + | - name: A
> + | type: number
> + | - name: B
> + | type: string
> + | ...
> +s.params
> + | ---
> + | - - name: '?'
> + | type: ANY
> + | - name: '?'
> + | type: ANY
> + | ...
> +s.param_count
> + | ---
> + | - 2
> + | ...
> +execute(s.stmt_id, {1, 2})
> + | ---
> + | - metadata:
> + | - name: ID
> + | type: integer
> + | - name: A
> + | type: number
> + | - name: B
> + | type: string
> + | rows:
> + | - [1, 2, '3']
> + | ...
> +execute(s.stmt_id, {1, 3})
> + | ---
> + | - metadata:
> + | - name: ID
> + | type: integer
> + | - name: A
> + | type: number
> + | - name: B
> + | type: string
> + | rows: []
> + | ...
> +s:execute({1, 2})
> + | ---
> + | - metadata:
> + | - name: ID
> + | type: integer
> + | - name: A
> + | type: number
> + | - name: B
> + | type: string
> + | rows:
> + | - [1, 2, '3']
> + | ...
> +s:execute({1, 3})
> + | ---
> + | - metadata:
> + | - name: ID
> + | type: integer
> + | - name: A
> + | type: number
> + | - name: B
> + | type: string
> + | rows: []
> + | ...
> +s:unprepare()
> + | ---
> + | ...
> +
> +-- Test preparation of different types of queries.
> +-- Let's start from DDL. It doesn't make much sense since
> +-- any prepared DDL statement can be executed once, but
> +-- anyway make sure that no crashes occur.
> +--
> +s = prepare("CREATE INDEX i1 ON test(a)")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - error: 'Failed to execute SQL statement: statement has expired'
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +s = prepare("DROP INDEX i1 ON test;")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - error: 'Failed to execute SQL statement: statement has expired'
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +s = prepare("CREATE VIEW v AS SELECT * FROM test;")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - error: 'Failed to execute SQL statement: statement has expired'
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +s = prepare("DROP VIEW v;")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - error: 'Failed to execute SQL statement: statement has expired'
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +s = prepare("ALTER TABLE test RENAME TO test1")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - row_count: 0
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - error: 'Failed to execute SQL statement: statement has expired'
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +box.execute("CREATE TABLE test2 (id INT PRIMARY KEY);")
> + | ---
> + | - row_count: 1
> + | ...
> +s = prepare("ALTER TABLE test2 ADD CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES test2")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - error: 'Failed to execute SQL statement: statement has expired'
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +box.space.TEST2:drop()
> + | ---
> + | ...
> +
> +s = prepare("CREATE TRIGGER tr1 INSERT ON test1 FOR EACH ROW BEGIN DELETE FROM test1; END;")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - error: 'Failed to execute SQL statement: statement has expired'
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +s = prepare("DROP TRIGGER tr1;")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - error: 'Failed to execute SQL statement: statement has expired'
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +s = prepare("DROP TABLE test1;")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - error: 'Failed to execute SQL statement: statement has expired'
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +-- DQL
> +--
> +execute('CREATE TABLE test (id INT PRIMARY KEY, a NUMBER, b TEXT)')
> + | ---
> + | - row_count: 1
> + | ...
> +space = box.space.TEST
> + | ---
> + | ...
> +space:replace{1, 2, '3'}
> + | ---
> + | - [1, 2, '3']
> + | ...
> +space:replace{4, 5, '6'}
> + | ---
> + | - [4, 5, '6']
> + | ...
> +space:replace{7, 8.5, '9'}
> + | ---
> + | - [7, 8.5, '9']
> + | ...
> +_ = prepare("SELECT a FROM test WHERE b = '3';")
> + | ---
> + | ...
> +s = prepare("SELECT a FROM test WHERE b = '3';")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - metadata:
> + | - name: A
> + | type: number
> + | rows:
> + | - [2]
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - metadata:
> + | - name: A
> + | type: number
> + | rows:
> + | - [2]
> + | ...
> +s:execute()
> + | ---
> + | - metadata:
> + | - name: A
> + | type: number
> + | rows:
> + | - [2]
> + | ...
> +s:execute()
> + | ---
> + | - metadata:
> + | - name: A
> + | type: number
> + | rows:
> + | - [2]
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +s = prepare("SELECT count(*), count(a - 3), max(b), abs(id) FROM test WHERE b = '3';")
> + | ---
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - metadata:
> + | - name: count(*)
> + | type: integer
> + | - name: count(a - 3)
> + | type: integer
> + | - name: max(b)
> + | type: scalar
> + | - name: abs(id)
> + | type: number
> + | rows:
> + | - [1, 1, '3', 1]
> + | ...
> +execute(s.stmt_id)
> + | ---
> + | - metadata:
> + | - name: count(*)
> + | type: integer
> + | - name: count(a - 3)
> + | type: integer
> + | - name: max(b)
> + | type: scalar
> + | - name: abs(id)
> + | type: number
> + | rows:
> + | - [1, 1, '3', 1]
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +-- Let's try something a bit more complicated. For instance recursive
> +-- query displaying Mandelbrot set.
> +--
> +s = prepare([[WITH RECURSIVE \
> + xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2), \
> + yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0), \
> + m(iter, cx, cy, x, y) AS ( \
> + SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis \
> + UNION ALL \
> + SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m \
> + WHERE (x*x + y*y) < 4.0 AND iter<28), \
> + m2(iter, cx, cy) AS ( \
> + SELECT max(iter), cx, cy FROM m GROUP BY cx, cy), \
> + a(t) AS ( \
> + SELECT group_concat( substr(' .+*#', 1+LEAST(iter/7,4), 1), '') \
> + FROM m2 GROUP BY cy) \
> + SELECT group_concat(TRIM(TRAILING FROM t),x'0a') FROM a;]])
> + | ---
> + | ...
> +
> +res = execute(s.stmt_id)
> + | ---
> + | ...
> +res.metadata
> + | ---
> + | - - name: group_concat(TRIM(TRAILING FROM t),x'0a')
> + | type: string
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +-- Workflow with bindings is still the same.
> +--
> +s = prepare("SELECT a FROM test WHERE b = ?;")
> + | ---
> + | ...
> +execute(s.stmt_id, {'6'})
> + | ---
> + | - metadata:
> + | - name: A
> + | type: number
> + | rows:
> + | - [5]
> + | ...
> +execute(s.stmt_id, {'9'})
> + | ---
> + | - metadata:
> + | - name: A
> + | type: number
> + | rows:
> + | - [8.5]
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +-- DML
> +s = prepare("INSERT INTO test VALUES (?, ?, ?);")
> + | ---
> + | ...
> +execute(s.stmt_id, {5, 6, '7'})
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id, {6, 10, '7'})
> + | ---
> + | - row_count: 1
> + | ...
> +execute(s.stmt_id, {9, 11, '7'})
> + | ---
> + | - row_count: 1
> + | ...
> +unprepare(s.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +-- EXPLAIN and PRAGMA work fine as well.
> +--
> +s1 = prepare("EXPLAIN SELECT a FROM test WHERE b = '3';")
> + | ---
> + | ...
> +res = execute(s1.stmt_id)
> + | ---
> + | ...
> +res.metadata
> + | ---
> + | - - name: addr
> + | type: integer
> + | - name: opcode
> + | type: text
> + | - name: p1
> + | type: integer
> + | - name: p2
> + | type: integer
> + | - name: p3
> + | type: integer
> + | - name: p4
> + | type: text
> + | - name: p5
> + | type: text
> + | - name: comment
> + | type: text
> + | ...
> +assert(res.rows ~= nil)
> + | ---
> + | - true
> + | ...
> +
> +s2 = prepare("EXPLAIN QUERY PLAN SELECT a FROM test WHERE b = '3';")
> + | ---
> + | ...
> +res = execute(s2.stmt_id)
> + | ---
> + | ...
> +res.metadata
> + | ---
> + | - - name: selectid
> + | type: integer
> + | - name: order
> + | type: integer
> + | - name: from
> + | type: integer
> + | - name: detail
> + | type: text
> + | ...
> +assert(res.rows ~= nil)
> + | ---
> + | - true
> + | ...
> +
> +s3 = prepare("PRAGMA count_changes;")
> + | ---
> + | ...
> +execute(s3.stmt_id)
> + | ---
> + | - metadata:
> + | - name: defer_foreign_keys
> + | type: integer
> + | rows:
> + | - [0]
> + | ...
> +
> +unprepare(s2.stmt_id)
> + | ---
> + | - null
> + | ...
> +unprepare(s3.stmt_id)
> + | ---
> + | - null
> + | ...
> +unprepare(s1.stmt_id)
> + | ---
> + | - null
> + | ...
> +
> +-- Setting cache size to 0 is possible only in case if
> +-- there's no any prepared statements right now .
> +--
> +box.cfg{sql_cache_size = 0}
> + | ---
> + | ...
> +prepare("SELECT a FROM test;")
> + | ---
> + | - error: 'Failed to prepare SQL statement: Memory limit for SQL prepared statements
> + | has been reached. Please, deallocate active statements or increase SQL cache size.'
> + | ...
> +box.cfg{sql_cache_size = 0}
> + | ---
> + | ...
> +
> +-- Still with small size everything should work.
> +--
> +box.cfg{sql_cache_size = 1500}
> + | ---
> + | ...
> +
> +test_run:cmd("setopt delimiter ';'");
> + | ---
> + | - true
> + | ...
> +ok = nil
> +res = nil
> +_ = fiber.create(function()
> + for i = 1, 5 do
> + pcall(prepare, string.format("SELECT * FROM test WHERE a = %d;", i))
> + end
> + ok, res = pcall(prepare, "SELECT * FROM test WHERE b = '3';")
> +end);
> + | ---
> + | ...
> +while ok == nil do fiber.sleep(0.00001) end;
> + | ---
> + | ...
> +assert(ok == false);
> + | ---
> + | - true
> + | ...
> +res;
> + | ---
> + | - 'Failed to prepare SQL statement: Memory limit for SQL prepared statements has been
> + | reached. Please, deallocate active statements or increase SQL cache size.'
> + | ...
> +
> +-- Check that after fiber is dead, its session gets rid of
> +-- all prepared statements.
> +--
> +box.cfg{sql_cache_size = 0};
> + | ---
> + | ...
> +box.cfg{sql_cache_size = 3000};
> + | ---
> + | ...
> +
> +-- Make sure that if prepared statement is busy (is executed
> +-- right now), prepared statement is not used, i.e. statement
> +-- is compiled from scratch, executed and finilized.
> +--
> +box.schema.func.create('SLEEP', {language = 'Lua',
> + body = 'function () fiber.sleep(0.1) return 1 end',
> + exports = {'LUA', 'SQL'}});
> + | ---
> + | ...
> +
> +s = prepare("SELECT id, SLEEP() FROM test;");
> + | ---
> + | ...
> +assert(s ~= nil);
> + | ---
> + | - true
> + | ...
> +
> +function implicit_yield()
> + prepare("SELECT id, SLEEP() FROM test;")
> + execute("SELECT id, SLEEP() FROM test;")
> +end;
> + | ---
> + | ...
> +
> +f1 = fiber.new(implicit_yield)
> +f2 = fiber.new(implicit_yield)
> +f1:set_joinable(true)
> +f2:set_joinable(true)
> +
> +f1:join();
> + | ---
> + | ...
> +f2:join();
> + | ---
> + | - true
> + | ...
> +
> +unprepare(s.stmt_id);
> + | ---
> + | - null
> + | ...
> +
> +test_run:cmd("setopt delimiter ''");
> + | ---
> + | - true
> + | ...
> +
> +box.cfg{sql_cache_size = 5 * 1024 * 1024}
> + | ---
> + | ...
> +box.space.TEST:drop()
> + | ---
> + | ...
> +box.schema.func.drop('SLEEP')
> + | ---
> + | ...
> diff --git a/test/sql/prepared.test.lua b/test/sql/prepared.test.lua
> new file mode 100644
> index 000000000..49d2fb3ae
> --- /dev/null
> +++ b/test/sql/prepared.test.lua
> @@ -0,0 +1,240 @@
> +remote = require('net.box')
> +test_run = require('test_run').new()
> +fiber = require('fiber')
> +
> +-- Wrappers to make remote and local execution interface return
> +-- same result pattern.
> +--
> +test_run:cmd("setopt delimiter ';'")
> +execute = function(...)
> + local res, err = box.execute(...)
> + if err ~= nil then
> + error(err)
> + end
> + return res
> +end;
> +prepare = function(...)
> + local res, err = box.prepare(...)
> + if err ~= nil then
> + error(err)
> + end
> + return res
> +end;
> +unprepare = function(...)
> + local res, err = box.unprepare(...)
> + if err ~= nil then
> + error(err)
> + end
> + return res
> +end;
> +
> +test_run:cmd("setopt delimiter ''");
> +
> +-- Test local interface and basic capabilities of prepared statements.
> +--
> +execute('CREATE TABLE test (id INT PRIMARY KEY, a NUMBER, b TEXT)')
> +space = box.space.TEST
> +space:replace{1, 2, '3'}
> +space:replace{4, 5, '6'}
> +space:replace{7, 8.5, '9'}
> +s, e = prepare("SELECT * FROM test WHERE id = ? AND a = ?;")
> +assert(e == nil)
> +assert(s ~= nil)
> +s.stmt_id
> +s.metadata
> +s.params
> +s.param_count
> +execute(s.stmt_id, {1, 2})
> +execute(s.stmt_id, {1, 3})
> +s:execute({1, 2})
> +s:execute({1, 3})
> +s:unprepare()
> +
> +-- Test preparation of different types of queries.
> +-- Let's start from DDL. It doesn't make much sense since
> +-- any prepared DDL statement can be executed once, but
> +-- anyway make sure that no crashes occur.
> +--
> +s = prepare("CREATE INDEX i1 ON test(a)")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +
> +s = prepare("DROP INDEX i1 ON test;")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +
> +s = prepare("CREATE VIEW v AS SELECT * FROM test;")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +
> +s = prepare("DROP VIEW v;")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +
> +s = prepare("ALTER TABLE test RENAME TO test1")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +
> +box.execute("CREATE TABLE test2 (id INT PRIMARY KEY);")
> +s = prepare("ALTER TABLE test2 ADD CONSTRAINT fk1 FOREIGN KEY (id) REFERENCES test2")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +box.space.TEST2:drop()
> +
> +s = prepare("CREATE TRIGGER tr1 INSERT ON test1 FOR EACH ROW BEGIN DELETE FROM test1; END;")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +
> +s = prepare("DROP TRIGGER tr1;")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +
> +s = prepare("DROP TABLE test1;")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +
> +-- DQL
> +--
> +execute('CREATE TABLE test (id INT PRIMARY KEY, a NUMBER, b TEXT)')
> +space = box.space.TEST
> +space:replace{1, 2, '3'}
> +space:replace{4, 5, '6'}
> +space:replace{7, 8.5, '9'}
> +_ = prepare("SELECT a FROM test WHERE b = '3';")
> +s = prepare("SELECT a FROM test WHERE b = '3';")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +s:execute()
> +s:execute()
> +unprepare(s.stmt_id)
> +
> +s = prepare("SELECT count(*), count(a - 3), max(b), abs(id) FROM test WHERE b = '3';")
> +execute(s.stmt_id)
> +execute(s.stmt_id)
> +unprepare(s.stmt_id)
> +
> +-- Let's try something a bit more complicated. For instance recursive
> +-- query displaying Mandelbrot set.
> +--
> +s = prepare([[WITH RECURSIVE \
> + xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2), \
> + yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0), \
> + m(iter, cx, cy, x, y) AS ( \
> + SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis \
> + UNION ALL \
> + SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m \
> + WHERE (x*x + y*y) < 4.0 AND iter<28), \
> + m2(iter, cx, cy) AS ( \
> + SELECT max(iter), cx, cy FROM m GROUP BY cx, cy), \
> + a(t) AS ( \
> + SELECT group_concat( substr(' .+*#', 1+LEAST(iter/7,4), 1), '') \
> + FROM m2 GROUP BY cy) \
> + SELECT group_concat(TRIM(TRAILING FROM t),x'0a') FROM a;]])
> +
> +res = execute(s.stmt_id)
> +res.metadata
> +unprepare(s.stmt_id)
> +
> +-- Workflow with bindings is still the same.
> +--
> +s = prepare("SELECT a FROM test WHERE b = ?;")
> +execute(s.stmt_id, {'6'})
> +execute(s.stmt_id, {'9'})
> +unprepare(s.stmt_id)
> +
> +-- DML
> +s = prepare("INSERT INTO test VALUES (?, ?, ?);")
> +execute(s.stmt_id, {5, 6, '7'})
> +execute(s.stmt_id, {6, 10, '7'})
> +execute(s.stmt_id, {9, 11, '7'})
> +unprepare(s.stmt_id)
> +
> +-- EXPLAIN and PRAGMA work fine as well.
> +--
> +s1 = prepare("EXPLAIN SELECT a FROM test WHERE b = '3';")
> +res = execute(s1.stmt_id)
> +res.metadata
> +assert(res.rows ~= nil)
> +
> +s2 = prepare("EXPLAIN QUERY PLAN SELECT a FROM test WHERE b = '3';")
> +res = execute(s2.stmt_id)
> +res.metadata
> +assert(res.rows ~= nil)
> +
> +s3 = prepare("PRAGMA count_changes;")
> +execute(s3.stmt_id)
> +
> +unprepare(s2.stmt_id)
> +unprepare(s3.stmt_id)
> +unprepare(s1.stmt_id)
> +
> +-- Setting cache size to 0 is possible only in case if
> +-- there's no any prepared statements right now .
> +--
> +box.cfg{sql_cache_size = 0}
> +prepare("SELECT a FROM test;")
> +box.cfg{sql_cache_size = 0}
> +
> +-- Still with small size everything should work.
> +--
> +box.cfg{sql_cache_size = 1500}
> +
> +test_run:cmd("setopt delimiter ';'");
> +ok = nil
> +res = nil
> +_ = fiber.create(function()
> + for i = 1, 5 do
> + pcall(prepare, string.format("SELECT * FROM test WHERE a = %d;", i))
> + end
> + ok, res = pcall(prepare, "SELECT * FROM test WHERE b = '3';")
> +end);
> +while ok == nil do fiber.sleep(0.00001) end;
> +assert(ok == false);
> +res;
> +
> +-- Check that after fiber is dead, its session gets rid of
> +-- all prepared statements.
> +--
> +box.cfg{sql_cache_size = 0};
> +box.cfg{sql_cache_size = 3000};
> +
> +-- Make sure that if prepared statement is busy (is executed
> +-- right now), prepared statement is not used, i.e. statement
> +-- is compiled from scratch, executed and finilized.
> +--
> +box.schema.func.create('SLEEP', {language = 'Lua',
> + body = 'function () fiber.sleep(0.1) return 1 end',
> + exports = {'LUA', 'SQL'}});
> +
> +s = prepare("SELECT id, SLEEP() FROM test;");
> +assert(s ~= nil);
> +
> +function implicit_yield()
> + prepare("SELECT id, SLEEP() FROM test;")
> + execute("SELECT id, SLEEP() FROM test;")
> +end;
> +
> +f1 = fiber.new(implicit_yield)
> +f2 = fiber.new(implicit_yield)
> +f1:set_joinable(true)
> +f2:set_joinable(true)
> +
> +f1:join();
> +f2:join();
> +
> +unprepare(s.stmt_id);
> +
> +test_run:cmd("setopt delimiter ''");
> +
> +box.cfg{sql_cache_size = 5 * 1024 * 1024}
> +box.space.TEST:drop()
> +box.schema.func.drop('SLEEP')
> --
> 2.15.1
>
More information about the Tarantool-patches
mailing list