From f0da9e84188aa69c753ec50345e308f274224a27 Mon Sep 17 00:00:00 2001
From: Konstantin Osipov <kostja.osipov@gmail.com>
Date: Fri, 3 Feb 2012 00:13:50 +0400
Subject: [PATCH] Feature 'add-delete-fields': add/delete fields with UPDATE

Implement blueprint 'add-delete-fields':
https://blueprints.launchpad.net/tarantool/+spec/add-and-delete-fields-in-tuple

Make it possible to add new fields with UPDATE, and
delete existing fields.

Rewrite the UPDATE algorithm to pre-compute new tuple
length and do most of the work in place, without
allocating temporary buffer for fields.

Add Connector/C support for new UPDATE options.

Add and update tests.

Joint effort by Konstantin Shulgin (initial implementation),
Dmitry Simonenko (Connector/C) and
Konstantin Osipov (spec/code review).
---
 .gitignore                           |   1 +
 connector/c/include/tnt_proto.h      | 100 ++++
 connector/c/include/tnt_update.h     | 131 +++++
 connector/c/tnt/include/tnt_update.h |   4 +
 connector/c/tnt/tnt_update.c         |  26 +-
 core/tarantool_lua.m                 |   4 +-
 include/errcode.h                    |  10 +-
 include/util.h                       |   4 +-
 mod/box/box.h                        |  16 +
 mod/box/box.m                        | 720 ++++++++++++++++++------
 test/box/lua.result                  |   8 +
 test/box/lua.test                    |   3 +
 test/box/sql.result                  |  11 +-
 test/box/sql.test                    |   7 +-
 test/connector_c/CMakeLists.txt      |   1 +
 test/connector_c/update.c            | 794 +++++++++++++++++++++++++++
 test/connector_c/update.result       | 197 +++++++
 test/connector_c/update.test         |  15 +
 18 files changed, 1861 insertions(+), 191 deletions(-)
 create mode 100644 connector/c/include/tnt_proto.h
 create mode 100644 connector/c/include/tnt_update.h
 create mode 100644 test/connector_c/update.c
 create mode 100644 test/connector_c/update.result
 create mode 100644 test/connector_c/update.test

diff --git a/.gitignore b/.gitignore
index b8e09d4551..1927572f6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,4 +37,5 @@ third_party/luajit/src/lj_libdef.h
 third_party/luajit/src/lj_recdef.h
 third_party/luajit/src/lj_vm.s
 test/connector_c/tt
+test/connector_c/update
 *.reject
diff --git a/connector/c/include/tnt_proto.h b/connector/c/include/tnt_proto.h
new file mode 100644
index 0000000000..c0fcafa45f
--- /dev/null
+++ b/connector/c/include/tnt_proto.h
@@ -0,0 +1,100 @@
+#ifndef TNT_PROTO_H_INCLUDED
+#define TNT_PROTO_H_INCLUDED
+
+/*
+ * Copyright (C) 2011 Mail.RU
+ *
+ * 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 AUTHOR AND CONTRIBUTORS ``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 AUTHOR 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.
+ */
+
+#define TNT_PROTO_TYPE_INSERT 13
+#define TNT_PROTO_TYPE_SELECT 17
+#define TNT_PROTO_TYPE_UPDATE 19
+#define TNT_PROTO_TYPE_DELETE 20
+#define TNT_PROTO_TYPE_PING   65280
+
+struct tnt_proto_header {
+	uint32_t type;
+	uint32_t len;
+	uint32_t reqid;
+};
+
+#define TNT_PROTO_IS_OK(V) ((V) == 0x0)
+
+struct tnt_proto_header_resp {
+	struct tnt_proto_header hdr;
+	uint32_t code;
+};
+
+struct tnt_proto_tuple {
+	uint32_t card;
+	unsigned char field[];
+};
+
+#define TNT_PROTO_FLAG_RETURN    0x01
+#define TNT_PROTO_FLAG_ADD       0x02
+#define TNT_PROTO_FLAG_REPLACE   0x04
+#define TNT_PROTO_FLAG_BOX_QUIET 0x08
+#define TNT_PROTO_FLAG_NOT_STORE 0x10
+
+struct tnt_proto_insert {
+	uint32_t ns;
+	uint32_t flags;
+	/* tuple data */
+};
+
+#define TNT_PROTO_UPDATE_ASSIGN		0
+#define TNT_PROTO_UPDATE_ADD		1
+#define TNT_PROTO_UPDATE_AND		2
+#define TNT_PROTO_UPDATE_XOR		3
+#define TNT_PROTO_UPDATE_OR		4
+#define TNT_PROTO_UPDATE_SPLICE		5
+#define TNT_PROTO_UPDATE_DELETE_FIELD 	6
+
+struct tnt_proto_update {
+	uint32_t ns;
+	uint32_t flags;
+	/* tuple data */
+	/* count */
+	/* operation */
+};
+
+struct tnt_proto_update_op {
+	uint32_t field;
+	unsigned char op;
+	/* op_arg */
+};
+
+struct tnt_proto_delete {
+	uint32_t ns;
+	/* tuple data */
+};
+
+struct tnt_proto_select {
+	uint32_t ns;
+	uint32_t index;
+	uint32_t offset;
+	uint32_t limit;
+	/* tuple data */
+};
+
+#endif /* TNT_PROTO_H_INCLUDED */
diff --git a/connector/c/include/tnt_update.h b/connector/c/include/tnt_update.h
new file mode 100644
index 0000000000..2b2d94f426
--- /dev/null
+++ b/connector/c/include/tnt_update.h
@@ -0,0 +1,131 @@
+#ifndef TNT_UPDATE_H_INCLUDED
+#define TNT_UPDATE_H_INCLUDED
+
+/*
+ * Copyright (C) 2011 Mail.RU
+ *
+ * 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 AUTHOR AND CONTRIBUTORS ``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 AUTHOR 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.
+ */
+
+/**
+ * @defgroup Update
+ * @ingroup Operations
+ * @brief Update operation
+ */
+enum tnt_update_type {
+	TNT_UPDATE_NONE,
+	TNT_UPDATE_ASSIGN,
+	TNT_UPDATE_ADD,
+	TNT_UPDATE_AND,
+	TNT_UPDATE_XOR,
+	TNT_UPDATE_OR,
+	TNT_UPDATE_SPLICE,
+	TNT_UPDATE_DELETE_FIELD
+};
+/** @} */
+
+struct tnt_update_op {
+	uint8_t op;
+	uint32_t field;
+	char *data;
+	uint32_t size;
+	uint32_t size_leb;
+	STAILQ_ENTRY(tnt_update_op) next;
+};
+
+struct tnt_update {
+	uint32_t count;
+	uint32_t size_enc;
+	STAILQ_HEAD(,tnt_update_op) list;
+};
+
+/**
+ * @defgroup Handler
+ * @ingroup Update
+ * @brief Update handler initizalization and operations 
+ *
+ * @{
+ */
+void tnt_update_init(struct tnt_update *update);
+void tnt_update_free(struct tnt_update *update);
+
+enum tnt_error
+tnt_update_assign(struct tnt_update *update, int field,
+		  char *value, int value_size);
+enum tnt_error
+tnt_update_arith(struct tnt_update *update, int field,
+		 enum tnt_update_type op, int value);
+enum tnt_error
+tnt_update_splice(struct tnt_update *update, int field,
+		  int offset, int length, char *list, int list_size);
+
+enum tnt_error
+tnt_update_delete_field(struct tnt_update *update, int field);
+/** @} */
+
+/**
+ * @defgroup Operation
+ * @ingroup Update
+ * @brief Update operation
+ *
+ * @{
+ */
+
+/**
+ * Update operation with tuple.
+ *
+ * If bufferization is in use, then request would be placed in
+ * internal buffer for later sending. Otherwise, operation
+ * would be processed immediately.
+ *
+ * @param t handler pointer
+ * @param reqid user supplied integer value
+ * @param ns namespace number
+ * @param flags update operation flags
+ * @param key tuple object pointer
+ * @param update update handler
+ * @returns 0 on success, -1 on error
+ */
+int tnt_update_tuple(struct tnt *t, int reqid, int ns, int flags,
+		     struct tnt_tuple *key, struct tnt_update *update);
+
+/**
+ * Update operation.
+ *
+ * If bufferization is in use, then request would be placed in
+ * internal buffer for later sending. Otherwise, operation
+ * would be processed immediately.
+ *
+ * @param t handler pointer
+ * @param reqid user supplied integer value
+ * @param ns namespace number
+ * @param flags update operation flags
+ * @param key key data
+ * @param key key data size
+ * @param update update handler
+ * @returns 0 on success, -1 on error
+ */
+int tnt_update(struct tnt *t, int reqid, int ns, int flags,
+	       char *key, int key_size, struct tnt_update *update);
+/** @} */
+
+#endif /* TNT_UPDATE_H_INCLUDED */
diff --git a/connector/c/tnt/include/tnt_update.h b/connector/c/tnt/include/tnt_update.h
index ae30e35576..a73690b045 100644
--- a/connector/c/tnt/include/tnt_update.h
+++ b/connector/c/tnt/include/tnt_update.h
@@ -32,6 +32,7 @@
 #define TNT_UPDATE_XOR    3
 #define TNT_UPDATE_OR     4
 #define TNT_UPDATE_SPLICE 5
