diff --git a/include/errcode.h b/include/errcode.h
index 72ae7d9420e31529cb52afaeedf0eb5df2566eea..b74a91522047eb31d80be45b33161c65c0cde4f0 100644
--- a/include/errcode.h
+++ b/include/errcode.h
@@ -101,14 +101,14 @@ enum { TNT_ERRMSG_MAX = 512 };
 	/* 46 */_(ER_UNUSED46,			2, "Unused46") \
 	/* 47 */_(ER_KEY_PART_COUNT,		2, "Key part count %d is greater than index part count %d") \
 	/* 48 */_(ER_PROC_RET,			2, "Return type '%s' is not supported in the binary protocol") \
-	/* 49 */_(ER_TUPLE_NOT_FOUND,		2, "Tuple doesn't exist") \
+	/* 49 */_(ER_TUPLE_NOT_FOUND,		2, "Tuple doesn't exist in index %d") \
 	/* 50 */_(ER_NO_SUCH_PROC,		2, "Procedure '%.*s' is not defined") \
 	/* 51 */_(ER_PROC_LUA,			2, "Lua error: %s") \
 	/* 52 */_(ER_SPACE_DISABLED,		2, "Space %u is disabled") \
 	/* 53 */_(ER_NO_SUCH_INDEX,		2, "No index #%u is defined in space %u") \
 	/* 54 */_(ER_NO_SUCH_FIELD,		2, "Field %u was not found in the tuple") \
-	/* 55 */_(ER_TUPLE_FOUND,		2, "Tuple already exists") \
-	/* 56 */_(ER_INDEX_VIOLATION,		2, "Duplicate key exists in unique index %d") \
+	/* 55 */_(ER_TUPLE_FOUND,		2, "Duplicate key exists in unique index %d") \
+	/* 56 */_(ER_UNUSED,		        2, "") \
 	/* 57 */_(ER_NO_SUCH_SPACE,		2, "Space %u does not exist")
 
 
