[patches] [PATCH 2/4] sql: introduce 'return_tuple' option for execute()

Vladislav Shpilevoy v.shpilevoy at tarantool.org
Wed Mar 14 00:49:25 MSK 2018


If 'return_tuple' is specified and is set to true, then
last updated/inserted tuple is returned in IPROTO_DATA section.

Closes #2618

Signed-off-by: Vladislav Shpilevoy <v.shpilevoy at tarantool.org>
---
 src/box/errcode.h        |  2 +-
 src/box/execute.c        | 95 +++++++++++++++++++++++++++++++++++++++++++-----
 src/box/execute.h        | 20 +++++++++-
 src/box/iproto.cc        |  4 +-
 src/box/lua/net_box.c    | 28 +++++++++++++-
 src/box/lua/net_box.lua  | 31 +++++++++-------
 test/box/misc.result     | 33 +++++++++--------
 test/sql/iproto.result   | 67 +++++++++++++++++++++++++++++++++-
 test/sql/iproto.test.lua | 22 +++++++++++
 9 files changed, 255 insertions(+), 47 deletions(-)

diff --git a/src/box/errcode.h b/src/box/errcode.h
index beb37aa2f..0740acfb9 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -107,7 +107,7 @@ struct errcode_record {
 	/* 52 */_(ER_FUNCTION_EXISTS,		"Function '%s' already exists") \
 	/* 53 */_(ER_BEFORE_REPLACE_RET,	"Invalid return value of space:before_replace trigger: expected tuple or nil, got %s") \
 	/* 54 */_(ER_FUNCTION_MAX,		"A limit on the total number of functions has been reached: %u") \
-	/* 55 */_(ER_UNUSED4,			"") \
+	/* 55 */_(ER_WRONG_SQL_OPTION,		"Wrong SQL option: code = %llu, name = %s") \
 	/* 56 */_(ER_USER_MAX,			"A limit on the total number of users has been reached: %u") \
 	/* 57 */_(ER_NO_SUCH_ENGINE,		"Space engine '%s' does not exist") \
 	/* 58 */_(ER_RELOAD_CFG,		"Can't set option '%s' dynamically") \
diff --git a/src/box/execute.c b/src/box/execute.c
index 756db5fd6..2c347e92c 100644
--- a/src/box/execute.c
+++ b/src/box/execute.c
@@ -42,6 +42,7 @@
 #include "schema.h"
 #include "port.h"
 #include "memtx_tuple.h"
+#include "tuple_convert.h"
 
 const char *sql_type_strs[] = {
 	NULL,
@@ -52,6 +53,10 @@ const char *sql_type_strs[] = {
 	"NULL",
 };
 
+const char *sql_options_key_strs[] = {
+	"return tuple",
+};
+
 /**
  * Name and value of an SQL prepared statement parameter.
  * @todo: merge with sqlite3_value.
@@ -241,6 +246,45 @@ sql_bind_list_decode(struct sql_request *request, const char *data,
 	return 0;
 }
 
+/**
+ * Decode IPROTO_SQL_OPTIONS.
+ * @param[out] sql_request Request to decode to.
+ * @param data MessagePack encoded options.
+ *
+ * @retval  0 Success.
+ * @retval -1 Client error.
+ */
+static inline int
+sql_options_decode(struct sql_request *request, const char *data)
+{
+	assert(request != NULL);
+	assert(data != NULL);
+	if (mp_typeof(*data) != MP_MAP) {
+mp_error:
+		diag_set(ClientError, ER_INVALID_MSGPACK, "SQL options");
+		return -1;
+	}
+	uint32_t opts_count = mp_decode_map(&data);
+	for (uint32_t i = 0; i < opts_count; ++i) {
+		if (mp_typeof(*data) != MP_UINT)
+			goto mp_error;
+		uint64_t key = mp_decode_uint(&data);
+		if (key != SQL_OPTION_RETURN_TUPLE ||
+		    mp_typeof(*data) != MP_BOOL) {
+			const char *name;
+			if (key >= sql_options_key_MAX)
+				name = "unknown";
+			else
+				name = sql_options_key_strs[key];
+			diag_set(ClientError, ER_WRONG_SQL_OPTION,
+				 (unsigned long long)key, name);
+			return -1;
+		}
+		request->is_last_tuple_needed = mp_decode_bool(&data);
+	}
+	return 0;
+}
+
 int
 xrow_decode_sql(const struct xrow_header *row, struct sql_request *request,
 		struct region *region)
@@ -255,7 +299,7 @@ xrow_decode_sql(const struct xrow_header *row, struct sql_request *request,
 	assert((end - data) > 0);
 
 	if (mp_typeof(*data) != MP_MAP || mp_check_map(data, end) > 0) {
-error:
+mp_error:
 		diag_set(ClientError, ER_INVALID_MSGPACK, "packet body");
 		return -1;
 	}
@@ -267,19 +311,34 @@ error:
 	request->sync = row->sync;
 	for (uint32_t i = 0; i < map_size; ++i) {
 		uint8_t key = *data;
-		if (key != IPROTO_SQL_BIND && key != IPROTO_SQL_TEXT) {
+		if (key != IPROTO_SQL_BIND && key != IPROTO_SQL_TEXT &&
+		    key != IPROTO_SQL_OPTIONS) {
 			mp_check(&data, end);   /* skip the key */
 			mp_check(&data, end);   /* skip the value */
 			continue;
 		}
 		const char *value = ++data;     /* skip the key */
 		if (mp_check(&data, end) != 0)  /* check the value */
-			goto error;
-		if (key == IPROTO_SQL_BIND) {
+			goto mp_error;
+		switch(key) {
+		case IPROTO_SQL_BIND:
 			if (sql_bind_list_decode(request, value, region) != 0)
 				return -1;
-		} else {
+			break;
+		case IPROTO_SQL_TEXT:
+			if (mp_typeof(*value) != MP_STR) {
+				diag_set(ClientError, ER_INVALID_MSGPACK,
+					 "SQL text");
+				return -1;
+			}
 			request->sql_text = value;
+			break;
+		case IPROTO_SQL_OPTIONS:
+			if (sql_options_decode(request, value) != 0)
+				return -1;
+			break;
+		default:
+			unreachable();
 		}
 	}
 	if (request->sql_text == NULL) {
@@ -288,7 +347,7 @@ error:
 		return -1;
 	}
 	if (data != end)
-		goto error;
+		goto mp_error;
 	return 0;
 }
 
@@ -621,7 +680,6 @@ sql_execute_and_encode(sqlite3 *db, struct sqlite3_stmt *stmt, struct obuf *out,
 			goto err_body;
 		}
 	} else {
-		keys = 1;
 		assert(port_tuple->size == 0);
 		if (iproto_reply_map_key(out, 1, IPROTO_SQL_INFO) != 0)
 			goto err_body;
@@ -635,6 +693,25 @@ sql_execute_and_encode(sqlite3 *db, struct sqlite3_stmt *stmt, struct obuf *out,
 		}
 		buf = mp_encode_uint(buf, IPROTO_SQL_ROW_COUNT);
 		buf = mp_encode_uint(buf, changes);
+
+		/* Reply DATA if needed. */
+		if (opts->last_tuple != NULL) {
+			keys = 2;
+			/*
+			 * Even if a last tuple was requested but
+			 * was not found, the empty IPROTO_DATA
+			 * must be returned.
+			 */
+			int tuples = *opts->last_tuple == NULL ? 0 : 1;
+			if (iproto_reply_array_key(out, tuples,
+						   IPROTO_DATA) != 0)
+				goto err_body;
+			if (tuples == 1 &&
+			    tuple_to_obuf(*opts->last_tuple, out) != 0)
+				goto err_body;
+		} else {
+			keys = 1;
+		}
 	}
 	port_destroy(&port);
 	iproto_reply_sql(out, &header_svp, sync, schema_version, keys);
@@ -649,7 +726,7 @@ err_execute:
 
 int
 sql_prepare_and_execute(const struct sql_request *request, struct obuf *out,
-			struct region *region, bool is_last_tuple_needed)
+			struct region *region)
 {
 	struct sql_options opts;
 	struct tuple *last_inserted_tuple = NULL;
@@ -671,7 +748,7 @@ sql_prepare_and_execute(const struct sql_request *request, struct obuf *out,
 	if (sql_bind(request, stmt) != 0)
 		goto err_stmt;
 
-	if (is_last_tuple_needed)
+	if (request->is_last_tuple_needed)
 		sql_options_create(&opts, &last_inserted_tuple);
 	else
 		sql_options_create(&opts, NULL);
diff --git a/src/box/execute.h b/src/box/execute.h
index 0aef26702..a0d23a4ef 100644
--- a/src/box/execute.h
+++ b/src/box/execute.h
@@ -38,6 +38,13 @@
 extern "C" {
 #endif
 
+enum sql_options_key {
+	SQL_OPTION_RETURN_TUPLE = 0,
+	sql_options_key_MAX,
+};
+
+extern const char *sql_options_key_strs[];
+
 struct obuf;
 struct region;
 struct sql_bind;
@@ -52,6 +59,8 @@ struct sql_request {
 	struct sql_bind *bind;
 	/** Length of the @bind. */
 	uint32_t bind_count;
+	/** True, if a last inserted tuple must be returned. */
+	bool is_last_tuple_needed;
 };
 
 /**
@@ -93,6 +102,15 @@ xrow_decode_sql(const struct xrow_header *row, struct sql_request *request,
  * |         IPROTO_SQL_ROW_COUNT: number         |
  * |     }                                        |
  * | }                                            |
+ * +-------------------- OR ----------------------+
+ * | IPROTO_BODY: {                               |
+ * |     IPROTO_SQL_INFO: {                       |
+ * |         IPROTO_SQL_ROW_COUNT: number         |
+ * |     },                                       |
+ * |     IPROTO_DATA: [                           |
+ * |         tuple                                |
+ * |     ]                                        |
+ * | }                                            |
  * +----------------------------------------------+
  *
  * @param request IProto request.
@@ -105,7 +123,7 @@ xrow_decode_sql(const struct xrow_header *row, struct sql_request *request,
  */
 int
 sql_prepare_and_execute(const struct sql_request *request, struct obuf *out,
-			struct region *region, bool is_last_tuple_needed);
+			struct region *region);
 
 #if defined(__cplusplus)
 } /* extern "C" { */
diff --git a/src/box/iproto.cc b/src/box/iproto.cc
index 9ace0d082..f7784ef23 100644
--- a/src/box/iproto.cc
+++ b/src/box/iproto.cc
@@ -1382,15 +1382,13 @@ tx_process_sql(struct cmsg *m)
 {
 	struct iproto_msg *msg = tx_accept_msg(m);
 	struct obuf *out = msg->connection->tx.p_obuf;
-	bool is_last_tuple_needed = true;
 
 	tx_fiber_init(msg->connection->session, msg->header.sync);
 
 	if (tx_check_schema(msg->header.schema_version))
 		goto error;
 	assert(msg->header.type == IPROTO_EXECUTE);
-	if (sql_prepare_and_execute(&msg->sql, out, &fiber()->gc,
-				    is_last_tuple_needed) != 0)
+	if (sql_prepare_and_execute(&msg->sql, out, &fiber()->gc) != 0)
 		goto error;
 	iproto_wpos_create(&msg->wpos, out);
 	return;
diff --git a/src/box/lua/net_box.c b/src/box/lua/net_box.c
index efb391379..40e585cfb 100644
--- a/src/box/lua/net_box.c
+++ b/src/box/lua/net_box.c
@@ -36,6 +36,7 @@
 #include "scramble.h"
 
 #include "box/iproto_constants.h"
+#include "box/execute.h"
 #include "box/lua/tuple.h" /* luamp_convert_tuple() / luamp_convert_key() */
 #include "box/xrow.h"
 
@@ -557,10 +558,31 @@ handle_error:
 static int
 netbox_encode_execute(lua_State *L)
 {
-	if (lua_gettop(L) < 6)
+	if (lua_gettop(L) < 6) {
+usage_error:
 		return luaL_error(L, "Usage: netbox.encode_execute(ibuf, "\
 				  "sync, schema_version, query, parameters, "\
 				  "options)");
+	}
+	/* Decode and validate options. */
+	struct luaL_field field;
+	luaL_tofield(L, cfg, 6, &field);
+	if (field.type != MP_MAP || field.size > 1)
+		goto usage_error;
+	bool opt_return_tuple;
+	if (field.size > 0) {
+		lua_getfield(L, 6, "return_tuple");
+		if (lua_isnil(L, -1) || !lua_isboolean(L, -1)) {
+			lua_pop(L, 1);
+			goto usage_error;
+		}
+		opt_return_tuple = lua_toboolean(L, -1);
+		lua_pop(L, 1);
+	} else {
+		opt_return_tuple = false;
+	}
+
+	/* Encode request. */
 	struct mpstream stream;
 	size_t svp = netbox_prepare_request(L, &stream, IPROTO_EXECUTE);
 
@@ -575,7 +597,9 @@ netbox_encode_execute(lua_State *L)
 	luamp_encode_tuple(L, cfg, &stream, 5);
 
 	luamp_encode_uint(cfg, &stream, IPROTO_SQL_OPTIONS);
-	luamp_encode_tuple(L, cfg, &stream, 6);
+	luamp_encode_map(cfg, &stream, 1);
+	luamp_encode_uint(cfg, &stream, SQL_OPTION_RETURN_TUPLE);
+	luamp_encode_bool(cfg, &stream, opt_return_tuple);
 
 	netbox_encode_request(&stream, svp);
 	return 0;
diff --git a/src/box/lua/net_box.lua b/src/box/lua/net_box.lua
index 9c660a9dd..0c6d30c24 100644
--- a/src/box/lua/net_box.lua
+++ b/src/box/lua/net_box.lua
@@ -73,6 +73,8 @@ local method_codec           = {
     end
 }
 
+local function setmap(tab) return setmetatable(tab, { __serialize = 'map' }) end
+
 local function next_id(id) return band(id + 1, 0x7FFFFFFF) end
 
 -- function create_transport(host, port, user, password, callback)
@@ -853,35 +855,36 @@ end
 
 function remote_methods:execute(query, parameters, sql_opts, netbox_opts)
     check_remote_arg(self, "execute")
-    if sql_opts ~= nil then
-        box.error(box.error.UNSUPPORTED, "execute", "options")
-    end
     local timeout = self:request_timeout(netbox_opts)
     local buffer = netbox_opts and netbox_opts.buffer
     parameters = parameters or {}
-    sql_opts = sql_opts or {}
-    local err, res, metadata, info = self._transport.perform_request(timeout,
-                                    buffer, 'execute', self.schema_version,
-                                    query, parameters, sql_opts)
+    sql_opts = sql_opts or setmap({})
+    local err, data, metadata, info =
+        self._transport.perform_request(timeout, buffer, 'execute',
+                                        self.schema_version, query, parameters,
+                                        sql_opts)
     if err then
-        box.error({code = err, reason = res})
+        box.error({code = err, reason = data})
     end
     if buffer ~= nil then
-        return res -- body length. Body is written to the buffer.
+        return data -- body length. Body is written to the buffer.
     end
-    assert((info == nil and metadata ~= nil and res ~= nil) or
-           (info ~= nil and metadata == nil and res == nil))
     if info ~= nil then
         assert(info[IPROTO_SQL_ROW_COUNT_KEY] ~= nil)
-        return {rowcount = info[IPROTO_SQL_ROW_COUNT_KEY]}
+        local ret = {rowcount = info[IPROTO_SQL_ROW_COUNT_KEY]}
+        if data ~= nil and #data > 0 then
+            assert(#data == 1)
+            ret.last_tuple = data[1]
+        end
+        return ret
     end
     -- Set readable names for the metadata fields.
     for i, field_meta in pairs(metadata) do
         field_meta["name"] = field_meta[IPROTO_FIELD_NAME_KEY]
         field_meta[IPROTO_FIELD_NAME_KEY] = nil
     end
-    setmetatable(res, sequence_mt)
-    return {metadata = metadata, rows = res}
+    setmetatable(data, sequence_mt)
+    return {metadata = metadata, rows = data}
 end
 
 function remote_methods:wait_state(state, timeout)
diff --git a/test/box/misc.result b/test/box/misc.result
index aeee42930..fdd43185f 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -314,6 +314,7 @@ t;
   - 'box.error.NO_SUCH_TRIGGER : 34'
   - 'box.error.SEQUENCE_EXISTS : 146'
   - 'box.error.CHECKPOINT_IN_PROGRESS : 120'
+  - 'box.error.WRONG_SQL_OPTION : 55'
   - 'box.error.FIELD_TYPE : 23'
   - 'box.error.SQL_BIND_PARAMETER_MAX : 156'
   - 'box.error.WRONG_SPACE_FORMAT : 141'
@@ -338,7 +339,7 @@ t;
   - 'box.error.CANT_CREATE_COLLATION : 150'
   - 'box.error.USER_EXISTS : 46'
   - 'box.error.WAL_IO : 40'
-  - 'box.error.PROC_RET : 21'
+  - 'box.error.RTREE_RECT : 101'
   - 'box.error.PRIV_GRANTED : 89'
   - 'box.error.CREATE_SPACE : 9'
   - 'box.error.GRANT : 88'
@@ -359,8 +360,8 @@ t;
   - 'box.error.DROP_FUNCTION : 71'
   - 'box.error.CFG : 59'
   - 'box.error.NO_SUCH_FIELD : 37'
-  - 'box.error.CONNECTION_TO_SELF : 117'
-  - 'box.error.FUNCTION_MAX : 54'
+  - 'box.error.MORE_THAN_ONE_TUPLE : 41'
+  - 'box.error.PROC_LUA : 32'
   - 'box.error.ILLEGAL_PARAMS : 1'
   - 'box.error.PARTIAL_KEY : 136'
   - 'box.error.SAVEPOINT_NO_TRANSACTION : 114'
@@ -393,13 +394,13 @@ t;
   - 'box.error.injection : table: <address>
   - 'box.error.FUNCTION_TX_ACTIVE : 30'
   - 'box.error.SQL_BIND_NOT_FOUND : 159'
-  - 'box.error.RELOAD_CFG : 58'
+  - 'box.error.FUNCTION_MAX : 54'
   - 'box.error.NO_SUCH_ENGINE : 57'
   - 'box.error.COMMIT_IN_SUB_STMT : 122'
   - 'box.error.SQL_EXECUTE : 157'
   - 'box.error.NULLABLE_MISMATCH : 153'
   - 'box.error.LAST_DROP : 15'
-  - 'box.error.NO_SUCH_ROLE : 82'
+  - 'box.error.INJECTION : 8'
   - 'box.error.DECOMPRESSION : 124'
   - 'box.error.CREATE_SEQUENCE : 142'
   - 'box.error.CREATE_USER : 43'
@@ -408,7 +409,7 @@ t;
   - 'box.error.SEQUENCE_OVERFLOW : 147'
   - 'box.error.SYSTEM : 115'
   - 'box.error.KEY_PART_IS_TOO_LONG : 118'
-  - 'box.error.TUPLE_FORMAT_LIMIT : 16'
+  - 'box.error.RELOAD_CFG : 58'
   - 'box.error.BEFORE_REPLACE_RET : 53'
   - 'box.error.NO_SUCH_SAVEPOINT : 61'
   - 'box.error.TRUNCATE_SYSTEM_SPACE : 137'
@@ -418,7 +419,7 @@ t;
   - 'box.error.INDEX_FIELD_COUNT_LIMIT : 127'
   - 'box.error.READ_VIEW_ABORTED : 130'
   - 'box.error.USER_MAX : 56'
-  - 'box.error.PROTOCOL : 104'
+  - 'box.error.CONNECTION_TO_SELF : 117'
   - 'box.error.TUPLE_NOT_ARRAY : 22'
   - 'box.error.KEY_PART_COUNT : 31'
   - 'box.error.ALTER_SPACE : 12'
@@ -426,22 +427,22 @@ t;
   - 'box.error.EXACT_FIELD_COUNT : 38'
   - 'box.error.DROP_SEQUENCE : 144'
   - 'box.error.INVALID_MSGPACK : 20'
-  - 'box.error.MORE_THAN_ONE_TUPLE : 41'
-  - 'box.error.RTREE_RECT : 101'
-  - 'box.error.SUB_STMT_MAX : 121'
+  - 'box.error.UPSERT_UNIQUE_SECONDARY_KEY : 105'
   - 'box.error.UNKNOWN_REQUEST_TYPE : 48'
+  - 'box.error.SUB_STMT_MAX : 121'
+  - 'box.error.PROC_RET : 21'
   - 'box.error.SPACE_EXISTS : 10'
-  - 'box.error.PROC_LUA : 32'
   - 'box.error.ROLE_NOT_GRANTED : 92'
+  - 'box.error.NO_SUCH_ROLE : 82'
   - 'box.error.NO_SUCH_SPACE : 36'
   - 'box.error.WRONG_INDEX_PARTS : 107'
-  - 'box.error.DROP_SPACE : 11'
   - 'box.error.MIN_FIELD_COUNT : 39'
   - 'box.error.REPLICASET_UUID_MISMATCH : 63'
   - 'box.error.UPDATE_FIELD : 29'
+  - 'box.error.INDEX_EXISTS : 85'
   - 'box.error.COMPRESSION : 119'
   - 'box.error.INVALID_ORDER : 68'
-  - 'box.error.INDEX_EXISTS : 85'
+  - 'box.error.DROP_SPACE : 11'
   - 'box.error.SPLICE : 25'
   - 'box.error.UNKNOWN : 0'
   - 'box.error.DROP_PRIMARY_KEY : 17'
@@ -449,7 +450,7 @@ t;
   - 'box.error.NO_SUCH_SEQUENCE : 145'
   - 'box.error.SQL : 158'
   - 'box.error.INVALID_UUID : 64'
-  - 'box.error.INJECTION : 8'
+  - 'box.error.TUPLE_FORMAT_LIMIT : 16'
   - 'box.error.TIMEOUT : 78'
   - 'box.error.IDENTIFIER : 70'
   - 'box.error.ITERATOR_TYPE : 72'
@@ -462,10 +463,10 @@ t;
   - 'box.error.UPDATE_INTEGER_OVERFLOW : 95'
   - 'box.error.NO_CONNECTION : 77'
   - 'box.error.INVALID_XLOG_ORDER : 76'
-  - 'box.error.UPSERT_UNIQUE_SECONDARY_KEY : 105'
-  - 'box.error.ROLLBACK_IN_SUB_STMT : 123'
   - 'box.error.WRONG_SCHEMA_VERSION : 109'
+  - 'box.error.ROLLBACK_IN_SUB_STMT : 123'
   - 'box.error.UNSUPPORTED_INDEX_FEATURE : 112'
+  - 'box.error.PROTOCOL : 104'
   - 'box.error.INDEX_PART_TYPE_MISMATCH : 24'
   - 'box.error.INVALID_XLOG_TYPE : 125'
 ...
diff --git a/test/sql/iproto.result b/test/sql/iproto.result
index f7749cb05..e17931496 100644
--- a/test/sql/iproto.result
+++ b/test/sql/iproto.result
@@ -86,7 +86,8 @@ cn:execute(100)
 ...
 cn:execute('select 1', nil, {dry_run = true})
 ---
-- error: execute does not support options
+- error: 'builtin/box/net_box.lua:250: Usage: netbox.encode_execute(ibuf, sync, schema_version,
+    query, parameters, options)'
 ...
 -- Empty request.
 cn:execute('')
@@ -471,6 +472,70 @@ res.metadata
 ---
 - [{'name': ID}, {'name': 'A'}, {'name': 'B'}]
 ...
+--
+-- gh-2618: add option 'return_tuple'.
+--
+space:truncate()
+---
+...
+cn:reload_schema()
+---
+...
+cn:execute("insert into test values (1, 2, '3')", nil, {return_tuple = true})
+---
+- last_tuple: [1, 2, '3']
+  rowcount: 1
+...
+cn:execute("insert into test values (2, 3, '4'), (3, 4, '5')", nil, {return_tuple = true})
+---
+- last_tuple: [3, 4, '5']
+  rowcount: 2
+...
+-- No last tuple in a case of error.
+cn:execute("insert into test values (1, 2, '3')", nil, {return_tuple = true})
+---
+- error: 'Failed to execute SQL statement: Duplicate key exists in unique index ''sqlite_autoindex_TEST_1''
+    in space ''TEST'''
+...
+-- Update.
+cn:execute('update test set a = 20 where id = 1', nil, {return_tuple = true})
+---
+- last_tuple: [1, 20, '3']
+  rowcount: 1
+...
+-- A last tuple is requested, but not found.
+cn:execute('update test set a = 200 where id = 100', nil, {return_tuple = true})
+---
+- rowcount: 0
+...
+-- Ignore requested tuple on delete, select.
+cn:execute('delete from test where id = 1', nil, {return_tuple = true})
+---
+- rowcount: 1
+...
+cn:execute('select * from test', nil, {return_tuple = true})
+---
+- metadata: [{'name': ID}, {'name': 'A'}, {'name': 'B'}]
+  rows:
+  - [2, 3, '4']
+  - [3, 4, '5']
+...
+-- Invalid options.
+cn:execute('select * from test', nil, {return_tuple = 1})
+---
+- error: 'builtin/box/net_box.lua:250: Usage: netbox.encode_execute(ibuf, sync, schema_version,
+    query, parameters, options)'
+...
+cn:execute('select * from test', nil, {[1] = {return_tuple = true}})
+---
+- error: 'builtin/box/net_box.lua:250: Usage: netbox.encode_execute(ibuf, sync, schema_version,
+    query, parameters, options)'
+...
+cn:execute('select * from test', nil, {bad_option = true})
+---
+- error: 'builtin/box/net_box.lua:250: Usage: netbox.encode_execute(ibuf, sync, schema_version,
+    query, parameters, options)'
+...
 cn:close()
 ---
 ...
diff --git a/test/sql/iproto.test.lua b/test/sql/iproto.test.lua
index 64c0a56fe..c5bff8e76 100644
--- a/test/sql/iproto.test.lua
+++ b/test/sql/iproto.test.lua
@@ -179,6 +179,28 @@ _ = space:replace{1, 1, string.rep('a', 4 * 1024 * 1024)}
 res = cn:execute('select * from test')
 res.metadata
 
+--
+-- gh-2618: add option 'return_tuple'.
+--
+space:truncate()
+cn:reload_schema()
+cn:execute("insert into test values (1, 2, '3')", nil, {return_tuple = true})
+cn:execute("insert into test values (2, 3, '4'), (3, 4, '5')", nil, {return_tuple = true})
+-- No last tuple in a case of error.
+cn:execute("insert into test values (1, 2, '3')", nil, {return_tuple = true})
+-- Update.
+cn:execute('update test set a = 20 where id = 1', nil, {return_tuple = true})
+-- A last tuple is requested, but not found.
+cn:execute('update test set a = 200 where id = 100', nil, {return_tuple = true})
+-- Ignore requested tuple on delete, select.
+cn:execute('delete from test where id = 1', nil, {return_tuple = true})
+cn:execute('select * from test', nil, {return_tuple = true})
+
+-- Invalid options.
+cn:execute('select * from test', nil, {return_tuple = 1})
+cn:execute('select * from test', nil, {[1] = {return_tuple = true}})
+cn:execute('select * from test', nil, {bad_option = true})
+
 cn:close()
 box.schema.user.revoke('guest', 'read,write,execute', 'universe')
 box.sql.execute('drop table test')
-- 
2.14.3 (Apple Git-98)




More information about the Tarantool-patches mailing list