+#define TNT_UPDATE_DELETE 6
 
 ssize_t
 tnt_update_arith(struct tnt_stream *s, uint32_t field,
@@ -46,6 +47,9 @@ tnt_update_splice(struct tnt_stream *s, uint32_t field,
 		  uint32_t offset,
 		  uint32_t length, char *data, size_t size);
 
+ssize_t
+tnt_update_delete(struct tnt_stream *s, uint32_t field);
+
 ssize_t
 tnt_update(struct tnt_stream *s, uint32_t ns, uint32_t flags,
 	   struct tnt_tuple *k,
diff --git a/connector/c/tnt/tnt_update.c b/connector/c/tnt/tnt_update.c
index 31bb70e4bf..5242ff5fe8 100644
--- a/connector/c/tnt/tnt_update.c
+++ b/connector/c/tnt/tnt_update.c
@@ -46,6 +46,7 @@ tnt_update_op(struct tnt_stream *s,
 	char enc[5];
 	tnt_enc_write(enc, size);
 	struct iovec iov[4];
+	int iovc = 3;
 	/* field */
 	iov[0].iov_base = &field;
 	iov[0].iov_len = 4;
@@ -56,9 +57,12 @@ tnt_update_op(struct tnt_stream *s,
 	iov[2].iov_base = enc;
 	iov[2].iov_len = encs;
 	/* data */
-	iov[3].iov_base = data;
-	iov[3].iov_len = size;
-	return s->writev(s, iov, 4);
+	if (data) {
+		iov[3].iov_base = data;
+		iov[3].iov_len = size;
+		iovc++;
+	}
+	return s->writev(s, iov, iovc);
 }
 
 /*
@@ -148,6 +152,22 @@ tnt_update_splice(struct tnt_stream *s, uint32_t field,
 	return rc;
 }
 
+/*
+ * tnt_update_delete()
+ *
+ * write update delete operation to buffer stream;
+ *
+ * s      - stream buffer pointer
+ * field  - field number
+ *
+ * returns number of bytes written, or -1 on error.
+*/
+ssize_t
+tnt_update_delete(struct tnt_stream *s, uint32_t field)
+{
+	return tnt_update_op(s, field, TNT_UPDATE_DELETE, NULL, 0);
+}
+
 struct tnt_header_update {
 	uint32_t ns;
 	uint32_t flags;
diff --git a/core/tarantool_lua.m b/core/tarantool_lua.m
index 04f6dcb3de..ccfaebdd59 100644
--- a/core/tarantool_lua.m
+++ b/core/tarantool_lua.m
@@ -94,8 +94,8 @@ static char format_to_opcode(char format)
 	case '=': return 0;
 	case '+': return 1;
 	case '&': return 2;
-	case '|': return 3;
-	case '^': return 4;
+	case '^': return 3;
+	case '|': return 4;
 	case ':': return 5;
 	default: return format;
 	}
diff --git a/include/errcode.h b/include/errcode.h
index e5ccf33e4c..8c7a22bf22 100644
--- a/include/errcode.h
+++ b/include/errcode.h
@@ -94,11 +94,11 @@ enum { TNT_ERRMSG_MAX = 512 };
 	/* 38 */_(ER_WRONG_VERSION,		2, "Unsupported version of protocol") \
 		/* end of silversearch error codes */					\
 	/* 39 */_(ER_WAL_IO,			2, "Failed to write to disk") \
-	/* 40 */_(ER_UNUSED40,			0, "Unused40") \
-	/* 41 */_(ER_UNUSED41,			0, "Unused41") \
-	/* 42 */_(ER_UNUSED42,			0, "Unused42") \
-	/* 43 */_(ER_UNUSED43,			0, "Unused43") \
-	/* 44 */_(ER_UNUSED44,			0, "Unused44") \
+	/* 40 */_(ER_FIELD_TYPE,		2, "Field type does not match one required by operation: expected a %s") \
+	/* 41 */_(ER_TYPE_MISMATCH,		2, "Argument type in operation does not match field type: expected a %s") \
+	/* 42 */_(ER_SPLICE,			2, "Field SPLICE error: %s") \
+	/* 43 */_(ER_TUPLE_IS_TOO_LONG,		2, "Tuple is too long %u") \
+	/* 44 */_(ER_UNKNOWN_UPDATE_OP,		2, "Unknown UPDATE operation") \
 	/* 45 */_(ER_UNUSED45,			0, "Unused45") \
 	/* 46 */_(ER_UNUSED46,			0, "Unused46") \
 	/* 47 */_(ER_UNUSED47,			0, "Unused47") \
diff --git a/include/util.h b/include/util.h
index 8e0aa7500d..91e710fcb7 100644
--- a/include/util.h
+++ b/include/util.h
@@ -31,8 +31,8 @@
 #include <inttypes.h>
 
 #ifndef MAX
-# define MAX(a, b) (a) > (b) ? (a) : (b)
-# define MIN(a, b) (a) < (b) ? (a) : (b)
+# define MAX(a, b) ((a) > (b) ? (a) : (b))
+# define MIN(a, b) ((a) < (b) ? (a) : (b))
 #endif
 
 /* Macros to define enum and corresponding strings. */
diff --git a/mod/box/box.h b/mod/box/box.h
index fcb031a079..4ec097ce71 100644
--- a/mod/box/box.h
+++ b/mod/box/box.h
@@ -39,6 +39,8 @@ enum
 {
 	BOX_INDEX_MAX = 10,
 	BOX_SPACE_MAX = 256,
+	/** A limit on how many operations a single UPDATE can have. */
+	BOX_UPDATE_OP_CNT_MAX = 128,
 };
 
 struct space {
@@ -147,6 +149,20 @@ struct box_txn {
 
 ENUM(messages, MESSAGES);
 
+/** UPDATE operation codes. */
+#define UPDATE_OP_CODES(_)			\
+	_(UPDATE_OP_SET, 0)			\
+	_(UPDATE_OP_ADD, 1)			\
+	_(UPDATE_OP_AND, 2)			\
+	_(UPDATE_OP_XOR, 3)			\
+	_(UPDATE_OP_OR, 4)			\
+	_(UPDATE_OP_SPLICE, 5)			\
+	_(UPDATE_OP_DELETE, 6)			\
+	_(UPDATE_OP_NONE, 7)			\
+	_(UPDATE_OP_MAX, 8)			\
+
+ENUM(update_op_codes, UPDATE_OP_CODES);
+
 extern iproto_callback rw_callback;
 
 /**
diff --git a/mod/box/box.m b/mod/box/box.m
index 708830bd68..5d2373f383 100644
--- a/mod/box/box.m
+++ b/mod/box/box.m
@@ -286,218 +286,584 @@ rollback_replace(struct box_txn *txn)
 	}
 }
 
-static void
-do_field_arith(u8 op, struct tbuf *field, void *arg, u32 arg_size)
+/** {{{ UPDATE request implementation.
+ * UPDATE request is represented by a sequence of operations,
+ * each working with a single field. However, there
+ * can be more than one operation on the same field.
+ * Supported operations are: SET, ADD, bitwise AND, XOR and OR,
+ * SPLICE and DELETE.
+ *
+ * To ensure minimal use of intermediate memory, UPDATE is
+ * performed in a streaming fashion: all operations in the request
+ * are sorted by field number. The resulting tuple length is
+ * calculated. A new tuple is allocated. Operation are applied
+ * sequentially, each copying data from the old tuple to the new
+ * data.
+ * With this approach we have in most cases linear (tuple length)
+ * UPDATE complexity and copy data from the old tuple to the new
+ * one only once.
+ *
+ * There are complications in this scheme caused by multiple
+ * operations on the same field: for example, we may have
+ * SET(4, "aaaaaa"), SPLICE(4, 0, 5, 0, ""), resulting in
+ * zero increase of total tuple length, but requiring an
+ * intermediate buffer to store SET results. Please
+ * read the source of do_update() to see how these
+ * complications  are worked around.
+ */
+
+/** Argument of SET operation. */
+struct op_set_arg {
+	u32 length;
+	void *value;
+};
+
+/** Argument of ADD, AND, XOR, OR operations. */
+struct op_arith_arg {
+	i32 i32_val;
+};
+
+/** Argument of SPLICE. */
+struct op_splice_arg {
+	i32 offset;	/** splice position */
+	i32 cut_length; /** cut this many bytes. */
+	void *paste;      /** paste what? */
+	i32 paste_length; /** paste this many bytes. */
+
+	/** Inferred data */
+	i32 tail_offset;
+	i32 tail_length;
+};
+
+union update_op_arg {
+	struct op_set_arg set;
+	struct op_arith_arg arith;
+	struct op_splice_arg splice;
+};
+
+struct update_cmd;
+struct update_field;
+struct update_op;
+
+typedef void (*init_op_func)(struct update_cmd *cmd,
+			     struct update_field *field,
+			     struct update_op *op);
+typedef void (*do_op_func)(union update_op_arg *arg, void *in, void *out);
+
+/** A set of functions and properties to initialize and do an op. */
+struct update_op_meta {
+	init_op_func init_op;
+	do_op_func do_op;
+	bool works_in_place;
+};
+
+/** A single UPDATE operation. */
+struct update_op {
+	struct update_op_meta *meta;
+	union update_op_arg arg;
+	u32 field_no;
+	u32 new_field_len;
+	u8 opcode;
+};
+
+/**
+ * We can have more than one operation on the same field.
+ * A descriptor of one changed field.
+ */
+struct update_field {
+	struct update_op *first; /** first operation on this field */
+	struct update_op *end;   /** points after last operation on
+				     this field. */
+	void *old;	         /** points at start of field *data* in old
+				     tuple. */
+	void *old_end;		 /** end of the old field. */
+	u32 new_len;             /** final length of the new field. */
+	u32 tail_len;		 /** copy old data after this field */
+	int tail_field_count;    /** how many fields we're copying. */
+};
+
+/** UPDATE command context. */
+struct update_cmd {
+	/** Search key */
+	void *key;
+	/** Search key cardinality */
+	u32 key_cardinality;
+	/** Operations. */
+	struct update_op *op;
+	struct update_op *op_end;
+	/* Distinct fields affected by UPDATE. */
+	struct update_field *field;
+	struct update_field *field_end;
+	/** new tuple length after all operations are applied. */
+	u32 new_tuple_len;
+	u32 new_field_count;
+};
+
+static int
+update_op_cmp(const void *op1_ptr, const void *op2_ptr)
 {
-	if (field->size != 4)
-		tnt_raise(IllegalParams, :"numeric operation on a field with length != 4");
+	const struct update_op *op1 = op1_ptr;
+	const struct update_op *op2 = op2_ptr;
 
-	if (arg_size != 4)
-		tnt_raise(IllegalParams, :"the argument of a numeric operation is not a 4-byte int");
+	if (op1->field_no < op2->field_no)
+		return -1;
+	if (op1->field_no > op2->field_no)
+		return 1;
 
-	switch (op) {
-	case 1:
-		*(i32 *)field->data += *(i32 *)arg;
-		break;
-	case 2:
-		*(u32 *)field->data &= *(u32 *)arg;
-		break;
-	case 3:
-		*(u32 *)field->data ^= *(u32 *)arg;
-		break;
-	case 4:
-		*(u32 *)field->data |= *(u32 *)arg;
-		break;
-	}
+	if (op1->arg.set.value < op2->arg.set.value)
+		return -1;
+	if (op1->arg.set.value > op2->arg.set.value)
+		return 1;
+	return 0;
 }
 
 static void
-do_field_splice(struct tbuf *field, void *args_data, u32 args_data_size)
+do_update_op_set(struct op_set_arg *arg, void *in __attribute__((unused)),
+		 void *out)
 {
-	struct tbuf args = {
-		.size = args_data_size,
-		.capacity = args_data_size,
-		.data = args_data,
-		.pool = NULL
-	};
-	struct tbuf *new_field = NULL;
-	void *offset_field, *length_field, *list_field;
-	u32 offset_size, length_size, list_size;
-	i32 offset, length;
-	u32 noffset, nlength;	/* normalized values */
-
-	new_field = tbuf_alloc(fiber->gc_pool);
-
-	offset_field = read_field(&args);
-	length_field = read_field(&args);
-	list_field = read_field(&args);
-	if (args.size != 0)
-		tnt_raise(IllegalParams, :"field splice: bad arguments");
-
-	offset_size = load_varint32(&offset_field);
-	if (offset_size == 0)
-		noffset = 0;
-	else if (offset_size == sizeof(offset)) {
-		offset = pick_u32(offset_field, &offset_field);
-		if (offset < 0) {
-			if (field->size < -offset)
-				tnt_raise(IllegalParams,
-					  :"field splice: offset is negative");
-			noffset = offset + field->size;
-		} else
-			noffset = offset;
-	} else
-		tnt_raise(IllegalParams, :"field splice: wrong size of offset");
-
-	if (noffset > field->size)
-		noffset = field->size;
-
-	length_size = load_varint32(&length_field);
-	if (length_size == 0)
-		nlength = field->size - noffset;
-	else if (length_size == sizeof(length)) {
-		if (offset_size == 0)
-			tnt_raise(IllegalParams,
-				  :"field splice: offset is empty but length is not");
-
-		length = pick_u32(length_field, &length_field);
-		if (length < 0) {
-			if ((field->size - noffset) < -length)
-				nlength = 0;
-			else
-				nlength = length + field->size - noffset;
-		} else
-			nlength = length;
-	} else
-		tnt_raise(IllegalParams, :"field splice: wrong size of length");
-
-	if (nlength > (field->size - noffset))
-		nlength = field->size - noffset;
-
-	list_size = load_varint32(&list_field);
-	if (list_size > 0 && length_size == 0)
-		tnt_raise(IllegalParams,
-			  :"field splice: length field is empty but list is not");
-	if (list_size > (UINT32_MAX - (field->size - nlength)))
-		tnt_raise(IllegalParams, :"field splice: list_size is too long");
-
-	say_debug("do_field_splice: noffset = %i, nlength = %i, list_size = %u",
-		  noffset, nlength, list_size);
-
-	new_field->size = 0;
-	tbuf_append(new_field, field->data, noffset);
-	tbuf_append(new_field, list_field, list_size);
-	tbuf_append(new_field, field->data + noffset + nlength, field->size - (noffset + nlength));
-
-	*field = *new_field;
+	memcpy(out, arg->value, arg->length);
 }
 
-static void __attribute__((noinline))
-prepare_update(struct box_txn *txn, struct tbuf *data)
+static void
+do_update_op_add(struct op_arith_arg *arg, void *in, void *out)
 {
-	struct tbuf **fields;
-	void *field;
-	int i;
-	void *key;
-	u32 op_cnt;
-	u32 tuples_affected = 1;
+	*(i32 *)out = *(i32 *)in + arg->i32_val;
+}
 
-	u32 key_len = read_u32(data);
-	if (key_len != 1)
-		tnt_raise(IllegalParams, :"key must be single valued");
+static void
+do_update_op_and(struct op_arith_arg *arg, void *in, void *out)
+{
+	*(i32 *)out = *(i32 *)in & arg->i32_val;
+}
 
-	key = read_field(data);
-	op_cnt = read_u32(data);
+static void
+do_update_op_xor(struct op_arith_arg *arg, void *in, void *out)
+{
+	*(i32 *)out = *(i32 *)in ^ arg->i32_val;
+}
 
-	if (op_cnt > 128)
-		tnt_raise(IllegalParams, :"too many operations for update");
-	if (op_cnt == 0)
-		tnt_raise(IllegalParams, :"no operations for update");
-	if (key == NULL)
-		tnt_raise(IllegalParams, :"invalid key");
+static void
+do_update_op_or(struct op_arith_arg *arg, void *in, void *out)
+{
+	*(i32 *)out = *(i32 *)in | arg->i32_val;
+}
 
-	txn->old_tuple = [txn->index find: key];
-	if (txn->old_tuple == NULL) {
-		txn->flags |= BOX_NOT_STORE;
+static void
+do_update_op_splice(struct op_splice_arg *arg, void *in, void *out)
+{
+	memcpy(out, in, arg->offset);           /* copy field head. */
+	out += arg->offset;
+	memcpy(out, arg->paste, arg->paste_length); /* copy the paste */
+	out += arg->paste_length;
+	memcpy(out, in + arg->tail_offset, arg->tail_length); /* copy tail */
+}
 
-		tuples_affected = 0;
+static void
+do_update_op_none(struct op_set_arg *arg, void *in, void *out)
+{
+	memcpy(out, in, arg->length);
+}
 
-		goto out;
+static void
+init_update_op_set(struct update_cmd *cmd __attribute__((unused)),
+		   struct update_field *field, struct update_op *op)
+{
+	/* Skip all previous ops. */
+	field->first = op;
+	op->new_field_len = op->arg.set.length;
+}
+
+static void
+init_update_op_arith(struct update_cmd *cmd __attribute__((unused)),
+		     struct update_field *field, struct update_op *op)
+{
+	/* Check the argument. */
+	if (field->new_len != sizeof(i32))
+		tnt_raise(ClientError, :ER_FIELD_TYPE, "32-bit int");
+
+	if (op->arg.set.length != sizeof(i32))
+		tnt_raise(ClientError, :ER_TYPE_MISMATCH, "32-bit int");
+	/* Parse the operands. */
+	op->arg.arith.i32_val = *(i32 *)op->arg.set.value;
+	op->new_field_len = sizeof(i32);
+}
+
+static void
+init_update_op_splice(struct update_cmd *cmd __attribute__((unused)),
+		      struct update_field *field, struct update_op *op)
+{
+	u32 field_len = field->new_len;
+	struct tbuf operands = {
+		.capacity = op->arg.set.length,
+		.size = op->arg.set.length,
+		.data = op->arg.set.value,
+		.pool = NULL
+	};
+	struct op_splice_arg *arg = &op->arg.splice;
+
+	/* Read the offset. */
+	void *offset_field = read_field(&operands);
+	u32 len = load_varint32(&offset_field);
+	if (len != sizeof(i32))
+		tnt_raise(IllegalParams, :"SPLICE offset");
+	/* Sic: overwrite of op->arg.set.length. */
+	arg->offset = *(i32 *)offset_field;
+	if (arg->offset < 0) {
+		if (-arg->offset > field_len)
+			tnt_raise(ClientError, :ER_SPLICE,
+				  "offset is out of bound");
+		arg->offset += field_len;
+	} else if (arg->offset > field_len) {
+		arg->offset = field_len;
+	}
+	assert(arg->offset >= 0 && arg->offset <= field_len);
+
+	/* Read the cut length. */
+	void *length_field = read_field(&operands);
+	len = load_varint32(&length_field);
+	if (len != sizeof(i32))
+		tnt_raise(IllegalParams, :"SPLICE length");
+	arg->cut_length = *(i32 *)length_field;
+	if (arg->cut_length < 0) {
+		if (-arg->cut_length > (field_len - arg->offset))
+			arg->cut_length = 0;
+		else
+			arg->cut_length += field_len - arg->offset;
+	} else if (arg->cut_length > field_len - arg->offset) {
+		arg->cut_length = field_len - arg->offset;
 	}
 
-	lock_tuple(txn, txn->old_tuple);
+	/* Read the paste. */
+	void *paste_field = read_field(&operands);
+	arg->paste_length = load_varint32(&paste_field);
+	arg->paste = paste_field;
 
-	fields = palloc(fiber->gc_pool, (txn->old_tuple->cardinality + 1) * sizeof(struct tbuf *));
-	memset(fields, 0, (txn->old_tuple->cardinality + 1) * sizeof(struct tbuf *));
+	/* Fill tail part */
+	arg->tail_offset = arg->offset + arg->cut_length;
+	arg->tail_length = field_len - arg->tail_offset;
 
-	for (i = 0, field = (uint8_t *)txn->old_tuple->data; i < txn->old_tuple->cardinality; i++) {
-		fields[i] = tbuf_alloc(fiber->gc_pool);
+	/* Check that the operands are fully read. */
+	if (operands.size != 0)
+		tnt_raise(IllegalParams, :"field splice format error");
 
-		u32 field_size = load_varint32(&field);
-		tbuf_append(fields[i], field, field_size);
-		field += field_size;
-	}
+	/* Record the new field length. */
+	op->new_field_len = arg->offset + arg->paste_length + arg->tail_length;
+}
 
-	while (op_cnt-- > 0) {
-		u8 op;
-		u32 field_no, arg_size;
-		void *arg;
+static void
+init_update_op_delete(struct update_cmd *cmd,
+		      struct update_field *field, struct update_op *op)
+{
+	/* Either this is the last op on this field  or next op is SET. */
+	if (op + 1 < cmd->op_end && op[1].field_no == op->field_no &&
+	    op[1].opcode != UPDATE_OP_SET && op[1].opcode != UPDATE_OP_DELETE)
+		tnt_raise(ClientError, :ER_NO_SUCH_FIELD, op->field_no);
+
+	/* Skip all ops on this field, including this one. */
+	field->first = op + 1;
+	op->new_field_len = 0;
+}
 
-		field_no = read_u32(data);
+static void
+init_update_op_none(struct update_cmd *cmd __attribute__((unused)),
+		    struct update_field *field, struct update_op *op)
+{
+	op->new_field_len = op->arg.set.length = field->new_len;
+}
 
-		if (field_no >= txn->old_tuple->cardinality)
-			tnt_raise(ClientError, :ER_NO_SUCH_FIELD, field_no);
+static void
+init_update_op_error(struct update_cmd *cmd __attribute__((unused)),
+		     struct update_field *field __attribute__((unused)),
+		     struct update_op *op __attribute__((unused)))
+{
+	tnt_raise(ClientError, :ER_UNKNOWN_UPDATE_OP);
+}
 
-		struct tbuf *sptr_field = fields[field_no];
+static struct update_op_meta update_op_meta[UPDATE_OP_MAX + 1] = {
+	{ init_update_op_set, (do_op_func) do_update_op_set, true },
+	{ init_update_op_arith, (do_op_func) do_update_op_add, true },
+	{ init_update_op_arith, (do_op_func) do_update_op_and, true },
+	{ init_update_op_arith, (do_op_func) do_update_op_xor, true },
+	{ init_update_op_arith, (do_op_func) do_update_op_or, true },
+	{ init_update_op_splice, (do_op_func) do_update_op_splice, false },
+	{ init_update_op_delete, (do_op_func) NULL, true },
+	{ init_update_op_none, (do_op_func) do_update_op_none, false },
+	{ init_update_op_error, (do_op_func) NULL, true }
+};
 
-		op = read_u8(data);
-		if (op > 5)
-			tnt_raise(IllegalParams, :"op is not 0, 1, 2, 3, 4 or 5");
+/**
+ * Initial parse of update command. Unpack and record
+ * update operations. Do not do too much, since the subject
+ * tuple may not exist.
+ */
+static struct update_cmd *
+parse_update_cmd(struct tbuf *data)
+{
+	struct update_cmd *cmd = palloc(fiber->gc_pool,
+					sizeof(struct update_cmd));
+	/* key cardinality */
+	cmd->key_cardinality = read_u32(data);
+	if (cmd->key_cardinality > 1)
+		tnt_raise(IllegalParams, :"key must be single valued");
+	if (cmd->key_cardinality == 0)
+		tnt_raise(IllegalParams, :"key is not defined");
+
+	/* key */
+	cmd->key = read_field(data);
+	/* number of operations */
+	u32 op_cnt = read_u32(data);
+	if (op_cnt > BOX_UPDATE_OP_CNT_MAX)
+		tnt_raise(IllegalParams, :"too many operations for update");
+	if (op_cnt == 0)
+		tnt_raise(IllegalParams, :"no operations for update");
+	/*
+	 * Read update operations. Allocate an extra dummy op to
+	 * optionally "apply" to the first field.
+	 */
+	struct update_op *op = palloc(fiber->gc_pool, (op_cnt + 1) *
+				      sizeof(struct update_op));
+	cmd->op = ++op; /* Skip the extra op for now. */
+	cmd->op_end = cmd->op + op_cnt;
+	for (; op < cmd->op_end; op++) {
+		/* read operation */
+		op->field_no = read_u32(data);
+		op->opcode = read_u8(data);
+		if (op->opcode > UPDATE_OP_MAX)
+			op->opcode = UPDATE_OP_MAX;
+		op->meta = &update_op_meta[op->opcode];
+		op->arg.set.value = read_field(data);
+		op->arg.set.length = load_varint32(&op->arg.set.value);
+	}
+	/* Check the remainder length, the request must be fully read. */
+	if (data->size != 0)
+		tnt_raise(IllegalParams, :"can't unpack request");
 
-		arg = read_field(data);
-		arg_size = load_varint32(&arg);
+	return cmd;
+}
 
-		if (op == 0) {
-			tbuf_ensure(sptr_field, arg_size);
-			sptr_field->size = arg_size;
-			memcpy(sptr_field->data, arg, arg_size);
-		} else {
-			switch (op) {
-			case 1:
-			case 2:
-			case 3:
-			case 4:
-				do_field_arith(op, sptr_field, arg, arg_size);
-				break;
-			case 5:
-				do_field_splice(sptr_field, arg, arg_size);
-				break;
+/**
+ * Skip fields unaffected by UPDATE.
+ * @return   length of skipped data
+ */
+static u32
+skip_fields(u32 field_count, void **data)
+{
+	void *begin = *data;
+	while (field_count-- > 0) {
+		u32 len = load_varint32(data);
+		*data += len;
+	}
+	return *data - begin;
+}
+
+static void
+update_field_init(struct update_cmd *cmd, struct update_field *field,
+		  struct update_op *op, void **old_data, int old_field_count)
+{
+	field->first = op;
+	if (op->field_no < old_field_count) {
+		/*
+		 * Find out the new field length and
+		 * shift the data pointer.
+		 */
+		field->new_len = load_varint32(old_data);
+		field->old = *old_data;
+		*old_data += field->new_len;
+		field->old_end = *old_data;
+	} else {
+		field->new_len = 0;
+		field->old = ""; /* Beyond old fields. */
+		field->old_end = field->old;
+		/*
+		 * Old tuple must have at least one field and we
+		 * always have an op on the first field.
+		 */
+		assert(op->field_no > 0 && op > cmd->op);
+	}
+}
+
+/**
+ * We found a tuple to do the update on. Prepare and optimize
+ * the operations.
+ */
+static void
+init_update_operations(struct box_txn *txn, struct update_cmd *cmd)
+{
+	/*
+	 * 1. Sort operations by field number and order within the
+	 * request.
+	 */
+	qsort(cmd->op, cmd->op_end - cmd->op, sizeof(struct update_op),
+	      update_op_cmp);
+	/*
+	 * 2. Take care of the old tuple head.
+	 */
+	if (cmd->op->field_no != 0) {
+		/*
+		 * We need to copy part of the old tuple to the
+		 * new one.
+		 * Prepend a no-op which copies the first field
+		 * intact. We may also make use of its tail_len
+		 * if next op field_no > 1.
+		 */
+		cmd->op--;
+		cmd->op->opcode = UPDATE_OP_NONE;
+		cmd->op->meta = &update_op_meta[UPDATE_OP_NONE];
+		cmd->op->field_no = 0;
+	}
+	/*
+	 * 3. Initialize and optimize the operations.
+	 */
+	assert(cmd->op < cmd->op_end); /* Ensured by parse_update_cmd(). */
+	cmd->field = palloc(fiber->gc_pool, (cmd->op_end - cmd->op) *
+			    sizeof(struct update_field));
+	struct update_op *op = cmd->op;
+	struct update_field *field = cmd->field;
+	void *old_data = txn->old_tuple->data;
+	int old_field_count = txn->old_tuple->cardinality;
+
+	update_field_init(cmd, field, op, &old_data, old_field_count);
+	do {
+		/*
+		 * Various checks for added fields:
+		 * 1) we can not do anything with a new field unless a
+		 *   previous field exists.
+		 * 2) we can not do any op except SET on a field
+		 *   which does not exist.
+		 */
+		if (op->field_no >= old_field_count &&
+		    /* check case 1. */
+		    (op->field_no > MAX(old_field_count,
+					op[-1].field_no + 1) ||
+		     /* check case 2. */
+		     (op->opcode != UPDATE_OP_SET &&
+		      op[-1].field_no != op->field_no))) {
+
+			tnt_raise(ClientError, :ER_NO_SUCH_FIELD, op->field_no);
+		}
+		op->meta->init_op(cmd, field, op);
+		field->new_len = op->new_field_len;
+
+		if (op + 1 >= cmd->op_end || op[1].field_no != op->field_no) {
+			/* Last op on this field. */
+			int skip_to = old_field_count;
+			if (op + 1 < cmd->op_end && op[1].field_no < old_field_count)
+				skip_to = op[1].field_no;
+			if (skip_to > op->field_no + 1) {
+				/* Jumping over a gap. */
+				field->tail_field_count = skip_to - op->field_no - 1;
+				field->tail_len =
+					skip_fields(field->tail_field_count,
+						    &old_data);
+			} else {
+				field->tail_len = 0;
+				field->tail_field_count = 0;
+			}
+			field->end = op + 1;
+			if (field->first < field->end) { /* Field is not deleted. */
+				cmd->new_tuple_len += varint32_sizeof(field->new_len);
+				cmd->new_tuple_len += field->new_len;
 			}
+			cmd->new_tuple_len += field->tail_len;
+			field++; /** Move to the next field. */
+			if (op + 1 < cmd->op_end) {
+				update_field_init(cmd, field, op + 1,
+						  &old_data, old_field_count);
+			}
+		}
+	} while (++op < cmd->op_end);
+
+	cmd->field_end = field;
+}
+
+static void
+do_update(struct box_txn *txn, struct update_cmd *cmd)
+{
+	void *new_data = txn->tuple->data;
+	void *new_data_end = new_data + txn->tuple->bsize;
+	struct update_field *field;
+	txn->tuple->cardinality = 0;
+
+	for (field = cmd->field; field < cmd->field_end; field++) {
+		if (field->first < field->end) { /* -> field is not deleted. */
+			new_data = save_varint32(new_data, field->new_len);
+			txn->tuple->cardinality++;
+		}
+		void *new_field = new_data;
+		void *old_field = field->old;
+
+		struct update_op *op;
+		for (op = field->first; op < field->end; op++) {
+			/*
+			 * Pre-allocate a temporary buffer when the
+			 * subject operation requires it, i.e.:
+			 * - op overwrites data while reading it thus
+			 *   can't work with in == out (SPLICE)
+			 * - op result doesn't fit into the new tuple
+			 *   (can happen when a big SET is then
+			 *   shrunk by a SPLICE).
+			 */
+			if ((old_field == new_field && !op->meta->works_in_place) ||
+			    /*
+			     * Sic: this predicate must function even if
+			     * new_field != new_data.
+			     */
+			    new_data + op->new_field_len > new_data_end) {
+				/*
+				 * Since we don't know which of the two
+				 * conditions above got us here, simply
+				 * palloc a *new* buffer of sufficient
+				 * size.
+			         */
+				new_field = palloc(fiber->gc_pool,
+						   op->new_field_len);
+			}
+			op->meta->do_op(&op->arg, old_field, new_field);
+			/* Next op uses previous op output as its input. */
+			old_field = new_field;
+		}
+		/*
+		 * Make sure op results end up in the tuple, copy
+		 * tail_len from the old tuple.
+		*/
+		if (new_field != new_data)
+			memcpy(new_data, new_field, field->new_len);
+		new_data += field->new_len;
+		if (field->tail_field_count) {
+			memcpy(new_data, field->old_end, field->tail_len);
+			new_data += field->tail_len;
+			txn->tuple->cardinality += field->tail_field_count;
 		}
 	}
+}
 
-	if (data->size != 0)
-		tnt_raise(IllegalParams, :"can't unpack request");
+static void __attribute__((noinline))
+prepare_update(struct box_txn *txn, struct tbuf *data)
+{
+	u32 tuples_affected = 1;
 
-	size_t bsize = 0;
-	for (int i = 0; i < txn->old_tuple->cardinality; i++)
-		bsize += fields[i]->size + varint32_sizeof(fields[i]->size );
-	txn->tuple = tuple_alloc(bsize);
-	tuple_txn_ref(txn, txn->tuple);
-	txn->tuple->cardinality = txn->old_tuple->cardinality;
+	/* Parse UPDATE request. */
+	struct update_cmd *cmd = parse_update_cmd(data);
+
+	/* Try to find the tuple. */
+	txn->old_tuple = [txn->index find: cmd->key];
+	if (txn->old_tuple == NULL) {
+		/* Not found. For simplicity, skip the logging. */
+		txn->flags |= BOX_NOT_STORE;
 
-	uint8_t *p = txn->tuple->data;
-	for (int i = 0; i < txn->old_tuple->cardinality; i++) {
-		p = save_varint32(p, fields[i]->size);
-		memcpy(p, fields[i]->data, fields[i]->size);
-		p += fields[i]->size;
+		tuples_affected = 0;
+
+		goto out;
 	}
 
+	init_update_operations(txn, cmd);
+	/* allocate new tuple */
+	txn->tuple = tuple_alloc(cmd->new_tuple_len);
+	tuple_txn_ref(txn, txn->tuple);
+	do_update(txn, cmd);
+	lock_tuple(txn, txn->old_tuple);
 	validate_indexes(txn);
 
-	if (data->size != 0)
-		tnt_raise(IllegalParams, :"can't unpack request");
-
 out:
 	txn->out->dup_u32(tuples_affected);
 
@@ -505,6 +871,8 @@ out:
 		txn->out->add_tuple(txn->tuple);
 }
 
+/** }}} */
+
 static void __attribute__((noinline))
 process_select(struct box_txn *txn, u32 limit, u32 offset, struct tbuf *data)
 {
@@ -1731,3 +2099,7 @@ mod_info(struct tbuf *out)
 		    recovery_state->recovery_last_update_tstamp);
 	tbuf_printf(out, "  status: %s" CRLF, status);
 }
+
+/**
+ * vim: foldmethod=marker
+ */
diff --git a/test/box/lua.result b/test/box/lua.result
index adbcc02553..500697e8a3 100644
--- a/test/box/lua.result
+++ b/test/box/lua.result
@@ -692,6 +692,14 @@ lua box.space[0]:update('test', ':p', 1, box.pack('ppp', 0, 2, 'every'))
 ---
  - 1953719668: {'every'}
 ...
+lua box.space[0]:update('test', ':p', 1, box.pack('ppp', 100, 2, 'every'))
+---
+ - 1953719668: {'everyevery'}
+...
+lua box.space[0]:update('test', ':p', 1, box.pack('ppp', -100, 2, 'every'))
+---
+error: 'Field SPLICE error: offset is out of bound'
+...
 lua box.space[0]:truncate()
 ---
 ...
diff --git a/test/box/lua.test b/test/box/lua.test
index 211e4679bf..350c34ed95 100644
--- a/test/box/lua.test
+++ b/test/box/lua.test
@@ -190,6 +190,9 @@ exec admin "lua box.fiber.find(920)"
 exec admin "lua box.space[0]:insert('test', 'something to splice')"
 exec admin "lua box.space[0]:update('test', ':p', 1, box.pack('ppp', 0, 4, 'no'))"
 exec admin "lua box.space[0]:update('test', ':p', 1, box.pack('ppp', 0, 2, 'every'))"
+# check an incorrect offset
+exec admin "lua box.space[0]:update('test', ':p', 1, box.pack('ppp', 100, 2, 'every'))"
+exec admin "lua box.space[0]:update('test', ':p', 1, box.pack('ppp', -100, 2, 'every'))"
 exec admin "lua box.space[0]:truncate()"
 exec admin "lua box.space[0]:insert('test', 'hello', 'october', '20th'):unpack()"
 exec admin "lua box.space[0]:truncate()"
diff --git a/test/box/sql.result b/test/box/sql.result
index c3472bdc3a..a42097abff 100644
--- a/test/box/sql.result
+++ b/test/box/sql.result
@@ -43,11 +43,16 @@ Update OK, 1 row affected
 select * from t0 where k0 = 1
 Found 1 tuple:
 [1, 'I am the newest tuple']
-update t0 set k1 = 'Huh', k2 = 'Oh-ho-ho' where k0=1
-An error occurred: ER_NO_SUCH_FIELD, 'Field 2 was not found in the tuple'
+update t0 set k1 = 'Huh', k2 = 'I am a new field! I was added via append' where k0=1
+Update OK, 1 row affected
 select * from t0 where k0 = 1
 Found 1 tuple:
-[1, 'I am the newest tuple']
+[1, 'Huh', 'I am a new field! I was added via append']
+update t0 set k1 = 'Huh', k1000 = 'invalid field' where k0=1
+An error occurred: ER_NO_SUCH_FIELD, 'Field 1000 was not found in the tuple'
+select * from t0 where k0 = 1
+Found 1 tuple:
+[1, 'Huh', 'I am a new field! I was added via append']
 insert into t0 values (1, 'I am a new tuple', 'stub')
 Insert OK, 1 row affected
 update t0 set k1 = 'Huh', k2 = 'Oh-ho-ho' where k0=1
diff --git a/test/box/sql.test b/test/box/sql.test
index 3b2282cdec..28eec05198 100644
--- a/test/box/sql.test
+++ b/test/box/sql.test
@@ -24,8 +24,11 @@ exec sql "insert into t0 values (1, 'I am a new tuple')"
 exec sql "select * from t0 where k0 = 1"
 exec sql "update t0 set k1 = 'I am the newest tuple' where k0=1"
 exec sql "select * from t0 where k0 = 1"
-# this is illegal, can't change tuple dimension with update
-exec sql "update t0 set k1 = 'Huh', k2 = 'Oh-ho-ho' where k0=1"
+# this is correct, can append field to tuple
+exec sql "update t0 set k1 = 'Huh', k2 = 'I am a new field! I was added via append' where k0=1"
+exec sql "select * from t0 where k0 = 1"
+# this is illegal
+exec sql "update t0 set k1 = 'Huh', k1000 = 'invalid field' where k0=1"
 exec sql "select * from t0 where k0 = 1"
 exec sql "insert into t0 values (1, 'I am a new tuple', 'stub')"
 exec sql "update t0 set k1 = 'Huh', k2 = 'Oh-ho-ho' where k0=1"
diff --git a/test/connector_c/CMakeLists.txt b/test/connector_c/CMakeLists.txt
index 91759d7325..cd27efddf2 100644
--- a/test/connector_c/CMakeLists.txt
+++ b/test/connector_c/CMakeLists.txt
@@ -1,2 +1,3 @@
 
 tarantool_client("tt" tt.c)
+tarantool_client("update" update.c)
diff --git a/test/connector_c/update.c b/test/connector_c/update.c
new file mode 100644
index 0000000000..2ced039588
--- /dev/null
+++ b/test/connector_c/update.c
@@ -0,0 +1,794 @@
+
+/*
+ * Copyright (C) 2011 Mail.RU
+ *
+ * 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 AUTHOR AND CONTRIBUTORS ``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 AUTHOR 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 <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <string.h>
+#include <limits.h>
+
+#include <connector/c/tnt/include/tnt.h>
+#include <connector/c/tntnet/include/tnt_net.h>
+#include <connector/c/tntnet/include/tnt_io.h>
+
+#include <util.h>
+#include <errcode.h>
+
+/*==========================================================================
+ * test variables
+ *==========================================================================*/
+
+/** tarantool connector instance */
+static struct tnt_stream *tnt;
+
+static char *long_string = "A long time ago, in a galaxy far, far away...\n"
+			   "It is a period of civil war. Rebel\n"
+			   "spaceships, striking from a hidden\n"
+			   "base, have won their first victory\n"
+			   "against the evil Galactic Empire.\n"
+			   "During the battle, Rebel spies managed\n"
+			   "to steal secret plans to the Empire's\n"
+			   "ultimate weapon, the Death Star, an\n"
+			   "armored space station with enough\n"
+			   "power to destroy an entire planet.\n"
+			   "Pursued by the Empire's sinister agents,\n"
+			   "Princess Leia races home aboard her\n"
+			   "starship, custodian of the stolen plans\n"
+			   "that can save her people and restore\n"
+			   "freedom to the galaxy....";
+
+/*==========================================================================
+ * function declaration
+ *==========================================================================*/
+
+/*--------------------------------------------------------------------------
+ * tarantool management functions
+ *--------------------------------------------------------------------------*/
+
+/** insert tuple */
+void
+insert_tuple(struct tnt_tuple *tuple);
+
+/** select tuple by key */
+void
+select_tuple(i32 key);
+
+/** update fields */
+void
+update(i32 key, struct tnt_stream *stream);
+
+/** add update fields operation: set int32 */
+void
+update_set_i32(struct tnt_stream *stream, i32 field, i32 value);
+
+/** add update fields operation: set string */
+void
+update_set_str(struct tnt_stream *stream, i32 field, char *str);
+
+/** add update fields operation: splice string */
+void
+update_splice_str(struct tnt_stream *stream, i32 field, i32 offset, i32 length, char *list);
+
+/** add update fields operation: delete field */
+void
+update_delete_field(struct tnt_stream *stream, i32 field);
+
+
+/** receive reply from server */
+void
+recv_command(char *command);
+
+/** print tuple */
+void
+print_tuple(struct tnt_tuple *tuple);
+
+/*--------------------------------------------------------------------------
+ * test suite functions
+ *--------------------------------------------------------------------------*/
+
+/** setup test suite */
+void
+test_suite_setup();
+
+/** clean-up test suite */
+void
+test_suite_tear_down();
+
+/** print error message and exit */
+void
+fail(char *msg);
+
+/** print tarantool error message and exit */
+void
+fail_tnt_error(char *msg, int error_code);
+
+/** print tarantool error message and exit */
+void
+fail_tnt_perror(char *msg);
+
+
+/*--------------------------------------------------------------------------
+ * test cases functions
+ *--------------------------------------------------------------------------*/
+
+/** update fields test case: simple set operation test */
+void
+test_simple_set();
+
+/** update fields test case: long set operation test */
+void
+test_long_set();
+
+/** update fields test case: append(set) operation test */
+void
+test_append();
+
+/** update fields test case: simple arithmetics operations test */
+void
+test_simple_arith();
+
+/** update fields test case: multi arithmetics operations test */
+void
+test_multi_arith();
+
+/** update fields test case: splice operations test */
+void
+test_splice();
+
+/** update fields test case: set and spice operations test */
+void
+test_set_and_splice();
+
+/** update fields test case: delete field operations test */
+void
+test_delete_field();
+
+
+/*==========================================================================
+ * function definition
+ *==========================================================================*/
+
+int
+main(void)
+{
+	/* initialize suite */
+	test_suite_setup();
+	/* run tests */
+	test_simple_set();
+	test_long_set();
+	test_append();
+	test_simple_arith();
+	test_multi_arith();
+	test_splice();
+	test_set_and_splice();
+	test_delete_field();
+	/* clean-up suite */
+	test_suite_tear_down();
+	return EXIT_SUCCESS;
+}
+
+
+/*--------------------------------------------------------------------------
+ * tarantool management functions
+ *--------------------------------------------------------------------------*/
+
+void
+insert_tuple(struct tnt_tuple *tuple)
+{
+	if (tnt_insert(tnt, 0, TNT_FLAG_RETURN, tuple) < 0)
+		fail_tnt_perror("tnt_insert");
+	if (tnt_flush(tnt) < 0)
+		fail_tnt_perror("tnt_flush");
+	recv_command("insert");
+}
+
+void
+select_tuple(i32 key)
+{
+	struct tnt_list tuple_list;
+	tnt_list_init(&tuple_list);
+	struct tnt_tuple *tuple = tnt_list_at(&tuple_list, NULL);
+	tnt_tuple(tuple, "%d", key);
+	if (tnt_select(tnt, 0, 0, 0, 1, &tuple_list) < 0)
+		fail_tnt_perror("tnt_select");
+	if (tnt_flush(tnt) < 0)
+		fail_tnt_perror("tnt_flush");
+	recv_command("select");
+	tnt_list_free(&tuple_list);
+}
+
+void
+update(i32 key, struct tnt_stream *stream)
+{
+	struct tnt_tuple *k = tnt_tuple(NULL, "%d", key);
+	if (tnt_update(tnt, 0, TNT_FLAG_RETURN, k, stream) < 0)
+		fail_tnt_perror("tnt_update");
+	if (tnt_flush(tnt) < 0)
+		fail_tnt_perror("tnt_flush");
+	tnt_tuple_free(k);
+	recv_command("update fields");
+}
+
+void
+update_set_i32(struct tnt_stream *stream, i32 field, i32 value)
+{
+	int result = tnt_update_assign(stream, field, (char *)&value, sizeof(value));
+	if (result < 0)
+		fail_tnt_error("tnt_update_assign", result);
+}
+
+void
+update_set_str(struct tnt_stream *stream, i32 field, char *str)
+{
+	int result = tnt_update_assign(stream, field, str, strlen(str));
+	if (result < 0)
+		fail_tnt_error("tnt_update_delete_field", result);
+}
+
+void
+update_splice_str(struct tnt_stream *stream, i32 field, i32 offset, i32 length, char *list)
+{
+	int result = tnt_update_splice(stream, field, offset, length, list, strlen(list));
+	if (result < 0)
+		fail_tnt_error("tnt_update_splice", result);
+}
+
+void
+update_delete_field(struct tnt_stream *stream, i32 field)
+{
+	int result = tnt_update_delete(stream, field);
+	if (result < 0)
+		fail_tnt_error("tnt_update_delete", result);
+}
+
+void
+recv_command(char *command)
+{
+	struct tnt_iter i;
+	tnt_iter_stream(&i, tnt);
+	while (tnt_next(&i)) {
+		struct tnt_reply *r = TNT_ISTREAM_REPLY(&i);
+		printf("%s: respond %s (op: %"PRIu32", reqid: %"PRIu32", code: %"PRIu32", count: %"PRIu32")\n",
+			command, tnt_strerror(tnt),
+			r->op,
+			r->reqid,
+			r->code,
+			r->count);
+		struct tnt_iter it;
+		tnt_iter_list(&it, TNT_REPLY_LIST(r));
+		while (tnt_next(&it)) {
+			struct tnt_tuple *tu = TNT_ILIST_TUPLE(&it);
+			print_tuple(tu);
+		}
+		tnt_iter_free(&it);
+	}
+	if (i.status == TNT_ITER_FAIL)
+		fail_tnt_perror("tnt_next");
+	tnt_iter_free(&i);
+}
+
+void
+print_tuple(struct tnt_tuple *tuple)
+{
+	bool is_first = true;
+	printf("(");
+
+	struct tnt_iter ifl;
+	tnt_iter(&ifl, tuple);
+	while (tnt_next(&ifl)) {
+		char *data = TNT_IFIELD_DATA(&ifl);
+		uint32_t size = TNT_IFIELD_SIZE(&ifl);
+		if (!is_first) {
+			printf(", ");
+		}
+		is_first = false;
+
+		switch(size) {
+		case 1:
+			printf("%"PRIi8" (0x%02"PRIx8")", *(i8 *)data, *(i8 *)data);
+			break;
+		case 2:
+			printf("%"PRIi16" (0x%04"PRIx16")", *(i16 *)data, *(i16 *)data);
+			break;
+		case 4:
+			printf("%"PRIi32" (0x%08"PRIx32")", *(i32 *)data, *(i32 *)data);
+			break;
+		case 8:
+			printf("%"PRIi64" (0x%016"PRIx64")", *(i64 *)data, *(i64 *)data);
+			break;
+		default:
+			printf("'%.*s'", size, data);
+			break;
+		}
+	}
+	if (ifl.status == TNT_ITER_FAIL)
+		fail("tuple parsing error");
+	tnt_iter_free(&ifl);
+	printf(")\n");
+}
+
+
+/*--------------------------------------------------------------------------
+ * test suite functions
+ *--------------------------------------------------------------------------*/
+
+void
+test_suite_setup()
+{
+	tnt = tnt_net(NULL);
+	if (tnt == NULL) {
+		fail("tnt_alloc");
+	}
+
+	tnt_set(tnt, TNT_OPT_HOSTNAME, "localhost");
+	tnt_set(tnt, TNT_OPT_PORT, 33013);
+
+	if (tnt_init(tnt) == -1)
+		fail_tnt_perror("tnt_init");
+	if (tnt_connect(tnt) == -1)
+		fail_tnt_perror("tnt_connect");
+}
+
+void
+test_suite_tear_down()
+{
+	tnt_stream_free(tnt);
+}
+
+void
+fail(char *msg)
+{
+	printf("fail: %s\n", msg);
+	exit(EXIT_FAILURE);
+}
+
+void
+fail_tnt_error(char *msg, int error_code)
+{
+	printf("fail: %s: %i\n", msg, error_code);
+	exit(EXIT_FAILURE);
+}
+
+void
+fail_tnt_perror(char *msg)
+{
+	printf("fail: %s: %s\n", msg, tnt_strerror(tnt));
+	exit(EXIT_FAILURE);
+}
+
+
+/*--------------------------------------------------------------------------
+ * test cases functions
+ *--------------------------------------------------------------------------*/
+
+void
+test_simple_set()
+{
+	printf(">>> test simple set\n");
+
+	/* insert tuple */
+	printf("# insert tuple\n");
+	struct tnt_tuple *tuple = tnt_tuple(NULL, "%d%d%d%s", 1, 2, 0, "");
+	insert_tuple(tuple);
+	tnt_tuple_free(tuple);
+
+	/* test simple set field */
+	struct tnt_stream *stream = tnt_buf(NULL);
+	printf("# test simple set field\n");
+	update_set_str(stream, 1, "new field value");
+	update_set_str(stream, 2, "");
+	update_set_str(stream, 3, "fLaC");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test useless set operations */
+	stream = tnt_buf(NULL);
+	printf("# set field\n");
+	update_set_str(stream, 1, "value?");
+	update_set_str(stream, 1, "very very very very very long field value?");
+	update_set_str(stream, 1, "field's new value");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	printf("<<< test simple set done\n");
+}
+
+void
+test_long_set()
+{
+	printf(">>> test long set\n");
+
+	/* insert tuple */
+	printf("# insert tuple\n");
+	struct tnt_tuple *tuple = tnt_tuple(NULL, "%d%s%s%s", 1, "first", "", "third");
+	insert_tuple(tuple);
+	tnt_tuple_free(tuple);
+
+	/* test set long value in empty field */
+	struct tnt_stream *stream = tnt_buf(NULL);
+	printf("# test set big value in empty field\n");
+	update_set_str(stream, 2, long_string);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test replace long value to short */
+	stream = tnt_buf(NULL);
+	printf("# test replace long value to short\n");
+	update_set_str(stream, 2, "short string");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	printf("<<< test long set done\n");
+}
+
+void
+test_append()
+{
+	printf(">>> test append\n");
+
+	/* insert tuple */
+	printf("# insert tuple\n");
+	struct tnt_tuple *tuple = tnt_tuple(NULL, "%d%s", 1, "first");
+	insert_tuple(tuple);
+	tnt_tuple_free(tuple);
+
+	/* test append field */
+	struct tnt_stream *stream = tnt_buf(NULL);
+	printf("# test append field\n");
+	update_set_str(stream, 2, "second");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test multi append field */
+	stream = tnt_buf(NULL);
+	printf("# test multi append\n");
+	update_set_str(stream, 3, "3");
+	update_set_str(stream, 3, "new field value");
+	update_set_str(stream, 3, "other new field value");
+	update_set_str(stream, 3, "third");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test append many field */
+	stream = tnt_buf(NULL);
+	printf("# test append many fields\n");
+	update_set_str(stream, 4, "fourth");
+	update_set_str(stream, 5, "fifth");
+	update_set_str(stream, 6, "sixth");
+	update_set_str(stream, 7, "seventh");
+	update_set_str(stream, 8, long_string);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test append and change field */
+	stream = tnt_buf(NULL);
+	printf("# test append and change field\n");
+	update_set_str(stream, 9, long_string);
+	update_splice_str(stream, 9, 1, 544, "ac");
+	tnt_update_arith(stream, 9, TNT_UPDATE_XOR, 0x3ffffff);
+	tnt_update_arith(stream, 9, TNT_UPDATE_ADD, 1024);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test set to not an exist field */
+	stream = tnt_buf(NULL);
+	printf("# test set to not an exist field\n");
+	update_set_str(stream, 0xDEADBEEF, "invalid!");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	printf("<<< test append done\n");
+}
+
+void
+test_simple_arith()
+{
+	printf(">>> test simple arith\n");
+
+	/* insert tuple */
+	printf("# insert tuple\n");
+	struct tnt_tuple *tuple = tnt_tuple(NULL, "%d%d%d%d", 1, 2, 0, 0);
+	insert_tuple(tuple);
+	tnt_tuple_free(tuple);
+
+	/* test simple add */
+	struct tnt_stream *stream = tnt_buf(NULL);
+	printf("# test simple add\n");
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 16);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test overflow add */
+	stream = tnt_buf(NULL);
+	printf("# test overflow add\n");
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, INT_MAX);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test overflow add */
+	stream = tnt_buf(NULL);
+	printf("# test underflow add\n");
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, INT_MIN);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test or */
+	stream = tnt_buf(NULL);
+	printf("# test simple or\n");
+	tnt_update_arith(stream, 2, TNT_UPDATE_OR, 0xbacf);
+	tnt_update_arith(stream, 3, TNT_UPDATE_OR, 0xfabc);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test xor */
+	stream = tnt_buf(NULL);
+	printf("# test simple xor\n");
+	tnt_update_arith(stream, 2, TNT_UPDATE_XOR, 0xffff);
+	tnt_update_arith(stream, 3, TNT_UPDATE_XOR, 0xffff);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test and */
+	stream = tnt_buf(NULL);
+	printf("# test simple and\n");
+	tnt_update_arith(stream, 2, TNT_UPDATE_AND, 0xf0f0);
+	tnt_update_arith(stream, 3, TNT_UPDATE_AND, 0x0f0f);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	printf("<<< test simple arith done\n");
+}
+
+void
+test_multi_arith()
+{
+	printf(">>> test multi splice\n");
+
+	/* insert tuple */
+	printf("# insert tuple\n");
+	struct tnt_tuple *tuple = tnt_tuple(NULL, "%d%s%d%s", 1, "first", 128, "third");
+	insert_tuple(tuple);
+	tnt_tuple_free(tuple);
+
+	/* test and */
+	struct tnt_stream *stream = tnt_buf(NULL);
+	printf("# test simple and\n");
+	update_set_i32(stream, 2, 0);
+	update_set_str(stream, 1, "first field new value");
+	tnt_update_arith(stream, 2, TNT_UPDATE_XOR, 0xF00F);
+	update_set_str(stream, 3, "third field new value");
+	tnt_update_arith(stream, 2, TNT_UPDATE_OR, 0xF00F);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	printf("<<< test multi arith done\n");
+}
+
+void
+test_splice()
+{
+	printf(">>> test simple splice\n");
+
+	/* insert tuple */
+	printf("# insert tuple\n");
+	struct tnt_tuple *tuple = tnt_tuple(NULL, "%d%s%s%s", 1, "first", "hi, this is a test string!", "third");
+	insert_tuple(tuple);
+	tnt_tuple_free(tuple);
+
+	/* test cut from begin */
+	struct tnt_stream *stream = tnt_buf(NULL);
+	printf("# test cut from begin\n");
+	update_splice_str(stream, 2, 0, 4, "");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test cut from middle */
+	stream = tnt_buf(NULL);
+	printf("# test cut from middle\n");
+	update_splice_str(stream, 2, 9, -8, "");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test cut from end */
+	stream = tnt_buf(NULL);
+	printf("# test cut from end\n");
+	update_splice_str(stream, 2, -1, 1, "");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test insert before begin */
+	stream = tnt_buf(NULL);
+	printf("# test insert before begin\n");
+	update_splice_str(stream, 2, 0, 0, "Bonjour, ");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test insert  */
+	stream = tnt_buf(NULL);
+	printf("# test insert after end\n");
+	update_splice_str(stream, 2, 10000, 0, " o_O!?");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test replace in begin */
+	stream = tnt_buf(NULL);
+	printf("# test replace in begin\n");
+	update_splice_str(stream, 2, 0, 7, "Hello");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test replace in middle */
+	stream = tnt_buf(NULL);
+	printf("# test replace in middle\n");
+	update_splice_str(stream, 2, 17, -6, "field");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test replace in end */
+	stream = tnt_buf(NULL);
+	printf("# test replace in end\n");
+	update_splice_str(stream, 2, -6, 4, "! Is this Sparta");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	printf("<<< test simple splice done\n");
+}
+
+void
+test_set_and_splice()
+{
+	printf(">>> test set and splice\n");
+
+	/* insert tuple */
+	printf("# insert tuple\n");
+	struct tnt_tuple *tuple = tnt_tuple(NULL, "%d%s%s%s", 1, "first", "hi, this is a test string!", "third");
+	insert_tuple(tuple);
+	tnt_tuple_free(tuple);
+
+	/* test set long string and splice to short */
+	struct tnt_stream *stream = tnt_buf(NULL);
+	printf("# test set long string and splice to short\n");
+	update_set_str(stream, 2, long_string);
+	update_splice_str(stream, 2, 45, 500, " away away away");
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test set short value and splice to long */
+	stream = tnt_buf(NULL);
+	printf("# test set short value and splice to long\n");
+	update_set_str(stream, 2, "test");
+	update_splice_str(stream, 2, -4, 4, long_string);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	printf("<<< test set and splice done\n");
+}
+
+/** update fields test case: delete field operations test */
+void
+test_delete_field()
+{
+	printf(">>> test delete field\n");
+
+	/* insert tuple */
+	printf("# insert tuple\n");
+	struct tnt_tuple *tuple = tnt_tuple(NULL, "%d%s%s%s%d%d%d%d%d%d%d%d%d%d", 1,
+			                    "first", "hi, this is a test string!", "third",
+					    1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+	insert_tuple(tuple);
+	tnt_tuple_free(tuple);
+
+	/* test simple delete fields */
+	struct tnt_stream *stream = tnt_buf(NULL);
+	printf("# test simple delete fields\n");
+	update_delete_field(stream, 2);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test useless operations with delete fields*/
+	stream = tnt_buf(NULL);
+	printf("# test useless operations with delete fields\n");
+	update_set_i32(stream, 1, 0);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	update_delete_field(stream, 1);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test multi delete fields */
+	stream = tnt_buf(NULL);
+	printf("# test multi delete fields\n");
+	update_delete_field(stream, 2);
+	update_delete_field(stream, 3);
+	update_delete_field(stream, 4);
+	update_delete_field(stream, 5);
+	update_delete_field(stream, 6);
+	update_delete_field(stream, 7);
+	update_delete_field(stream, 8);
+	update_delete_field(stream, 9);
+	update_delete_field(stream, 10);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test delete and set */
+	stream = tnt_buf(NULL);
+	printf("# test multi delete fields\n");
+	update_delete_field(stream, 1);
+	update_set_i32(stream, 1, 3);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	tnt_update_arith(stream, 1, TNT_UPDATE_ADD, 1);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test append and delete */
+	stream = tnt_buf(NULL);
+	printf("# test append and delete\n");
+	update_set_str(stream, 3, "second");
+	update_delete_field(stream, 3);
+	update_set_str(stream, 3, "third");
+	update_set_str(stream, 4, "third");
+	update_delete_field(stream, 4);
+	update_set_str(stream, 4, "third");
+	update_set_str(stream, 4, "fourth");
+	update_set_str(stream, 5, "fifth");
+	update_set_str(stream, 6, "sixth");
+	update_set_str(stream, 7, "seventh");
+	update_set_str(stream, 8, "eighth");
+	update_set_str(stream, 9, "ninth");
+	update_delete_field(stream, 7);
+	update_delete_field(stream, 6);
+	update(1, stream);
+	tnt_stream_free(stream);
+
+	/* test double delete */
+	stream = tnt_buf(NULL);
+	printf("# test double delete\n");
+	update_delete_field(stream, 3);
+	update_delete_field(stream, 3);
+	update(1, stream);
+	tnt_stream_free(stream);
+	select_tuple(1);
+
+	/* test delete not an exist field */
+	stream = tnt_buf(NULL);
+	printf("# test delete not an exist field\n");
+	update_delete_field(stream, 0xDEADBEEF);
+	update(1, stream);
+	tnt_stream_free(stream);
+	select_tuple(1);
+
+	printf("<<< test delete field done\n");
+}
diff --git a/test/connector_c/update.result b/test/connector_c/update.result
new file mode 100644
index 0000000000..23aaf2eb21
--- /dev/null
+++ b/test/connector_c/update.result
@@ -0,0 +1,197 @@
+>>> test simple set
+# insert tuple
+insert: respond ok (op: 13, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 2 (0x00000002), 0 (0x00000000), '')
+# test simple set field
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'new field value', '', 1130450022 (0x43614c66))
+# set field
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'field's new value', '', 1130450022 (0x43614c66))
+<<< test simple set done
+>>> test long set
+# insert tuple
+insert: respond ok (op: 13, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', '', 'third')
+# test set big value in empty field
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'A long time ago, in a galaxy far, far away...
+It is a period of civil war. Rebel
+spaceships, striking from a hidden
+base, have won their first victory
+against the evil Galactic Empire.
+During the battle, Rebel spies managed
+to steal secret plans to the Empire's
+ultimate weapon, the Death Star, an
+armored space station with enough
+power to destroy an entire planet.
+Pursued by the Empire's sinister agents,
+Princess Leia races home aboard her
+starship, custodian of the stolen plans
+that can save her people and restore
+freedom to the galaxy....', 'third')
+# test replace long value to short
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'short string', 'third')
+<<< test long set done
+>>> test append
+# insert tuple
+insert: respond ok (op: 13, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first')
+# test append field
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'second')
+# test multi append
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'second', 'third')
+# test append many fields
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'A long time ago, in a galaxy far, far away...
+It is a period of civil war. Rebel
+spaceships, striking from a hidden
+base, have won their first victory
+against the evil Galactic Empire.
+During the battle, Rebel spies managed
+to steal secret plans to the Empire's
+ultimate weapon, the Death Star, an
+armored space station with enough
+power to destroy an entire planet.
+Pursued by the Empire's sinister agents,
+Princess Leia races home aboard her
+starship, custodian of the stolen plans
+that can save her people and restore
+freedom to the galaxy....')
+# test append and change field
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'A long time ago, in a galaxy far, far away...
+It is a period of civil war. Rebel
+spaceships, striking from a hidden
+base, have won their first victory
+against the evil Galactic Empire.
+During the battle, Rebel spies managed
+to steal secret plans to the Empire's
+ultimate weapon, the Death Star, an
+armored space station with enough
+power to destroy an entire planet.
+Pursued by the Empire's sinister agents,
+Princess Leia races home aboard her
+starship, custodian of the stolen plans
+that can save her people and restore
+freedom to the galaxy....', 765239998 (0x2d9ca2be))
+# test set to not an exist field
+update fields: respond ok (op: 19, reqid: 0, code: 13826, count: 0)
+<<< test append done
+>>> test simple arith
+# insert tuple
+insert: respond ok (op: 13, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 2 (0x00000002), 0 (0x00000000), 0 (0x00000000))
+# test simple add
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 18 (0x00000012), 0 (0x00000000), 0 (0x00000000))
+# test overflow add
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), -2147483631 (0x80000011), 0 (0x00000000), 0 (0x00000000))
+# test underflow add
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 17 (0x00000011), 0 (0x00000000), 0 (0x00000000))
+# test simple or
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 17 (0x00000011), 47823 (0x0000bacf), 64188 (0x0000fabc))
+# test simple xor
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 17 (0x00000011), 17712 (0x00004530), 1347 (0x00000543))
+# test simple and
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 17 (0x00000011), 16432 (0x00004030), 1283 (0x00000503))
+<<< test simple arith done
+>>> test multi splice
+# insert tuple
+insert: respond ok (op: 13, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 128 (0x00000080), 'third')
+# test simple and
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first field new value', 61455 (0x0000f00f), 'third field new value')
+<<< test multi arith done
+>>> test simple splice
+# insert tuple
+insert: respond ok (op: 13, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'hi, this is a test string!', 'third')
+# test cut from begin
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'this is a test string!', 'third')
+# test cut from middle
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'this is a string!', 'third')
+# test cut from end
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'this is a string', 'third')
+# test insert before begin
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'Bonjour, this is a string', 'third')
+# test insert after end
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'Bonjour, this is a string o_O!?', 'third')
+# test replace in begin
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'Hello, this is a string o_O!?', 'third')
+# test replace in middle
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'Hello, this is a field o_O!?', 'third')
+# test replace in end
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'Hello, this is a field! Is this Sparta!?', 'third')
+<<< test simple splice done
+>>> test set and splice
+# insert tuple
+insert: respond ok (op: 13, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'hi, this is a test string!', 'third')
+# test set long string and splice to short
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'A long time ago, in a galaxy far, far away... away away away.', 'third')
+# test set short value and splice to long
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'A long time ago, in a galaxy far, far away...
+It is a period of civil war. Rebel
+spaceships, striking from a hidden
+base, have won their first victory
+against the evil Galactic Empire.
+During the battle, Rebel spies managed
+to steal secret plans to the Empire's
+ultimate weapon, the Death Star, an
+armored space station with enough
+power to destroy an entire planet.
+Pursued by the Empire's sinister agents,
+Princess Leia races home aboard her
+starship, custodian of the stolen plans
+that can save her people and restore
+freedom to the galaxy....', 'third')
+<<< test set and splice done
+>>> test delete field
+# insert tuple
+insert: respond ok (op: 13, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'hi, this is a test string!', 'third', 1 (0x00000001), 2 (0x00000002), 3 (0x00000003), 4 (0x00000004), 5 (0x00000005), 6 (0x00000006), 7 (0x00000007), 8 (0x00000008), 9 (0x00000009), 10 (0x0000000a))
+# test simple delete fields
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'first', 'third', 1 (0x00000001), 2 (0x00000002), 3 (0x00000003), 4 (0x00000004), 5 (0x00000005), 6 (0x00000006), 7 (0x00000007), 8 (0x00000008), 9 (0x00000009), 10 (0x0000000a))
+# test useless operations with delete fields
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'third', 1 (0x00000001), 2 (0x00000002), 3 (0x00000003), 4 (0x00000004), 5 (0x00000005), 6 (0x00000006), 7 (0x00000007), 8 (0x00000008), 9 (0x00000009), 10 (0x0000000a))
+# test multi delete fields
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 'third', 10 (0x0000000a))
+# test multi delete fields
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 9 (0x00000009), 10 (0x0000000a))
+# test append and delete
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 9 (0x00000009), 10 (0x0000000a), 'third', 'fourth', 'fifth', 'eighth', 'ninth')
+# test double delete
+update fields: respond ok (op: 19, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 9 (0x00000009), 10 (0x0000000a), 'fourth', 'fifth', 'eighth', 'ninth')
+select: respond ok (op: 17, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 9 (0x00000009), 10 (0x0000000a), 'fourth', 'fifth', 'eighth', 'ninth')
+# test delete not an exist field
+update fields: respond ok (op: 19, reqid: 0, code: 13826, count: 0)
+select: respond ok (op: 17, reqid: 0, code: 0, count: 1)
+(1 (0x00000001), 9 (0x00000009), 10 (0x0000000a), 'fourth', 'fifth', 'eighth', 'ninth')
+<<< test delete field done
diff --git a/test/connector_c/update.test b/test/connector_c/update.test
new file mode 100644
index 0000000000..75bf1f5986
--- /dev/null
+++ b/test/connector_c/update.test
@@ -0,0 +1,15 @@
+# encoding: tarantool
+import subprocess
+import sys
+import os
+
+p = subprocess.Popen([ os.path.join(builddir, "test/connector_c/update") ], stdout=subprocess.PIPE)
+p.wait()
+for line in p.stdout.readlines():
+      sys.stdout.write(line)
+
+# resore default suite
+server.stop()
+server.deploy(self.suite_ini["config"])
+server.start()
+# vim: syntax=python
-- 
GitLab