diff --git a/include/errinj.h b/include/errinj.h
index 6276020c026e4e74abff46a1d1362e594adacace..c9e13c3ee758d659bc2144397ba4438e28f228cb 100644
--- a/include/errinj.h
+++ b/include/errinj.h
@@ -42,7 +42,8 @@ struct errinj {
 #define ERRINJ_LIST(_) \
 	_(ERRINJ_TESTING, false) \
 	_(ERRINJ_WAL_IO, false) \
-	_(ERRINJ_WAL_ROTATE, false)
+	_(ERRINJ_WAL_ROTATE, false) \
+	_(ERRINJ_INDEX_ALLOC, false)
 
 ENUM0(errinj_enum, ERRINJ_LIST);
 extern struct errinj errinjs[];
@@ -52,6 +53,7 @@ bool errinj_get(int id);
 void errinj_set(int id, bool state);
 int errinj_set_byname(char *name, bool state);
 
+struct tbuf;
 void errinj_info(struct tbuf *out);
 
 #ifdef NDEBUG
diff --git a/include/mhash.h b/include/mhash.h
index d7e97fa4020f88705a550999576f69367869044a..a1fa30916c7869d4d90aef399efd1c87eb5887e4 100644
--- a/include/mhash.h
+++ b/include/mhash.h
@@ -282,6 +282,37 @@ _mh(put)(struct _mh(t) *h, const mh_node_t *node,
 	return x;
 }
 
+
+/**
+ * Find a node in the hash and replace it with a new value.
+ * Save the old node in p_old pointer, if it is provided.
+ * If the old node didn't exist, just insert the new node.
+ */
+static inline mh_int_t
+_mh(replace)(struct _mh(t) *h, const mh_node_t *node, mh_node_t **p_old,
+	 mh_hash_arg_t hash_arg, mh_eq_arg_t eq_arg)
+{
+	mh_int_t k = _mh(get)(h, node, hash_arg, eq_arg);
+	if (k == mh_end(h)) {
+		/* No such node yet: insert a new one. */
+		if (p_old) {
+			*p_old = NULL;
+		}
+		return _mh(put)(h, node, hash_arg, eq_arg, NULL);
+	} else {
+		/*
+		 * Maintain uniqueness: replace the old node
+		 * with a new value.
+		 */
+		if (p_old) {
+			/* Save the old value. */
+			memcpy(*p_old, &(h->p[k]), sizeof(mh_node_t));
+		}
+		memcpy(&(h->p[k]), node, sizeof(mh_node_t));
+		return k;
+	}
+}
+
 static inline void
 _mh(del)(struct _mh(t) *h, mh_int_t x,
 	 mh_hash_arg_t hash_arg, mh_eq_arg_t eq_arg)
@@ -299,6 +330,15 @@ _mh(del)(struct _mh(t) *h, mh_int_t x,
 }
 #endif
 
+static inline void
+_mh(remove)(struct _mh(t) *h, const mh_node_t *node,
+	 mh_hash_arg_t hash_arg, mh_eq_arg_t eq_arg)
+{
+	mh_int_t k = _mh(get)(h, node, hash_arg, eq_arg);
+	if (k != mh_end(h))
+		_mh(del)(h, k, hash_arg, eq_arg);
+}
+
 
 #ifdef MH_SOURCE
 
diff --git a/src/box/box.m b/src/box/box.m
index 79e032367aac92728735e88a3d0a7ec435be4f4b..35a0d4427465daff402313e935fc88ec07f51a83 100644
--- a/src/box/box.m
+++ b/src/box/box.m
@@ -493,9 +493,6 @@ static void
 snapshot_write_tuple(struct log_io *l, struct fio_batch *batch,
 		     unsigned n, struct tuple *tuple)
 {
-	if (tuple->flags & GHOST)	// do not save fictive rows
-		return;
-
 	struct box_snap_row header;
 	header.space = n;
 	header.tuple_size = tuple->field_count;
diff --git a/src/box/box_lua.m b/src/box/box_lua.m
index 81b695171198f97c71a79841859dae0551e7131c..a792245eaec40fed0cb3d323f6c4d4b0d1b9160c 100644
--- a/src/box/box_lua.m
+++ b/src/box/box_lua.m
@@ -835,11 +835,9 @@ lbox_index_count(struct lua_State *L)
 	[index initIterator: it :ITER_EQ :key :key_part_count];
 	/* iterating over the index and counting tuples */
 	struct tuple *tuple;
-	while ((tuple = it->next(it)) != NULL) {
-		if (tuple->flags & GHOST)
-			continue;
+	while ((tuple = it->next(it)) != NULL)
 		count++;
-	}
+
 	/* returning subtree size */
 	lua_pushnumber(L, count);
 	return 1;
diff --git a/src/box/index.h b/src/box/index.h
index bf9697c48b3b98826d39af543a4588688ffa4d71..fb81e2e79fd5ecee675b77f03bb1c922221efb9b 100644
--- a/src/box/index.h
+++ b/src/box/index.h
@@ -123,6 +123,29 @@ struct index_traits
 	bool allows_partial_key;
 };
 
+/**
+ * The manner in which replace in a unique index must treat
+ * duplicates (tuples with the same value of indexed key),
+ * possibly present in the index.
+ */
+enum dup_replace_mode {
+        /**
+	 * If a duplicate is found, delete it and insert
+	 * a new tuple instead. Otherwise, insert a new tuple.
+         */
+	DUP_REPLACE_OR_INSERT,
+	/**
+	 * If a duplicate is found, produce an error.
+	 * I.e. require that no old key exists with the same
+	 * value.
+         */
+	DUP_INSERT,
+	/**
+	 * Unless a duplicate exists, throw an error.
+	 */
+	DUP_REPLACE
+};
+
 @interface Index: tnt_Object {
 	/* Index features. */
 	struct index_traits *traits;
@@ -172,9 +195,9 @@ struct index_traits
 - (struct tuple *) min;
 - (struct tuple *) max;
 - (struct tuple *) findByKey: (void *) key :(int) part_count;
-- (struct tuple *) findByTuple: (struct tuple *) tuple;
-- (void) remove: (struct tuple *) tuple;
-- (void) replace: (struct tuple *) old_tuple :(struct tuple *) new_tuple;
+- (struct tuple *) replace: (struct tuple *) old_tuple
+			  :(struct tuple *) new_tuple
+			  :(enum dup_replace_mode) mode;
 /**
  * Create a structure to represent an iterator. Must be
  * initialized separately.
@@ -184,10 +207,6 @@ struct index_traits
 		     :(enum iterator_type) type
 		     :(void *) key :(int) part_count;
 
-/**
- * Unsafe search methods that do not check key part count.
- */
-- (struct tuple *) findUnsafe: (void *) key :(int) part_count;
 @end
 
 struct iterator {
@@ -199,4 +218,9 @@ void
 check_key_parts(struct key_def *key_def, int part_count,
 		bool partial_key_allowed);
 
+uint32_t
+replace_check_dup(struct tuple *old_tuple,
+		  struct tuple *dup_tuple,
+		  enum dup_replace_mode mode);
+
 #endif /* TARANTOOL_BOX_INDEX_H_INCLUDED */
diff --git a/src/box/index.m b/src/box/index.m
index 3c01401ca30ce1d32003cf839525a4a61bed8763..8d02ed5238bb587b90199cbc769ebb8247c92f04 100644
--- a/src/box/index.m
+++ b/src/box/index.m
@@ -34,9 +34,10 @@
 #include "exception.h"
 #include "space.h"
 #include "assoc.h"
+#include "errinj.h"
 
 static struct index_traits index_traits = {
-	.allows_partial_key = true,
+	.allows_partial_key = false,
 };
 
 static struct index_traits hash_index_traits = {
@@ -60,6 +61,39 @@ check_key_parts(struct key_def *key_def,
 			  part_count, key_def->part_count);
 }
 
+/**
+ * Check if replacement of an old tuple with a new one is
+ * allowed.
+ */
+uint32_t
+replace_check_dup(struct tuple *old_tuple,
+		  struct tuple *dup_tuple,
+		  enum dup_replace_mode mode)
+{
+	if (dup_tuple == NULL) {
+		if (mode == DUP_REPLACE) {
+			/*
+			 * dup_replace_mode is DUP_REPLACE, and
+			 * a tuple with the same key is not found.
+			 */
+			return ER_TUPLE_NOT_FOUND;
+		}
+	} else { /* dup_tuple != NULL */
+		if (dup_tuple != old_tuple &&
+		    (old_tuple != NULL || mode == DUP_INSERT)) {
+			/*
+			 * There is a duplicate of new_tuple,
+			 * and it's not old_tuple: we can't
+			 * possibly delete more than one tuple
+			 * at once.
+			 */
+			return ER_TUPLE_FOUND;
+		}
+	}
+	return 0;
+}
+
+
 /* {{{ Index -- base class for all indexes. ********************/
 
 @interface HashIndex: Index
@@ -182,12 +216,6 @@ check_key_parts(struct key_def *key_def,
 }
 
 - (struct tuple *) findByKey: (void *) key :(int) part_count
-{
-	check_key_parts(key_def, part_count, false);
-	return [self findUnsafe: key :part_count];
-}
-
-- (struct tuple *) findUnsafe: (void *) key :(int) part_count
 {
 	(void) key;
 	(void) part_count;
@@ -195,25 +223,15 @@ check_key_parts(struct key_def *key_def,
 	return NULL;
 }
 
-- (struct tuple *) findByTuple: (struct tuple *) pattern
-{
-	(void) pattern;
-	[self subclassResponsibility: _cmd];
-	return NULL;
-}
-
-- (void) remove: (struct tuple *) tuple
-{
-	(void) tuple;
-	[self subclassResponsibility: _cmd];
-}
-
-- (void) replace: (struct tuple *) old_tuple
-	:(struct tuple *) new_tuple
+- (struct tuple *) replace: (struct tuple *) old_tuple
+			  : (struct tuple *) new_tuple
+			  : (enum dup_replace_mode) mode
 {
 	(void) old_tuple;
 	(void) new_tuple;
+	(void) mode;
 	[self subclassResponsibility: _cmd];
+	return NULL;
 }
 
 - (struct iterator *) allocIterator
@@ -353,7 +371,7 @@ hash_iterator_lstr_eq(struct iterator *it)
 
 - (void) buildNext: (struct tuple *)tuple
 {
-	[self replace: NULL :tuple];
+	[self replace: NULL :tuple :DUP_INSERT];
 }
 
 - (void) endBuild
@@ -377,7 +395,7 @@ hash_iterator_lstr_eq(struct iterator *it)
 	[pk initIterator: it :ITER_ALL :NULL :0];
 
 	while ((tuple = it->next(it)))
-	      [self replace: NULL :tuple];
+	      [self replace: NULL :tuple :DUP_INSERT];
 }
 
 - (void) free
@@ -397,31 +415,30 @@ hash_iterator_lstr_eq(struct iterator *it)
 	return NULL;
 }
 
-- (struct tuple *) findByTuple: (struct tuple *) tuple
-{
-	/* Hash index currently is always single-part. */
-	void *field = tuple_field(tuple, key_def->parts[0].fieldno);
-	if (field == NULL)
-		tnt_raise(ClientError, :ER_NO_SUCH_FIELD,
-			  key_def->parts[0].fieldno);
-	return [self findUnsafe :field :1];
-}
-
 @end
 
 /* }}} */
 
 /* {{{ Hash32Index ************************************************/
 
-static u32
-int32_key_to_value(void *key)
+static inline struct mh_i32ptr_node_t
+int32_key_to_node(void *key)
 {
 	u32 key_size = load_varint32(&key);
 	if (key_size != 4)
 		tnt_raise(ClientError, :ER_KEY_FIELD_TYPE, "u32");
-	return *((u32 *) key);
+	struct mh_i32ptr_node_t node = { .key = *(u32 *) key };
+	return node;
 }
 
+static inline struct mh_i32ptr_node_t
+int32_tuple_to_node(struct tuple *tuple, struct key_def *key_def)
+{
+	void *field = tuple_field(tuple, key_def->parts[0].fieldno);
+	struct mh_i32ptr_node_t node = int32_key_to_node(field);
+	node.val = tuple;
+	return node;
+}
 
 @implementation Hash32Index
 
@@ -450,65 +467,73 @@ int32_key_to_value(void *key)
 	return mh_size(int_hash);
 }
 
-- (struct tuple *) findUnsafe: (void *) key :(int) part_count
+- (struct tuple *) findByKey: (void *) key :(int) part_count
 {
+	assert(key_def->is_unique);
+	check_key_parts(key_def, part_count, false);
+
 	(void) part_count;
 
 	struct tuple *ret = NULL;
-	u32 num = int32_key_to_value(key);
-	const struct mh_i32ptr_node_t node = { .key = num };
+	struct mh_i32ptr_node_t node = int32_key_to_node(key);
 	mh_int_t k = mh_i32ptr_get(int_hash, &node, NULL, NULL);
 	if (k != mh_end(int_hash))
 		ret = mh_i32ptr_node(int_hash, k)->val;
 #ifdef DEBUG
-	say_debug("Hash32Index find(self:%p, key:%i) = %p", self, num, ret);
+	say_debug("Hash32Index find(self:%p, key:%i) = %p", self, node.key, ret);
 #endif
 	return ret;
 }
 
-- (void) remove: (struct tuple *) tuple
+- (struct tuple *) replace: (struct tuple *) old_tuple
+			  :(struct tuple *) new_tuple
+			  :(enum dup_replace_mode) mode
 {
-	void *field = tuple_field(tuple, key_def->parts[0].fieldno);
-	u32 num = int32_key_to_value(field);
-	const struct mh_i32ptr_node_t node = { .key = num };
-	mh_int_t k = mh_i32ptr_get(int_hash, &node, NULL, NULL);
-	if (k != mh_end(int_hash))
-		mh_i32ptr_del(int_hash, k, NULL, NULL);
-#ifdef DEBUG
-	say_debug("Hash32Index remove(self:%p, key:%i)", self, num);
-#endif
-}
+	struct mh_i32ptr_node_t new_node, old_node;
+	uint32_t errcode;
 
-- (void) replace: (struct tuple *) old_tuple
-	:(struct tuple *) new_tuple
-{
-	void *field = tuple_field(new_tuple, key_def->parts[0].fieldno);
-	u32 num = int32_key_to_value(field);
+	if (new_tuple) {
+		struct mh_i32ptr_node_t *dup_node = &old_node;
+		new_node = int32_tuple_to_node(new_tuple, key_def);
+		mh_int_t pos = mh_i32ptr_replace(int_hash, &new_node,
+						 &dup_node, NULL, NULL);
 
-	if (old_tuple != NULL) {
-		void *old_field = tuple_field(old_tuple,
-					      key_def->parts[0].fieldno);
-		load_varint32(&old_field);
-		u32 old_num = *(u32 *)old_field;
-		const struct mh_i32ptr_node_t node = { .key = old_num };
-		mh_int_t k = mh_i32ptr_get(int_hash, &node, NULL, NULL);
-		if (k != mh_end(int_hash))
-			mh_i32ptr_del(int_hash, k, NULL, NULL);
-	}
+		ERROR_INJECT(ERRINJ_INDEX_ALLOC,
+		{
+			mh_i32ptr_del(int_hash, pos, NULL, NULL);
+			pos = mh_end(int_hash);
+		});
 
-	const struct mh_i32ptr_node_t node = { .key = num, .val = new_tuple };
-	mh_int_t pos = mh_i32ptr_put(int_hash, &node, NULL, NULL, NULL);
-
-	if (pos == mh_end(int_hash))
-		tnt_raise(LoggedError, :ER_MEMORY_ISSUE, (ssize_t) pos,
-			  "int hash", "key");
-
-#ifdef DEBUG
-	say_debug("Hash32Index replace(self:%p, old_tuple:%p, new_tuple:%p) key:%i",
-		  self, old_tuple, new_tuple, num);
-#endif
+		if (pos == mh_end(int_hash)) {
+			tnt_raise(LoggedError, :ER_MEMORY_ISSUE, (ssize_t) pos,
+				  "int hash", "key");
+		}
+		struct tuple *dup_tuple = dup_node ? dup_node->val : NULL;
+		errcode = replace_check_dup(old_tuple, dup_tuple, mode);
+
+		if (errcode) {
+			mh_i32ptr_remove(int_hash, &new_node, NULL, NULL);
+			if (dup_node) {
+				pos = mh_i32ptr_replace(int_hash, dup_node,
+							NULL, NULL, NULL);
+				if (pos == mh_end(int_hash)) {
+					panic("Failed to allocate memory in "
+					      "recover of int hash");
+				}
+			}
+			tnt_raise(ClientError, :errcode, index_n(self));
+		}
+		if (dup_tuple)
+			return dup_tuple;
+	}
+	if (old_tuple) {
+		old_node = int32_tuple_to_node(old_tuple, key_def);
+		mh_i32ptr_remove(int_hash, &old_node, NULL, NULL);
+	}
+	return old_tuple;
 }
 
+
 - (struct iterator *) allocIterator
 {
 	struct hash_i32_iterator *it = malloc(sizeof(struct hash_i32_iterator));
@@ -523,17 +548,16 @@ int32_key_to_value(void *key)
 - (void) initIterator: (struct iterator *) ptr: (enum iterator_type) type
                         :(void *) key :(int) part_count
 {
-	(void) part_count;
 	assert(ptr->free == hash_iterator_free);
 	struct hash_i32_iterator *it = (struct hash_i32_iterator *) ptr;
+	struct mh_i32ptr_node_t node;
 
 	switch (type) {
 	case ITER_GE:
 		if (key != NULL) {
 			check_key_parts(key_def, part_count,
 					traits->allows_partial_key);
-			u32 num = int32_key_to_value(key);
-			const struct mh_i32ptr_node_t node = { .key = num };
+			node = int32_key_to_node(key);
 			it->h_pos = mh_i32ptr_get(int_hash, &node, NULL, NULL);
 			it->base.next = hash_iterator_i32_ge;
 			break;
@@ -546,8 +570,7 @@ int32_key_to_value(void *key)
 	case ITER_EQ:
 		check_key_parts(key_def, part_count,
 				traits->allows_partial_key);
-		u32 num = int32_key_to_value(key);
-		const struct mh_i32ptr_node_t node = { .key = num };
+		node = int32_key_to_node(key);
 		it->h_pos = mh_i32ptr_get(int_hash, &node, NULL, NULL);
 		it->base.next = hash_iterator_i32_eq;
 		break;
@@ -563,13 +586,23 @@ int32_key_to_value(void *key)
 
 /* {{{ Hash64Index ************************************************/
 
-static u64
-int64_key_to_value(void *key)
+static inline struct mh_i64ptr_node_t
+int64_key_to_node(void *key)
 {
 	u32 key_size = load_varint32(&key);
 	if (key_size != 8)
 		tnt_raise(ClientError, :ER_KEY_FIELD_TYPE, "u64");
-	return *((u64 *) key);
+	struct mh_i64ptr_node_t node = { .key = *(u64 *) key };
+	return node;
+}
+
+static inline struct mh_i64ptr_node_t
+int64_tuple_to_node(struct tuple *tuple, struct key_def *key_def)
+{
+	void *field = tuple_field(tuple, key_def->parts[0].fieldno);
+	struct mh_i64ptr_node_t node = int64_key_to_node(field);
+	node.val = tuple;
+	return node;
 }
 
 @implementation Hash64Index
@@ -598,64 +631,70 @@ int64_key_to_value(void *key)
 	return mh_size(int64_hash);
 }
 
-- (struct tuple *) findUnsafe: (void *) key :(int) part_count
+- (struct tuple *) findByKey: (void *) key :(int) part_count
 {
-	(void) part_count;
+	assert(key_def->is_unique);
+	check_key_parts(key_def, part_count, false);
 
 	struct tuple *ret = NULL;
-	u64 num = int64_key_to_value(key);
-	const struct mh_i64ptr_node_t node = { .key = num };
+	struct mh_i64ptr_node_t node = int64_key_to_node(key);
 	mh_int_t k = mh_i64ptr_get(int64_hash, &node, NULL, NULL);
 	if (k != mh_end(int64_hash))
 		ret = mh_i64ptr_node(int64_hash, k)->val;
 #ifdef DEBUG
-	say_debug("Hash64Index find(self:%p, key:%"PRIu64") = %p", self, num, ret);
+	say_debug("Hash64Index find(self:%p, key:%"PRIu64") = %p", self, node.key, ret);
 #endif
 	return ret;
 }
 
-- (void) remove: (struct tuple *) tuple
+- (struct tuple *) replace: (struct tuple *) old_tuple
+			  :(struct tuple *) new_tuple
+			  :(enum dup_replace_mode) mode
 {
-	void *field = tuple_field(tuple, key_def->parts[0].fieldno);
-	u64 num = int64_key_to_value(field);
+	struct mh_i64ptr_node_t new_node, old_node;
+	uint32_t errcode;
 
-	const struct mh_i64ptr_node_t node = { .key = num };
-	mh_int_t k = mh_i64ptr_get(int64_hash, &node, NULL, NULL);
-	if (k != mh_end(int64_hash))
-		mh_i64ptr_del(int64_hash, k, NULL, NULL);
-#ifdef DEBUG
-	say_debug("Hash64Index remove(self:%p, key:%"PRIu64")", self, num);
-#endif
-}
+	if (new_tuple) {
+		struct mh_i64ptr_node_t *dup_node = &old_node;
+		new_node = int64_tuple_to_node(new_tuple, key_def);
+		mh_int_t pos = mh_i64ptr_replace(int64_hash, &new_node,
+						 &dup_node, NULL, NULL);
 
-- (void) replace: (struct tuple *) old_tuple
-	:(struct tuple *) new_tuple
-{
-	void *field = tuple_field(new_tuple, key_def->parts[0].fieldno);
-	u64 num = int64_key_to_value(field);
-
-	if (old_tuple != NULL) {
-		void *old_field = tuple_field(old_tuple,
-					      key_def->parts[0].fieldno);
-		load_varint32(&old_field);
-		u64 old_num = *(u64 *)old_field;
-		const struct mh_i64ptr_node_t node = { .key = old_num };
-		mh_int_t k = mh_i64ptr_get(int64_hash, &node, NULL, NULL);
-		if (k != mh_end(int64_hash))
-			mh_i64ptr_del(int64_hash, k, NULL, NULL);
+		ERROR_INJECT(ERRINJ_INDEX_ALLOC,
+		{
+			mh_i64ptr_del(int64_hash, pos, NULL, NULL);
+			pos = mh_end(int64_hash);
+		});
+		if (pos == mh_end(int64_hash)) {
+			tnt_raise(LoggedError, :ER_MEMORY_ISSUE, (ssize_t) pos,
+				  "int64 hash", "key");
+		}
+		struct tuple *dup_tuple = dup_node ? dup_node->val : NULL;
+		errcode = replace_check_dup(old_tuple, dup_tuple, mode);
+
+		if (errcode) {
+			mh_i64ptr_remove(int64_hash, &new_node, NULL, NULL);
+			if (dup_node) {
+				pos = mh_i64ptr_replace(int64_hash, dup_node,
+							NULL, NULL, NULL);
+				if (pos == mh_end(int64_hash)) {
+					panic("Failed to allocate memory in "
+					      "recover of int64 hash");
+				}
+			}
+			tnt_raise(ClientError, :errcode, index_n(self));
+		}
+		if (dup_tuple)
+			return dup_tuple;
 	}
-
-	const struct mh_i64ptr_node_t node = { .key = num, .val = new_tuple };
-	mh_int_t pos = mh_i64ptr_put(int64_hash, &node, NULL, NULL, NULL);
-	if (pos == mh_end(int64_hash))
-		tnt_raise(LoggedError, :ER_MEMORY_ISSUE, (ssize_t) pos,
-			  "int64 hash", "key");
-#ifdef DEBUG
-	say_debug("Hash64Index replace(self:%p, old_tuple:%p, tuple:%p) key:%"PRIu64,
-		  self, old_tuple, new_tuple, num);
-#endif
+	if (old_tuple) {
+		old_node = int64_tuple_to_node(old_tuple, key_def);
+		mh_i64ptr_remove(int64_hash, &old_node, NULL, NULL);
+	}
+	return old_tuple;
 }
 
+
 - (struct iterator *) allocIterator
 {
 	struct hash_i64_iterator *it = malloc(sizeof(struct hash_i64_iterator));
@@ -674,14 +713,14 @@ int64_key_to_value(void *key)
 	(void) part_count;
 	assert(ptr->free == hash_iterator_free);
 	struct hash_i64_iterator *it = (struct hash_i64_iterator *) ptr;
+	struct mh_i64ptr_node_t node;
 
 	switch (type) {
 	case ITER_GE:
 		if (key != NULL) {
 			check_key_parts(key_def, part_count,
 					traits->allows_partial_key);
-			u64 num = int64_key_to_value(key);
-			const struct mh_i64ptr_node_t node = { .key = num };
+			node = int64_key_to_node(key);
 			it->h_pos = mh_i64ptr_get(int64_hash, &node, NULL, NULL);
 			it->base.next = hash_iterator_i64_ge;
 			break;
@@ -694,8 +733,7 @@ int64_key_to_value(void *key)
 	case ITER_EQ:
 		check_key_parts(key_def, part_count,
 				traits->allows_partial_key);
-		u64 num = int64_key_to_value(key);
-		const struct mh_i64ptr_node_t node = { .key = num };
+		node = int64_key_to_node(key);
 		it->h_pos = mh_i64ptr_get(int64_hash, &node, NULL, NULL);
 		it->base.next = hash_iterator_i64_eq;
 		break;
@@ -711,6 +749,19 @@ int64_key_to_value(void *key)
 
 /* {{{ HashStrIndex ***********************************************/
 
+static inline struct mh_lstrptr_node_t
+lstrptr_tuple_to_node(struct tuple *tuple, struct key_def *key_def)
+{
+	void *field = tuple_field(tuple, key_def->parts[0].fieldno);
+	if (field == NULL)
+		tnt_raise(ClientError, :ER_NO_SUCH_FIELD,
+			  key_def->parts[0].fieldno);
+
+	struct mh_lstrptr_node_t node = { .key = field, .val = tuple };
+	return node;
+}
+
+
 @implementation HashStrIndex
 - (void) reserve: (u32) n_tuples
 {
@@ -737,9 +788,11 @@ int64_key_to_value(void *key)
 	return mh_size(str_hash);
 }
 
-- (struct tuple *) findUnsafe: (void *) key :(int) part_count
+- (struct tuple *) findByKey: (void *) key :(int) part_count
 {
-	(void) part_count;
+	assert(key_def->is_unique);
+	check_key_parts(key_def, part_count, false);
+
 	struct tuple *ret = NULL;
 	const struct mh_lstrptr_node_t node = { .key = key };
 	mh_int_t k = mh_lstrptr_get(str_hash, &node, NULL, NULL);
@@ -753,49 +806,52 @@ int64_key_to_value(void *key)
 	return ret;
 }
 
-- (void) remove: (struct tuple *) tuple
+- (struct tuple *) replace: (struct tuple *) old_tuple
+			  :(struct tuple *) new_tuple
+			  :(enum dup_replace_mode) mode
 {
-	void *field = tuple_field(tuple, key_def->parts[0].fieldno);
-
-	const struct mh_lstrptr_node_t node = { .key = field };
-	mh_int_t k = mh_lstrptr_get(str_hash, &node, NULL, NULL);
-	if (k != mh_end(str_hash))
-		mh_lstrptr_del(str_hash, k, NULL, NULL);
-#ifdef DEBUG
-	u32 field_size = load_varint32(&field);
-	say_debug("HashStrIndex remove(self:%p, key:'%.*s')",
-		  self, field_size, (u8 *)field);
-#endif
-}
+	struct mh_lstrptr_node_t new_node, old_node;
+	uint32_t errcode;
 
-- (void) replace: (struct tuple *) old_tuple
-	:(struct tuple *) new_tuple
-{
-	void *field = tuple_field(new_tuple, key_def->parts[0].fieldno);
+	if (new_tuple) {
+		struct mh_lstrptr_node_t *dup_node = &old_node;
+		new_node = lstrptr_tuple_to_node(new_tuple, key_def);
+		mh_int_t pos = mh_lstrptr_replace(str_hash, &new_node,
+						  &dup_node, NULL, NULL);
 
-	if (field == NULL)
-		tnt_raise(ClientError, :ER_NO_SUCH_FIELD,
-			  key_def->parts[0].fieldno);
+		ERROR_INJECT(ERRINJ_INDEX_ALLOC,
+		{
+			mh_lstrptr_del(str_hash, pos, NULL, NULL);
+			pos = mh_end(str_hash);
+		});
 
-	if (old_tuple != NULL) {
-		void *old_field = tuple_field(old_tuple,
-					      key_def->parts[0].fieldno);
-		const struct mh_lstrptr_node_t node = { .key = old_field };
-		mh_int_t k = mh_lstrptr_get(str_hash, &node, NULL, NULL);
-		if (k != mh_end(str_hash))
-			mh_lstrptr_del(str_hash, k, NULL, NULL);
+		if (pos == mh_end(str_hash)) {
+			tnt_raise(LoggedError, :ER_MEMORY_ISSUE, (ssize_t) pos,
+				  "str hash", "key");
+		}
+		struct tuple *dup_tuple = dup_node ? dup_node->val : NULL;
+		errcode = replace_check_dup(old_tuple, dup_tuple, mode);
+
+		if (errcode) {
+			mh_lstrptr_remove(str_hash, &new_node, NULL, NULL);
+			if (dup_node) {
+				pos = mh_lstrptr_replace(str_hash, dup_node,
+							 NULL, NULL, NULL);
+				if (pos == mh_end(str_hash)) {
+					panic("Failed to allocate memory in "
+					      "recover of str hash");
+				}
+			}
+			tnt_raise(ClientError, :errcode, index_n(self));
+		}
+		if (dup_tuple)
+			return dup_tuple;
 	}
-
-	const struct mh_lstrptr_node_t node = { .key = field, .val = new_tuple};
-	mh_int_t pos = mh_lstrptr_put(str_hash, &node, NULL, NULL, NULL);
-	if (pos == mh_end(str_hash))
-		tnt_raise(LoggedError, :ER_MEMORY_ISSUE, (ssize_t) pos,
-			  "str hash", "key");
-#ifdef DEBUG
-	u32 field_size = load_varint32(&field);
-	say_debug("HashStrIndex replace(self:%p, old_tuple:%p, tuple:%p) key:'%.*s'",
-		  self, old_tuple, new_tuple, field_size, (u8 *)field);
-#endif
+	if (old_tuple) {
+		old_node = lstrptr_tuple_to_node(old_tuple, key_def);
+		mh_lstrptr_remove(str_hash, &old_node, NULL, NULL);
+	}
+	return old_tuple;
 }
 
 - (struct iterator *) allocIterator
@@ -818,13 +874,14 @@ int64_key_to_value(void *key)
 
 	assert(ptr->free == hash_iterator_free);
 	struct hash_lstr_iterator *it = (struct hash_lstr_iterator *) ptr;
+	struct mh_lstrptr_node_t node;
 
 	switch (type) {
 	case ITER_GE:
 		if (key != NULL) {
 			check_key_parts(key_def, part_count,
 					traits->allows_partial_key);
-			const struct mh_lstrptr_node_t node = { .key = key };
+			node.key = key;
 			it->h_pos = mh_lstrptr_get(str_hash, &node, NULL, NULL);
 			it->base.next = hash_iterator_lstr_ge;
 			break;
@@ -837,7 +894,7 @@ int64_key_to_value(void *key)
 	case ITER_EQ:
 		check_key_parts(key_def, part_count,
 				traits->allows_partial_key);
-		const struct mh_lstrptr_node_t node = { .key = key };
+		node.key = key;
 		it->h_pos = mh_lstrptr_get(str_hash, &node, NULL, NULL);
 		it->base.next = hash_iterator_lstr_eq;
 		break;
diff --git a/src/box/request.m b/src/box/request.m
index b42ac6dfbe23398d9123e5ead51d380d9ce36d9c..e8e590f532ae419f027c27e172af1dd6a8a7579e 100644
--- a/src/box/request.m
+++ b/src/box/request.m
@@ -65,6 +65,14 @@ read_space(struct tbuf *data)
 	return space_find(space_no);
 }
 
+enum dup_replace_mode
+dup_replace_mode(uint32_t flags)
+{
+	return flags & BOX_ADD ? DUP_INSERT :
+		flags & BOX_REPLACE ?
+		DUP_REPLACE : DUP_REPLACE_OR_INSERT;
+}
+
 static void
 execute_replace(struct request *request, struct txn *txn)
 {
@@ -80,31 +88,19 @@ execute_replace(struct request *request, struct txn *txn)
 	if (data->size == 0 || data->size != valid_tuple(data, field_count))
 		tnt_raise(IllegalParams, :"incorrect tuple length");
 
-	txn->new_tuple = tuple_alloc(data->size);
-	tuple_ref(txn->new_tuple, 1);
-	txn->new_tuple->field_count = field_count;
-	memcpy(txn->new_tuple->data, data->data, data->size);
+	struct tuple *new_tuple = tuple_alloc(data->size);
+	new_tuple->field_count = field_count;
+	memcpy(new_tuple->data, data->data, data->size);
 
-	/* Try to find tuple by primary key */
-	Index *pk = space_index(sp, 0);
+	@try {
+		space_validate_tuple(sp, new_tuple);
+		enum dup_replace_mode mode = dup_replace_mode(request->flags);
+		txn_replace(txn, sp, NULL, new_tuple, mode);
 
-	/* Check to see if the tuple has a sufficient number of fields. */
-	if (unlikely(txn->new_tuple->field_count < sp->max_fieldno)) {
-		tnt_raise(IllegalParams, :"tuple must have all indexed fields");
+	} @catch (tnt_Exception *e) {
+		tuple_free(new_tuple);
+		@throw;
 	}
-
-	/* lookup old_tuple only when we have enough fields in new_tuple */
-	struct tuple *old_tuple = [pk findByTuple: txn->new_tuple];
-
-	if (request->flags & BOX_ADD && old_tuple != NULL)
-		tnt_raise(ClientError, :ER_TUPLE_FOUND);
-
-	if (request->flags & BOX_REPLACE && old_tuple == NULL)
-		tnt_raise(ClientError, :ER_TUPLE_NOT_FOUND);
-
-	space_validate(sp, old_tuple, txn->new_tuple);
-
-	txn_add_undo(txn, sp, old_tuple, txn->new_tuple);
 }
 
 /** {{{ UPDATE request implementation.
@@ -708,20 +704,27 @@ execute_update(struct request *request, struct txn *txn)
 	/* Try to find the tuple by primary key. */
 	struct tuple *old_tuple = [pk findByKey :key :key_part_count];
 
-	if (old_tuple != NULL) {
-		/* number of operations */
-		u32 op_cnt = read_u32(data);
-		struct update_op *ops = update_read_ops(data, op_cnt);
-		struct rope *rope = update_create_rope(ops, ops + op_cnt,
-						       old_tuple);
-		/* allocate new tuple */
-		size_t new_tuple_len = update_calc_new_tuple_length(rope);
-		txn->new_tuple = tuple_alloc(new_tuple_len);
-		tuple_ref(txn->new_tuple, 1);
-		do_update_ops(rope, txn->new_tuple);
-		space_validate(sp, old_tuple, txn->new_tuple);
+	if (old_tuple == NULL)
+		return;
+
+	/* number of operations */
+	u32 op_cnt = read_u32(data);
+	struct update_op *ops = update_read_ops(data, op_cnt);
+	struct rope *rope = update_create_rope(ops, ops + op_cnt,
+					       old_tuple);
+	/* Allocate a new tuple. */
+	size_t new_tuple_len = update_calc_new_tuple_length(rope);
+	struct tuple *new_tuple = tuple_alloc(new_tuple_len);
+
+	@try {
+		do_update_ops(rope, new_tuple);
+		space_validate_tuple(sp, new_tuple);
+		txn_replace(txn, sp, old_tuple, new_tuple, DUP_INSERT);
+
+	} @catch (tnt_Exception *e) {
+		tuple_free(new_tuple);
+		@throw;
 	}
-	txn_add_undo(txn, sp, old_tuple, txn->new_tuple);
 }
 
 /** }}} */
@@ -759,9 +762,6 @@ execute_select(struct request *request, struct port *port)
 
 		struct tuple *tuple;
 		while ((tuple = it->next(it)) != NULL) {
-			if (tuple->flags & GHOST)
-				continue;
-
 			if (offset > 0) {
 				offset--;
 				continue;
@@ -794,7 +794,10 @@ execute_delete(struct request *request, struct txn *txn)
 	Index *pk = space_index(sp, 0);
 	struct tuple *old_tuple = [pk findByKey :key :key_part_count];
 
-	txn_add_undo(txn, sp, old_tuple, NULL);
+	if (old_tuple == NULL)
+		return;
+
+	txn_replace(txn, sp, old_tuple, NULL, DUP_REPLACE_OR_INSERT);
 }
 
 /** To collects stats, we need a valid request type.
diff --git a/src/box/space.h b/src/box/space.h
index dec54040830ba05593935d4bacd85b2ffaf46e44..aff311369bd59b79c05bcf286b574ffaf70babc9 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -85,12 +85,101 @@ struct space {
 /** Get space ordinal number. */
 static inline i32 space_n(struct space *sp) { return sp->no; }
 
-void space_validate(struct space *sp, struct tuple *old_tuple,
-		    struct tuple *new_tuple);
-void space_replace(struct space *sp, struct tuple *old_tuple,
-		   struct tuple *new_tuple);
-void space_remove(struct space *sp, struct tuple *tuple);
+/**
+ * @brief A single method to handle REPLACE, DELETE and UPDATE.
+ *
+ * @param sp space
+ * @param old_tuple the tuple that should be removed (can be NULL)
+ * @param new_tuple the tuple that should be inserted (can be NULL)
+ * @param mode      dup_replace_mode, used only if new_tuple is not
+ *                  NULL and old_tuple is NULL, and only for the
+ *                  primary key.
+ *
+ * For DELETE, new_tuple must be NULL. old_tuple must be
+ * previously found in the primary key.
+ *
+ * For REPLACE, old_tuple must be NULL. The additional
+ * argument dup_replace_mode further defines how REPLACE
+ * should proceed.
+ *
+ * For UPDATE, both old_tuple and new_tuple must be given,
+ * where old_tuple must be previously found in the primary key.
+ *
+ * Let's consider these three cases in detail:
+ *
+ * 1. DELETE, old_tuple is not NULL, new_tuple is NULL
+ *    The effect is that old_tuple is removed from all
+ *    indexes. dup_replace_mode is ignored.
+ *
+ * 2. REPLACE, old_tuple is NULL, new_tuple is not NULL,
+ *    has one simple sub-case and two with further
+ *    ramifications:
+ *
+ *	A. dup_replace_mode is DUP_INSERT. Attempts to insert the
+ *	new tuple into all indexes. If *any* of the unique indexes
+ *	has a duplicate key, deletion is aborted, all of its
+ *	effects are removed, and an error is thrown.
+ *
+ *	B. dup_replace_mode is DUP_REPLACE. It means an existing
+ *	tuple has to be replaced with the new one. To do it, tries
+ *	to find a tuple with a duplicate key in the primary index.
+ *	If the tuple is not found, throws an error. Otherwise,
+ *	replaces the old tuple with a new one in the primary key.
+ *	Continues on to secondary keys, but if there is any
+ *	secondary key, which has a duplicate tuple, but one which
+ *	is different from the duplicate found in the primary key,
+ *	aborts, puts everything back, throws an exception.
+ *
+ *	For example, if there is a space with 3 unique keys and
+ *	two tuples { 1, 2, 3 } and { 3, 1, 2 }:
+ *
+ *	This REPLACE/DUP_REPLACE is OK: { 1, 5, 5 }
+ *	This REPLACE/DUP_REPLACE is not OK: { 2, 2, 2 } (there
+ *	is no tuple with key '2' in the primary key)
+ *	This REPLACE/DUP_REPLACE is not OK: { 1, 1, 1 } (there
+ *	is a conflicting tuple in the secondary unique key).
+ *
+ *	C. dup_replace_mode is DUP_REPLACE_OR_INSERT. If
+ *	there is a duplicate tuple in the primary key, behaves the
+ *	same way as DUP_REPLACE, otherwise behaves the same way as
+ *	DUP_INSERT.
+ *
+ * 3. UPDATE has to delete the old tuple and insert a new one.
+ *    dup_replace_mode is ignored.
+ *    Note that old_tuple primary key doesn't have to match
+ *    new_tuple primary key, thus a duplicate can be found.
+ *    For this reason, and since there can be duplicates in
+ *    other indexes, UPDATE is the same as DELETE +
+ *    REPLACE/DUP_INSERT.
+ *
+ * @return old_tuple. DELETE, UPDATE and REPLACE/DUP_REPLACE
+ * always produce an old tuple. REPLACE/DUP_INSERT always returns
+ * NULL. REPLACE/DUP_REPLACE_OR_INSERT may or may not find
+ * a duplicate.
+ *
+ * The method is all-or-nothing in all cases. Changes are either
+ * applied to all indexes, or nothing applied at all.
+ *
+ * Note, that even in case of REPLACE, dup_replace_mode only
+ * affects the primary key, for secondary keys it's always
+ * DUP_INSERT.
+ *
+ * @return tuple that was removed from the space.
+ *         The call never removes more than one tuple: if
+ *         old_tuple is given, dup_replace_mode is ignored.
+ *         Otherwise, it's taken into account only for the
+ *         primary key.
+ */
+struct tuple *
+space_replace(struct space *space, struct tuple *old_tuple,
+	      struct tuple *new_tuple, enum dup_replace_mode mode);
 
+/**
+ * Check that the tuple has correct arity and correct field
+ * types (a pre-requisite for an INSERT).
+ */
+void
+space_validate_tuple(struct space *sp, struct tuple *new_tuple);
 
 /**
  * Get index by index number.
diff --git a/src/box/space.m b/src/box/space.m
index 7f3dbfab014c211d6b70fe8cbb06e6a4101014da..523f7ae05e550e8eafce018310cac73698975eed 100644
--- a/src/box/space.m
+++ b/src/box/space.m
@@ -120,31 +120,51 @@ key_free(struct key_def *key_def)
 	free(key_def->cmp_order);
 }
 
-void
+struct tuple *
 space_replace(struct space *sp, struct tuple *old_tuple,
-	      struct tuple *new_tuple)
+	      struct tuple *new_tuple, enum dup_replace_mode mode)
 {
-	int n = index_count(sp);
-	for (int i = 0; i < n; i++) {
-		Index *index = sp->index[i];
-		[index replace: old_tuple :new_tuple];
+	int i = 0;
+	@try {
+		/* Update the primary key */
+		Index *pk = sp->index[0];
+		assert(pk->key_def->is_unique);
+		/*
+		 * If old_tuple is not NULL, the index
+		 * has to find and delete it, or raise an
+		 * error.
+		 */
+		old_tuple = [pk replace: old_tuple :new_tuple :mode];
+
+		assert(old_tuple || new_tuple);
+		int n = index_count(sp);
+		/* Update secondary keys */
+		for (i = i + 1; i < n; i++) {
+			Index *index = sp->index[i];
+			[index replace: old_tuple :new_tuple :DUP_INSERT];
+		}
+		return old_tuple;
+	} @catch (tnt_Exception *e) {
+		/* Rollback all changes */
+		for (i = i - 1; i >= 0;  i--) {
+			Index *index = sp->index[i];
+			[index replace: new_tuple: old_tuple: DUP_INSERT];
+		}
+		@throw;
 	}
 }
 
 void
-space_validate(struct space *sp, struct tuple *old_tuple,
-	       struct tuple *new_tuple)
+space_validate_tuple(struct space *sp, struct tuple *new_tuple)
 {
-	int n = index_count(sp);
-
-	/* Only secondary indexes are validated here. So check to see
-	   if there are any.*/
-	if (n <= 1) {
-		return;
-	}
+	/* Check to see if the tuple has a sufficient number of fields. */
+	if (new_tuple->field_count < sp->max_fieldno)
+		tnt_raise(IllegalParams,
+			  :"tuple must have all indexed fields");
 
 	if (sp->arity > 0 && sp->arity != new_tuple->field_count)
-		tnt_raise(IllegalParams, :"tuple field count must match space cardinality");
+		tnt_raise(IllegalParams,
+			  :"tuple field count must match space cardinality");
 
 	/* Sweep through the tuple and check the field sizes. */
 	u8 *data = new_tuple->data;
@@ -152,38 +172,20 @@ space_validate(struct space *sp, struct tuple *old_tuple,
 		/* Get the size of the current field and advance. */
 		u32 len = load_varint32((void **) &data);
 		data += len;
-
-		/* Check fixed size fields (NUM and NUM64) and skip undefined
-		   size fields (STRING and UNKNOWN). */
+		/*
+		 * Check fixed size fields (NUM and NUM64) and
+		 * skip undefined size fields (STRING and UNKNOWN).
+		 */
 		if (sp->field_types[f] == NUM) {
 			if (len != sizeof(u32))
-				tnt_raise(IllegalParams, :"field must be NUM");
+				tnt_raise(ClientError, :ER_KEY_FIELD_TYPE,
+					  "u32");
 		} else if (sp->field_types[f] == NUM64) {
 			if (len != sizeof(u64))
-				tnt_raise(IllegalParams, :"field must be NUM64");
+				tnt_raise(ClientError, :ER_KEY_FIELD_TYPE,
+					  "u64");
 		}
 	}
-
-	/* Check key uniqueness */
-	for (int i = 1; i < n; ++i) {
-		Index *index = sp->index[i];
-		if (index->key_def->is_unique) {
-			struct tuple *tuple = [index findByTuple: new_tuple];
-			if (tuple != NULL && tuple != old_tuple)
-				tnt_raise(ClientError, :ER_INDEX_VIOLATION,
-					  index_n(index));
-		}
-	}
-}
-
-void
-space_remove(struct space *sp, struct tuple *tuple)
-{
-	int n = index_count(sp);
-	for (int i = 0; i < n; i++) {
-		Index *index = sp->index[i];
-		[index remove: tuple];
-	}
 }
 
 void
diff --git a/src/box/tree.h b/src/box/tree.h
index d6b4276fa94b06f08ac446638e72448ae9eed565..6cacb96a192aaba6aec68189faf6c8bc5e8392ac 100644
--- a/src/box/tree.h
+++ b/src/box/tree.h
@@ -45,6 +45,8 @@ typedef int (*tree_cmp_t)(const void *, const void *, void *);
 	sptree_index tree;
 };
 
++ (struct index_traits *) traits;
+
 + (Index *) alloc: (struct key_def *) key_def :(struct space *) space;
 
 - (void) buildNext: (struct tuple *) tuple;
diff --git a/src/box/tree.m b/src/box/tree.m
index 79f99cb824b5dad24bf0ac6c6534a0773ec1947f..b437e987f1b12130c9d82e80888508cd7dc8ea22 100644
--- a/src/box/tree.m
+++ b/src/box/tree.m
@@ -30,10 +30,15 @@
 #include "tuple.h"
 #include "space.h"
 #include "exception.h"
+#include "errinj.h"
 #include <pickle.h>
 
 /* {{{ Utilities. *************************************************/
 
+static struct index_traits tree_index_traits = {
+	.allows_partial_key = true,
+};
+
 /**
  * Unsigned 32-bit int comparison.
  */
@@ -867,6 +872,11 @@ tree_iterator_gt(struct iterator *iterator)
 
 @implementation TreeIndex
 
++ (struct index_traits *) traits
+{
+	return &tree_index_traits;
+}
+
 + (Index *) alloc: (struct key_def *) key_def :(struct space *) space
 {
 	enum tree_type type = find_tree_type(space, key_def);
@@ -915,8 +925,11 @@ tree_iterator_gt(struct iterator *iterator)
 	return [self unfold: node];
 }
 
-- (struct tuple *) findUnsafe: (void *) key : (int) part_count
+- (struct tuple *) findByKey: (void *) key : (int) part_count
 {
+	assert(key_def->is_unique);
+	check_key_parts(key_def, part_count, false);
+
 	struct key_data *key_data
 		= alloca(sizeof(struct key_data) +
 			 _SIZEOF_SPARSE_PARTS(part_count));
@@ -929,40 +942,39 @@ tree_iterator_gt(struct iterator *iterator)
 	return [self unfold: node];
 }
 
-- (struct tuple *) findByTuple: (struct tuple *) tuple
+- (struct tuple *) replace: (struct tuple *) old_tuple
+			  :(struct tuple *) new_tuple
+			  :(enum dup_replace_mode) mode
 {
-	struct key_data *key_data
-		= alloca(sizeof(struct key_data) + _SIZEOF_SPARSE_PARTS(tuple->field_count));
-
-	key_data->data = tuple->data;
-	key_data->part_count = tuple->field_count;
-	fold_with_sparse_parts(key_def, tuple, key_data->parts);
+	size_t node_size = [self node_size];
+	void *new_node = alloca(node_size);
+	void *old_node = alloca(node_size);
+	uint32_t errcode;
 
-	void *node = sptree_index_find(&tree, key_data);
-	return [self unfold: node];
-}
+	if (new_tuple) {
+		void *dup_node = old_node;
+		[self fold: new_node :new_tuple];
 
-- (void) remove: (struct tuple *) tuple
-{
-	void *node = alloca([self node_size]);
-	[self fold: node :tuple];
-	sptree_index_delete(&tree, node);
-}
+		/* Try to optimistically replace the new_tuple. */
+		sptree_index_replace(&tree, new_node, &dup_node);
 
-- (void) replace: (struct tuple *) old_tuple
-		: (struct tuple *) new_tuple
-{
-	if (new_tuple->field_count < key_def->max_fieldno)
-		tnt_raise(ClientError, :ER_NO_SUCH_FIELD,
-			  key_def->max_fieldno);
+		struct tuple *dup_tuple = [self unfold: dup_node];
+		errcode = replace_check_dup(old_tuple, dup_tuple, mode);
 
-	void *node = alloca([self node_size]);
+		if (errcode) {
+			sptree_index_delete(&tree, new_node);
+			if (dup_node)
+				sptree_index_replace(&tree, dup_node, NULL);
+			tnt_raise(ClientError, :errcode, index_n(self));
+		}
+		if (dup_tuple)
+			return dup_tuple;
+	}
 	if (old_tuple) {
-		[self fold: node :old_tuple];
-		sptree_index_delete(&tree, node);
+		[self fold: old_node :old_tuple];
+		sptree_index_delete(&tree, old_node);
 	}
-	[self fold: node :new_tuple];
-	sptree_index_insert(&tree, node);
+	return old_tuple;
 }
 
 - (struct iterator *) allocIterator
diff --git a/src/box/txn.h b/src/box/txn.h
index dedc66f930ccc6d91575044eaa91f2e0285b4f8c..60ab3fe437a7b485709889bb41d0782c044ef190 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -29,16 +29,12 @@
  * SUCH DAMAGE.
  */
 #include <tbuf.h>
+#include "index.h"
+
 struct tuple;
 struct space;
 
-enum txn_flags {
-	BOX_NOT_STORE = 0x1
-};
-
 struct txn {
-	u32 txn_flags;
-
 	/* Undo info. */
 	struct space *space;
 	struct tuple *old_tuple;
@@ -49,19 +45,12 @@ struct txn {
 	struct tbuf req;
 };
 
-/** Tuple flags used for locking. */
-enum tuple_flags {
-	/** Waiting on WAL write to complete. */
-	WAL_WAIT = 0x1,
-	/** A new primary key is created but not yet written to WAL. */
-	GHOST = 0x2,
-};
-
 struct txn *txn_begin();
 void txn_commit(struct txn *txn);
 void txn_finish(struct txn *txn);
 void txn_rollback(struct txn *txn);
 void txn_add_redo(struct txn *txn, u16 op, struct tbuf *data);
-void txn_add_undo(struct txn *txn, struct space *space,
-		  struct tuple *old_tuple, struct tuple *new_tuple);
+void txn_replace(struct txn *txn, struct space *space,
+		 struct tuple *old_tuple, struct tuple *new_tuple,
+		 enum dup_replace_mode mode);
 #endif /* TARANTOOL_BOX_TXN_H_INCLUDED */
diff --git a/src/box/txn.m b/src/box/txn.m
index 1dbeafdcb97f4b08ab0a86d5d275a83e52415046..925aa950dbadaca42ecf08efc75757d3e916fdbe 100644
--- a/src/box/txn.m
+++ b/src/box/txn.m
@@ -32,20 +32,6 @@
 #include <recovery.h>
 #include <fiber.h>
 
-static void
-txn_lock(struct txn *txn __attribute__((unused)), struct tuple *tuple)
-{
-	say_debug("txn_lock(%p)", tuple);
-	tuple->flags |= WAL_WAIT;
-}
-
-static void
-txn_unlock(struct txn *txn __attribute__((unused)), struct tuple *tuple)
-{
-	assert(tuple->flags & WAL_WAIT);
-	tuple->flags &= ~WAL_WAIT;
-}
-
 void
 txn_add_redo(struct txn *txn, u16 op, struct tbuf *data)
 {
@@ -59,31 +45,23 @@ txn_add_redo(struct txn *txn, u16 op, struct tbuf *data)
 }
 
 void
-txn_add_undo(struct txn *txn, struct space *space,
-	     struct tuple *old_tuple, struct tuple *new_tuple)
+txn_replace(struct txn *txn, struct space *space,
+	    struct tuple *old_tuple, struct tuple *new_tuple,
+	    enum dup_replace_mode mode)
 {
 	/* txn_add_undo() must be done after txn_add_redo() */
 	assert(txn->op != 0);
-	txn->new_tuple = new_tuple;
-	if (new_tuple == NULL && old_tuple == NULL) {
-		/*
-		 * There is no subject tuple we could write to
-		 * WAL, which means, to do a write, we would have
-		 * to allocate one. Too complicated, for now, just
-		 * do no logging for DELETEs that do nothing.
-		*/
-		txn->txn_flags |= BOX_NOT_STORE;
-	} else if (new_tuple == NULL) {
-		space_remove(space, old_tuple);
-	} else {
-		space_replace(space, old_tuple, new_tuple);
-		txn_lock(txn, new_tuple);
-	}
-	/* Remember the old tuple only if we locked it
-	 * successfully, to not unlock a tuple locked by another
-	 * transaction in rollback().
+	assert(old_tuple || new_tuple);
+	/*
+	 * Remember the old tuple only if we replaced it
+	 * successfully, to not remove a tuple inserted by
+	 * another transaction in rollback().
 	 */
-	txn->old_tuple = old_tuple;
+	txn->old_tuple = space_replace(space, old_tuple, new_tuple, mode);
+	if (new_tuple) {
+		txn->new_tuple = new_tuple;
+		tuple_ref(txn->new_tuple, 1);
+	}
 	txn->space = space;
 }
 
@@ -97,9 +75,7 @@ txn_begin()
 void
 txn_commit(struct txn *txn)
 {
-	if (txn->op == 0) /* Nothing to do. */
-		return;
-	if (! (txn->txn_flags & BOX_NOT_STORE)) {
+	if (txn->old_tuple || txn->new_tuple) {
 		int64_t lsn = next_lsn(recovery_state);
 		int res = wal_write(recovery_state, lsn, 0,
 				    txn->op, &txn->req);
@@ -114,22 +90,16 @@ txn_finish(struct txn *txn)
 {
 	if (txn->old_tuple)
 		tuple_ref(txn->old_tuple, -1);
-	if (txn->new_tuple)
-		txn_unlock(txn, txn->new_tuple);
 	TRASH(txn);
 }
 
 void
 txn_rollback(struct txn *txn)
 {
-	if (txn->op == 0) /* Nothing to do. */
-		return;
-	if (txn->old_tuple) {
-		space_replace(txn->space, txn->new_tuple, txn->old_tuple);
-	} else if (txn->new_tuple && txn->new_tuple->flags & WAL_WAIT) {
-		space_remove(txn->space, txn->new_tuple);
+	if (txn->old_tuple || txn->new_tuple) {
+		space_replace(txn->space, txn->new_tuple, txn->old_tuple, DUP_INSERT);
+		if (txn->new_tuple)
+			tuple_ref(txn->new_tuple, -1);
 	}
-	if (txn->new_tuple)
-		tuple_ref(txn->new_tuple, -1);
 	TRASH(txn);
 }
diff --git a/test/big/hash.result b/test/big/hash.result
index d619ae4248c3658d1ec0ac910f4ceb0668753c37..56c1ba70d46f0fcd88c52e3dfd648c40b068759d 100644
--- a/test/big/hash.result
+++ b/test/big/hash.result
@@ -420,3 +420,128 @@ lua box.space[11]:truncate()
 lua box.space[12]:truncate()
 ---
 ...
+lua box.space[21]:truncate()
+---
+...
+insert into t21 values (0, 0, 0, 0)
+Insert OK, 1 row affected
+insert into t21 values (1, 1, 1, 1)
+Insert OK, 1 row affected
+insert into t21 values (2, 2, 2, 2)
+Insert OK, 1 row affected
+replace into t21 values (1, 1, 1, 1)
+Replace OK, 1 row affected
+replace into t21 values (1, 10, 10, 10)
+Replace OK, 1 row affected
+replace into t21 values (1, 1, 1, 1)
+Replace OK, 1 row affected
+select * from t21 WHERE k0 = 10
+No match
+select * from t21 WHERE k1 = 10
+No match
+select * from t21 WHERE k2 = 10
+No match
+select * from t21 WHERE k3 = 10
+No match
+select * from t21 WHERE k0 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+select * from t21 WHERE k1 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+select * from t21 WHERE k2 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+select * from t21 WHERE k3 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+insert into t21 values (10, 10, 10, 10)
+Insert OK, 1 row affected
+delete from t21 WHERE k0 = 10
+Delete OK, 1 row affected
+select * from t21 WHERE k0 = 10
+No match
+select * from t21 WHERE k1 = 10
+No match
+select * from t21 WHERE k2 = 10
+No match
+select * from t21 WHERE k3 = 10
+No match
+insert into t21 values (1, 10, 10, 10)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 0'
+select * from t21 WHERE k0 = 10
+No match
+select * from t21 WHERE k1 = 10
+No match
+select * from t21 WHERE k2 = 10
+No match
+select * from t21 WHERE k3 = 10
+No match
+select * from t21 WHERE k0 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+replace into t21 values (10, 10, 10, 10)
+An error occurred: ER_TUPLE_NOT_FOUND, 'Tuple doesn't exist in index 0'
+select * from t21 WHERE k0 = 10
+No match
+select * from t21 WHERE k1 = 10
+No match
+select * from t21 WHERE k2 = 10
+No match
+select * from t21 WHERE k3 = 10
+No match
+insert into t21 values (10, 0, 10, 10)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 1'
+select * from t21 WHERE k0 = 10
+No match
+select * from t21 WHERE k1 = 10
+No match
+select * from t21 WHERE k2 = 10
+No match
+select * from t21 WHERE k3 = 10
+No match
+select * from t21 WHERE k1 = 0
+Found 1 tuple:
+[0, 0, 0, 0]
+replace into t21 values (2, 0, 10, 10)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 1'
+select * from t21 WHERE k0 = 10
+No match
+select * from t21 WHERE k1 = 10
+No match
+select * from t21 WHERE k2 = 10
+No match
+select * from t21 WHERE k3 = 10
+No match
+select * from t21 WHERE k1 = 0
+Found 1 tuple:
+[0, 0, 0, 0]
+insert into t21 values (10, 10, 10, 0)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 3'
+select * from t21 WHERE k0 = 10
+No match
+select * from t21 WHERE k1 = 10
+No match
+select * from t21 WHERE k2 = 10
+No match
+select * from t21 WHERE k3 = 10
+No match
+select * from t21 WHERE k3 = 0
+Found 1 tuple:
+[0, 0, 0, 0]
+replace into t21 values (2, 10, 10, 0)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 3'
+select * from t21 WHERE k0 = 10
+No match
+select * from t21 WHERE k1 = 10
+No match
+select * from t21 WHERE k2 = 10
+No match
+select * from t21 WHERE k3 = 10
+No match
+select * from t21 WHERE k3 = 0
+Found 1 tuple:
+[0, 0, 0, 0]
+lua box.space[21]:truncate()
+---
+...
diff --git a/test/big/hash.test b/test/big/hash.test
index de42a55988cea7f296f064f46f2369d77eb47f4c..51c46574092d540d1b34acf510427594c2ee57da 100644
--- a/test/big/hash.test
+++ b/test/big/hash.test
@@ -265,3 +265,84 @@ exec admin "lua box.space[12]:delete('key 1', 'key 2')"
 exec admin "lua box.space[10]:truncate()"
 exec admin "lua box.space[11]:truncate()"
 exec admin "lua box.space[12]:truncate()"
+
+#
+# hash::replace tests
+#
+
+exec admin "lua box.space[21]:truncate()"
+
+exec sql "insert into t21 values (0, 0, 0, 0)"
+exec sql "insert into t21 values (1, 1, 1, 1)"
+exec sql "insert into t21 values (2, 2, 2, 2)"
+
+# OK
+exec sql "replace into t21 values (1, 1, 1, 1)"
+exec sql "replace into t21 values (1, 10, 10, 10)"
+exec sql "replace into t21 values (1, 1, 1, 1)"
+exec sql "select * from t21 WHERE k0 = 10"
+exec sql "select * from t21 WHERE k1 = 10"
+exec sql "select * from t21 WHERE k2 = 10"
+exec sql "select * from t21 WHERE k3 = 10"
+exec sql "select * from t21 WHERE k0 = 1"
+exec sql "select * from t21 WHERE k1 = 1"
+exec sql "select * from t21 WHERE k2 = 1"
+exec sql "select * from t21 WHERE k3 = 1"
+
+# OK
+exec sql "insert into t21 values (10, 10, 10, 10)"
+exec sql "delete from t21 WHERE k0 = 10"
+exec sql "select * from t21 WHERE k0 = 10"
+exec sql "select * from t21 WHERE k1 = 10"
+exec sql "select * from t21 WHERE k2 = 10"
+exec sql "select * from t21 WHERE k3 = 10"
+
+
+# TupleFound (primary key)
+exec sql "insert into t21 values (1, 10, 10, 10)"
+exec sql "select * from t21 WHERE k0 = 10"
+exec sql "select * from t21 WHERE k1 = 10"
+exec sql "select * from t21 WHERE k2 = 10"
+exec sql "select * from t21 WHERE k3 = 10"
+exec sql "select * from t21 WHERE k0 = 1"
+
+# TupleNotFound (primary key)
+exec sql "replace into t21 values (10, 10, 10, 10)"
+exec sql "select * from t21 WHERE k0 = 10"
+exec sql "select * from t21 WHERE k1 = 10"
+exec sql "select * from t21 WHERE k2 = 10"
+exec sql "select * from t21 WHERE k3 = 10"
+
+# TupleFound (key #1)
+exec sql "insert into t21 values (10, 0, 10, 10)"
+exec sql "select * from t21 WHERE k0 = 10"
+exec sql "select * from t21 WHERE k1 = 10"
+exec sql "select * from t21 WHERE k2 = 10"
+exec sql "select * from t21 WHERE k3 = 10"
+exec sql "select * from t21 WHERE k1 = 0"
+
+# TupleFound (key #1)
+exec sql "replace into t21 values (2, 0, 10, 10)"
+exec sql "select * from t21 WHERE k0 = 10"
+exec sql "select * from t21 WHERE k1 = 10"
+exec sql "select * from t21 WHERE k2 = 10"
+exec sql "select * from t21 WHERE k3 = 10"
+exec sql "select * from t21 WHERE k1 = 0"
+
+# TupleFound (key #3)
+exec sql "insert into t21 values (10, 10, 10, 0)"
+exec sql "select * from t21 WHERE k0 = 10"
+exec sql "select * from t21 WHERE k1 = 10"
+exec sql "select * from t21 WHERE k2 = 10"
+exec sql "select * from t21 WHERE k3 = 10"
+exec sql "select * from t21 WHERE k3 = 0"
+
+# TupleFound (key #3)
+exec sql "replace into t21 values (2, 10, 10, 0)"
+exec sql "select * from t21 WHERE k0 = 10"
+exec sql "select * from t21 WHERE k1 = 10"
+exec sql "select * from t21 WHERE k2 = 10"
+exec sql "select * from t21 WHERE k3 = 10"
+exec sql "select * from t21 WHERE k3 = 0"
+
+exec admin "lua box.space[21]:truncate()"
diff --git a/test/big/suite.ini b/test/big/suite.ini
index a7d20861193f0e45a52e5ed574549ae238cd7ac3..4c9d65bcc5f5f528c7e51e57a5d3bdf7de8fc46b 100644
--- a/test/big/suite.ini
+++ b/test/big/suite.ini
@@ -5,3 +5,4 @@ config = tarantool.cfg
 # disabled = lua.test
 # put disabled in valgrind test here
 #valgrind_disabled = ...
+release_disabled = hash_errinj.test
diff --git a/test/big/tarantool.cfg b/test/big/tarantool.cfg
index 5ecbbbd01a3a48e39bf7df5f36499941e3d1897a..b3334affc2dfc6e962f41ba0fe9bbbf8de2b063d 100644
--- a/test/big/tarantool.cfg
+++ b/test/big/tarantool.cfg
@@ -282,3 +282,49 @@ space[20].index[4].type = "HASH"
 space[20].index[4].unique = 1
 space[20].index[4].key_field[0].fieldno = 0
 space[20].index[4].key_field[0].type = "STR"
+
+# hash::replace
+space[21].enabled = true
+
+space[21].index[0].type = "HASH"
+space[21].index[0].unique = true
+space[21].index[0].key_field[0].fieldno = 0
+space[21].index[0].key_field[0].type = "NUM"
+
+space[21].index[1].type = "HASH"
+space[21].index[1].unique = true
+space[21].index[1].key_field[0].fieldno = 1
+space[21].index[1].key_field[0].type = "NUM"
+
+space[21].index[2].type = "HASH"
+space[21].index[2].unique = true
+space[21].index[2].key_field[0].fieldno = 2
+space[21].index[2].key_field[0].type = "NUM"
+
+space[21].index[3].type = "HASH"
+space[21].index[3].unique = true
+space[21].index[3].key_field[0].fieldno = 3
+space[21].index[3].key_field[0].type = "NUM"
+
+# tree::replace test
+space[22].enabled = true
+
+space[22].index[0].type = "TREE"
+space[22].index[0].unique = true
+space[22].index[0].key_field[0].fieldno = 0
+space[22].index[0].key_field[0].type = "NUM"
+
+space[22].index[1].type = "TREE"
+space[22].index[1].unique = true
+space[22].index[1].key_field[0].fieldno = 1
+space[22].index[1].key_field[0].type = "NUM"
+
+space[22].index[2].type = "TREE"
+space[22].index[2].unique = false
+space[22].index[2].key_field[0].fieldno = 2
+space[22].index[2].key_field[0].type = "NUM"
+
+space[22].index[3].type = "TREE"
+space[22].index[3].unique = true
+space[22].index[3].key_field[0].fieldno = 3
+space[22].index[3].key_field[0].type = "NUM"
diff --git a/test/big/tree_pk.result b/test/big/tree_pk.result
index 7ee015ed0c60748af23ab04c3df245f00cdcb501..47d1de4390b9638e389cbc474e7b066e80b16474 100644
--- a/test/big/tree_pk.result
+++ b/test/big/tree_pk.result
@@ -129,3 +129,149 @@ lua box.space[15].index[0]:select_range(3, 'abcdb')
 lua box.space[15]:truncate()
 ---
 ...
+lua box.space[22]:truncate()
+---
+...
+insert into t22 values (0, 0, 0, 0)
+Insert OK, 1 row affected
+insert into t22 values (1, 1, 1, 1)
+Insert OK, 1 row affected
+insert into t22 values (2, 2, 2, 2)
+Insert OK, 1 row affected
+replace into t22 values (1, 1, 1, 1)
+Replace OK, 1 row affected
+replace into t22 values (1, 10, 10, 10)
+Replace OK, 1 row affected
+replace into t22 values (1, 1, 1, 1)
+Replace OK, 1 row affected
+select * from t22 WHERE k0 = 10
+No match
+select * from t22 WHERE k1 = 10
+No match
+select * from t22 WHERE k2 = 10
+No match
+select * from t22 WHERE k3 = 10
+No match
+select * from t22 WHERE k0 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+select * from t22 WHERE k1 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+select * from t22 WHERE k2 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+select * from t22 WHERE k3 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+insert into t22 values (10, 10, 10, 10)
+Insert OK, 1 row affected
+delete from t22 WHERE k0 = 10
+Delete OK, 1 row affected
+select * from t22 WHERE k0 = 10
+No match
+select * from t22 WHERE k1 = 10
+No match
+select * from t22 WHERE k2 = 10
+No match
+select * from t22 WHERE k3 = 10
+No match
+insert into t22 values (1, 10, 10, 10)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 0'
+select * from t22 WHERE k0 = 10
+No match
+select * from t22 WHERE k1 = 10
+No match
+select * from t22 WHERE k2 = 10
+No match
+select * from t22 WHERE k3 = 10
+No match
+select * from t22 WHERE k0 = 1
+Found 1 tuple:
+[1, 1, 1, 1]
+replace into t22 values (10, 10, 10, 10)
+An error occurred: ER_TUPLE_NOT_FOUND, 'Tuple doesn't exist in index 0'
+select * from t22 WHERE k0 = 10
+No match
+select * from t22 WHERE k1 = 10
+No match
+select * from t22 WHERE k2 = 10
+No match
+select * from t22 WHERE k3 = 10
+No match
+insert into t22 values (10, 0, 10, 10)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 1'
+select * from t22 WHERE k0 = 10
+No match
+select * from t22 WHERE k1 = 10
+No match
+select * from t22 WHERE k2 = 10
+No match
+select * from t22 WHERE k3 = 10
+No match
+select * from t22 WHERE k1 = 0
+Found 1 tuple:
+[0, 0, 0, 0]
+replace into t22 values (2, 0, 10, 10)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 1'
+select * from t22 WHERE k0 = 10
+No match
+select * from t22 WHERE k1 = 10
+No match
+select * from t22 WHERE k2 = 10
+No match
+select * from t22 WHERE k3 = 10
+No match
+select * from t22 WHERE k1 = 0
+Found 1 tuple:
+[0, 0, 0, 0]
+insert into t22 values (10, 10, 10, 0)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 3'
+select * from t22 WHERE k0 = 10
+No match
+select * from t22 WHERE k1 = 10
+No match
+select * from t22 WHERE k2 = 10
+No match
+select * from t22 WHERE k3 = 10
+No match
+select * from t22 WHERE k3 = 0
+Found 1 tuple:
+[0, 0, 0, 0]
+replace into t22 values (2, 10, 10, 0)
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 3'
+select * from t22 WHERE k0 = 10
+No match
+select * from t22 WHERE k1 = 10
+No match
+select * from t22 WHERE k2 = 10
+No match
+select * from t22 WHERE k3 = 10
+No match
+select * from t22 WHERE k3 = 0
+Found 1 tuple:
+[0, 0, 0, 0]
+insert into t22 values (4, 4, 0, 4)
+Insert OK, 1 row affected
+insert into t22 values (5, 5, 0, 5)
+Insert OK, 1 row affected
+insert into t22 values (6, 6, 0, 6)
+Insert OK, 1 row affected
+replace into t22 values (5, 5, 0, 5)
+Replace OK, 1 row affected
+select * from t22 WHERE k2 = 0
+Found 4 tuples:
+[0, 0, 0, 0]
+[4, 4, 0, 4]
+[6, 6, 0, 6]
+[5, 5, 0, 5]
+delete from t22 WHERE k0 = 5
+Delete OK, 1 row affected
+select * from t22 WHERE k2 = 0
+Found 3 tuples:
+[0, 0, 0, 0]
+[4, 4, 0, 4]
+[6, 6, 0, 6]
+lua box.space[22]:truncate()
+---
+...
diff --git a/test/big/tree_pk.test b/test/big/tree_pk.test
index 7537b8581e10de58a66d1713f566ddf1c7ec295f..91f5fe57b4ecad0d6dade64f0fec71cf69d12514 100644
--- a/test/big/tree_pk.test
+++ b/test/big/tree_pk.test
@@ -79,3 +79,94 @@ exec sql "insert into t15 values ('abcdc')"
 exec sql "insert into t15 values ('abcdc_')"
 exec admin "lua box.space[15].index[0]:select_range(3, 'abcdb')"
 exec admin "lua box.space[15]:truncate()"
+
+#
+# tree::replace tests
+#
+
+exec admin "lua box.space[22]:truncate()"
+
+exec sql "insert into t22 values (0, 0, 0, 0)"
+exec sql "insert into t22 values (1, 1, 1, 1)"
+exec sql "insert into t22 values (2, 2, 2, 2)"
+
+# OK
+exec sql "replace into t22 values (1, 1, 1, 1)"
+exec sql "replace into t22 values (1, 10, 10, 10)"
+exec sql "replace into t22 values (1, 1, 1, 1)"
+exec sql "select * from t22 WHERE k0 = 10"
+exec sql "select * from t22 WHERE k1 = 10"
+exec sql "select * from t22 WHERE k2 = 10"
+exec sql "select * from t22 WHERE k3 = 10"
+exec sql "select * from t22 WHERE k0 = 1"
+exec sql "select * from t22 WHERE k1 = 1"
+exec sql "select * from t22 WHERE k2 = 1"
+exec sql "select * from t22 WHERE k3 = 1"
+
+# OK
+exec sql "insert into t22 values (10, 10, 10, 10)"
+exec sql "delete from t22 WHERE k0 = 10"
+exec sql "select * from t22 WHERE k0 = 10"
+exec sql "select * from t22 WHERE k1 = 10"
+exec sql "select * from t22 WHERE k2 = 10"
+exec sql "select * from t22 WHERE k3 = 10"
+
+
+# TupleFound (primary key)
+exec sql "insert into t22 values (1, 10, 10, 10)"
+exec sql "select * from t22 WHERE k0 = 10"
+exec sql "select * from t22 WHERE k1 = 10"
+exec sql "select * from t22 WHERE k2 = 10"
+exec sql "select * from t22 WHERE k3 = 10"
+exec sql "select * from t22 WHERE k0 = 1"
+
+# TupleNotFound (primary key)
+exec sql "replace into t22 values (10, 10, 10, 10)"
+exec sql "select * from t22 WHERE k0 = 10"
+exec sql "select * from t22 WHERE k1 = 10"
+exec sql "select * from t22 WHERE k2 = 10"
+exec sql "select * from t22 WHERE k3 = 10"
+
+# TupleFound (key #1)
+exec sql "insert into t22 values (10, 0, 10, 10)"
+exec sql "select * from t22 WHERE k0 = 10"
+exec sql "select * from t22 WHERE k1 = 10"
+exec sql "select * from t22 WHERE k2 = 10"
+exec sql "select * from t22 WHERE k3 = 10"
+exec sql "select * from t22 WHERE k1 = 0"
+
+# TupleFound (key #1)
+exec sql "replace into t22 values (2, 0, 10, 10)"
+exec sql "select * from t22 WHERE k0 = 10"
+exec sql "select * from t22 WHERE k1 = 10"
+exec sql "select * from t22 WHERE k2 = 10"
+exec sql "select * from t22 WHERE k3 = 10"
+exec sql "select * from t22 WHERE k1 = 0"
+
+# TupleFound (key #3)
+exec sql "insert into t22 values (10, 10, 10, 0)"
+exec sql "select * from t22 WHERE k0 = 10"
+exec sql "select * from t22 WHERE k1 = 10"
+exec sql "select * from t22 WHERE k2 = 10"
+exec sql "select * from t22 WHERE k3 = 10"
+exec sql "select * from t22 WHERE k3 = 0"
+
+# TupleFound (key #3)
+exec sql "replace into t22 values (2, 10, 10, 0)"
+exec sql "select * from t22 WHERE k0 = 10"
+exec sql "select * from t22 WHERE k1 = 10"
+exec sql "select * from t22 WHERE k2 = 10"
+exec sql "select * from t22 WHERE k3 = 10"
+exec sql "select * from t22 WHERE k3 = 0"
+
+# Non-Uniq test (key #2)
+exec sql "insert into t22 values (4, 4, 0, 4)"
+exec sql "insert into t22 values (5, 5, 0, 5)"
+exec sql "insert into t22 values (6, 6, 0, 6)"
+exec sql "replace into t22 values (5, 5, 0, 5)"
+exec sql "select * from t22 WHERE k2 = 0"
+exec sql "delete from t22 WHERE k0 = 5"
+exec sql "select * from t22 WHERE k2 = 0"
+
+exec admin "lua box.space[22]:truncate()"
+
diff --git a/test/box/errinj.result b/test/box/errinj.result
index 63245d8c26f1e45f11d7335b20a052a93fe85869..8156e496766c40c24f700da34457e0fe9c4dd27c 100644
--- a/test/box/errinj.result
+++ b/test/box/errinj.result
@@ -7,6 +7,8 @@ error injections:
     state: off
   - name: ERRINJ_WAL_ROTATE
     state: off
+  - name: ERRINJ_INDEX_ALLOC
+    state: off
 ...
 set injection some-injection on
 ---
diff --git a/test/box/fiber.result b/test/box/fiber.result
index b44c7713d2bd616d5509798e322ecc3fa0aafefc..8a333e0aa16bd8b5b34177ded344b9267241affe 100644
--- a/test/box/fiber.result
+++ b/test/box/fiber.result
@@ -93,7 +93,7 @@ call box.insert(0, 'test', 'old', 'abcd')
 Found 1 tuple:
 [1953719668, 'old', 1684234849]
 call box.insert(0, 'test', 'old', 'abcd')
-An error occurred: ER_TUPLE_FOUND, 'Tuple already exists'
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 0'
 call box.update(0, 'test', '=p=p', 0, 'pass', 1, 'new')
 Found 1 tuple:
 [1936941424, 'new', 1684234849]
diff --git a/test/box/lua.result b/test/box/lua.result
index fad4d96661d197ebad07fe4ac9d93dd583726fb3..53e88664a3bd991e0d71edf8d8130f9f86c71782 100644
--- a/test/box/lua.result
+++ b/test/box/lua.result
@@ -314,7 +314,7 @@ call box.insert(0, 'test', 'old', 'abcd')
 Found 1 tuple:
 [1953719668, 'old', 1684234849]
 call box.insert(0, 'test', 'old', 'abcd')
-An error occurred: ER_TUPLE_FOUND, 'Tuple already exists'
+An error occurred: ER_TUPLE_FOUND, 'Duplicate key exists in unique index 0'
 call box.update(0, 'test', '=p=p', 0, 'pass', 1, 'new')
 Found 1 tuple:
 [1936941424, 'new', 1684234849]
@@ -828,7 +828,7 @@ lua f = box.fiber.create(function () return true end)
 #
 lua box.space[0]:insert('test', 'something to splice')
 ---
-error: 'Tuple already exists'
+error: 'Duplicate key exists in unique index 0'
 ...
 lua box.space[0]:update('test', ':p', 1, box.pack('ppp', 0, 4, 'no'))
 ---
@@ -980,7 +980,7 @@ lua pcall(box.insert, 0, 1, 'hello')
 lua pcall(box.insert, 0, 1, 'hello')
 ---
  - false
- - Tuple already exists
+ - Duplicate key exists in unique index 0
 ...
 lua box.space[0]:truncate()
 ---
diff --git a/third_party/sptree.h b/third_party/sptree.h
index 1bab0347d1919fbf2eee94f6ba318a73a5ebadd9..e8c8c4be555febd6f0b07f97fcdb8d4d4d5ba16a 100644
--- a/third_party/sptree.h
+++ b/third_party/sptree.h
@@ -77,7 +77,7 @@ typedef struct sptree_node_pointers {
  *                         int (*elemcompare)(const void *e1, const void *e2, void *arg),
  *                         void *arg)
  *
- *   void sptree_NAME_insert(sptree_NAME *tree, void *value)
+ *   void sptree_NAME_replace(sptree_NAME *tree, void *value, void **p_oldvalue)
  *   void sptree_NAME_delete(sptree_NAME *tree, void *value)
  *   void* sptree_NAME_find(sptree_NAME *tree, void *key)
  *
@@ -300,7 +300,7 @@ sptree_##name##_balance(sptree_##name *t, spnode_t node, spnode_t size) {
 }                                                                                         \
                                                                                           \
 static inline void                                                                        \
-sptree_##name##_insert(sptree_##name *t, void *v) {                                       \
+sptree_##name##_replace(sptree_##name *t, void *v, void **p_old) {                        \
     spnode_t    node, depth = 0;                                                          \
     spnode_t    path[ t->max_depth + 2];                                                  \
                                                                                           \
@@ -312,6 +312,8 @@ sptree_##name##_insert(sptree_##name *t, void *v) {
         t->garbage_head = SPNIL;                                                          \
         t->nmember = 1;                                                                   \
         t->size=1;                                                                        \
+        if (p_old)                                                                        \
+            *p_old = NULL;                                                                \
         return;                                                                           \
     } else {                                                                              \
         spnode_t    parent = t->root;                                                     \
@@ -319,6 +321,8 @@ sptree_##name##_insert(sptree_##name *t, void *v) {
         for(;;)    {                                                                      \
             int r = t->elemcompare(v, ITHELEM(t, parent), t->arg);                        \
             if (r==0) {                                                                   \
+                if (p_old)                                                                \
+                    memcpy(*p_old, ITHELEM(t, parent), t->elemsize);                      \
                 memcpy(ITHELEM(t, parent), v, t->elemsize);                               \
                 return;                                                                   \
             }                                                                             \
@@ -345,6 +349,8 @@ sptree_##name##_insert(sptree_##name *t, void *v) {
             }                                                                             \
         }                                                                                 \
     }                                                                                     \
+    if (p_old)                                                                        \
+        *p_old = NULL;                                                                \
                                                                                           \
     t->size++;                                                                            \
     if ( t->size > t->max_size )                                                          \