[Tarantool-patches] [PATCH 1/1] tuple: turn invalid bar update into nop

Vladislav Shpilevoy v.shpilevoy at tarantool.org
Sat Jul 4 20:20:40 MSK 2020


There was a bug about invalid bar update operations. Bar update is
a single update operation with non empty isolated JSON path.
Isolated JSON path means that it does not interleave with any
other update operation in the same update/upsert() call.

In case bar update fails, there were 2 outcomes:

- it could crash in several places, when JSON path was invalid, or
  a type assumed in the JSON path didn't match the actual type
  (such as [...] applied to a scalar value);

- it could log error and all, but in the end still saved the
  operation result like if it was '='. For example, invalid '+'
  would behave just like '='.

The errors were happening for upsert() only, because this call
never treats client errors as errors. Instead, they are just
ignored. Bad JSON - ignore, integer overflow - ignore, and so on.
Are not ignored only client errors found during operation
decoding.

The expected behaviour is that such invalid operations are
skipped. Note, that it is possible, that some operations are
skipped and some are not.

The patch adds a 'commit' phase to bar field update. This is based
on the fact that bar is *always* created from a nop field (not
updated, part of the old tuple). So it is enough to leave this
field nop, if something goes wrong.

With all the other field types all is fine. They already do that
when apply an operation.

It is worth mentioning that even if during an attempt to apply an
invalid operation the update tree was changed anyhow, this is ok.
Structure changes don't affect the final result. For example, it
is ok if a rope field in an array will be split in 2. It is also
fine if a bar field is branched into route -> bar. This is fine
if a map field sequence will be split in 2 sequences.

Closes #5135
---
Branch: http://github.com/tarantool/tarantool/tree/gerold103/gh-5135-invalid-upsert
Issue: https://github.com/tarantool/tarantool/issues/5135

While working on this I found an inconsistency in which errors do
we ignore. https://github.com/tarantool/tarantool/issues/5137

 src/box/xrow_update_bar.c                |  38 +-
 test/box/gh-5135-invalid-upsert.result   | 146 ++++++++
 test/box/gh-5135-invalid-upsert.test.lua |  72 ++++
 test/box/update.result                   | 456 +++++++++++++++++++++--
 test/box/update.test.lua                 | 196 ++++++++--
 5 files changed, 824 insertions(+), 84 deletions(-)
 create mode 100644 test/box/gh-5135-invalid-upsert.result
 create mode 100644 test/box/gh-5135-invalid-upsert.test.lua

diff --git a/src/box/xrow_update_bar.c b/src/box/xrow_update_bar.c
index 0033f0044..1d20d6c74 100644
--- a/src/box/xrow_update_bar.c
+++ b/src/box/xrow_update_bar.c
@@ -31,6 +31,22 @@
 #include "xrow_update_field.h"
 #include "tuple.h"
 
+/**
+ * 'Commit' bar creation only when it is fully initialized and
+ * valid. Because if all this is happening inside an upsert()
+ * operation, it won't stop the whole xrow upsert. This field will
+ * still be saved in the result tuple. But in case of an error
+ * this operation should be skipped. So this is kept 'nop' when
+ * error happens.
+ */
+static inline int
+xrow_update_bar_commit(struct xrow_update_field *field)
+{
+	assert(field->type == XUPDATE_NOP);
+	field->type = XUPDATE_BAR;
+	return 0;
+}
+
 /**
  * Locate a field to update by @a op's JSON path and initialize
  * @a field as a bar update.
@@ -59,8 +75,12 @@ xrow_update_bar_locate(struct xrow_update_op *op,
 	 * terminal.
 	 */
 	assert(!xrow_update_op_is_term(op));
+	/*
+	 * Nop means this function can change field->bar and
+	 * nothing will break.
+	 */
+	assert(field->type == XUPDATE_NOP);
 	int rc;
-	field->type = XUPDATE_BAR;
 	field->bar.op = op;
 	field->bar.path = op->lexer.src + op->lexer.offset;
 	field->bar.path_len = op->lexer.src_len - op->lexer.offset;
@@ -114,8 +134,12 @@ xrow_update_bar_locate_opt(struct xrow_update_op *op,
 	 * terminal.
 	 */
 	assert(!xrow_update_op_is_term(op));
+	/*
+	 * Nop means this function can change field->bar and
+	 * nothing will break.
+	 */
+	assert(field->type == XUPDATE_NOP);
 	int rc;
-	field->type = XUPDATE_BAR;
 	field->bar.op = op;
 	field->bar.path = op->lexer.src + op->lexer.offset;
 	field->bar.path_len = op->lexer.src_len - op->lexer.offset;
@@ -231,7 +255,7 @@ xrow_update_op_do_nop_insert(struct xrow_update_op *op,
 		 */
 		op->new_field_len += mp_sizeof_str(key_len);
 	}
-	return 0;
+	return xrow_update_bar_commit(field);
 }
 
 int
@@ -250,7 +274,7 @@ xrow_update_op_do_nop_set(struct xrow_update_op *op,
 		if (mp_typeof(*field->bar.parent) == MP_MAP)
 			op->new_field_len += mp_sizeof_str(key_len);
 	}
-	return 0;
+	return xrow_update_bar_commit(field);
 }
 
 int
@@ -279,7 +303,7 @@ xrow_update_op_do_nop_delete(struct xrow_update_op *op,
 		field->bar.point -= key_len_or_index;
 		field->bar.point_size += key_len_or_index;
 	}
-	return 0;
+	return xrow_update_bar_commit(field);
 }
 
 #define DO_NOP_OP_GENERIC(op_type)						\
@@ -291,7 +315,9 @@ xrow_update_op_do_nop_##op_type(struct xrow_update_op *op,			\
 	int key_len_or_index;							\
 	if (xrow_update_bar_locate(op, field, &key_len_or_index) != 0)		\
 		return -1;							\
-	return xrow_update_op_do_##op_type(op, field->bar.point);		\
+	if (xrow_update_op_do_##op_type(op, field->bar.point) != 0)		\
+		return -1;							\
+	return xrow_update_bar_commit(field);					\
 }
 
 DO_NOP_OP_GENERIC(arith)
diff --git a/test/box/gh-5135-invalid-upsert.result b/test/box/gh-5135-invalid-upsert.result
new file mode 100644
index 000000000..45e85cdf5
--- /dev/null
+++ b/test/box/gh-5135-invalid-upsert.result
@@ -0,0 +1,146 @@
+-- test-run result file version 2
+--
+-- gh-5135: there was a crash in case upsert operation was
+-- invalid with an error appeared not in the operation, but in
+-- something else. For example, bad json. Or json type mismatching
+-- field type (attempt to do [...] on a scalar field). The
+-- expected behaviour is that such operations are just skipped.
+-- All the crashes were related to a so called 'bar' update. When
+-- an operation has a json path not interleaving with any other
+-- path.
+-- In all tests no crash should happen + not less importantly the
+-- bad operation should be nop, and should not affect other
+-- operations.
+--
+
+ops = {}
+ | ---
+ | ...
+ops[1] = {'!', 1, 1}
+ | ---
+ | ...
+ops[3] = {'!', 3, 2}
+ | ---
+ | ...
+
+-- Duplicate in a map.
+t = box.tuple.new({{a = 100}})
+ | ---
+ | ...
+ops[2] = {'!', '[1].a', 200}
+ | ---
+ | ...
+t:upsert(ops)
+ | ---
+ | - [1, {'a': 100}, 2]
+ | ...
+
+-- Bad JSON when do '!'.
+ops[2] = {'!', '[1].a[crash]', 200}
+ | ---
+ | ...
+t:upsert(ops)
+ | ---
+ | - [1, {'a': 100}, 2]
+ | ...
+
+-- Bad JSON when do '='.
+t = box.tuple.new({{1}})
+ | ---
+ | ...
+ops[2] = {'!', '[1][crash]', 200}
+ | ---
+ | ...
+t:upsert(ops)
+ | ---
+ | - [1, [1], 2]
+ | ...
+
+-- Can't delete more than 1 field from map in one
+-- operation.
+t = box.tuple.new({{a = 100}})
+ | ---
+ | ...
+ops[2] = {'#', '[1].a', 2}
+ | ---
+ | ...
+t:upsert(ops)
+ | ---
+ | - [1, {'a': 100}, 2]
+ | ...
+
+-- Bad JSON in '#'
+ops[2] = {'#', '[1].a[crash]', 1}
+ | ---
+ | ...
+t:upsert(ops)
+ | ---
+ | - [1, {'a': 100}, 2]
+ | ...
+
+-- Bad JSON in scalar operations.
+ops[2] = {'+', '[1].a[crash]', 1}
+ | ---
+ | ...
+t:upsert(ops)
+ | ---
+ | - [1, {'a': 100}, 2]
+ | ...
+t = box.tuple.new({{1}})
+ | ---
+ | ...
+ops[2] = {'&', '[1][crash]', 2}
+ | ---
+ | ...
+t:upsert(ops)
+ | ---
+ | - [1, [1], 2]
+ | ...
+
+-- Several fields, multiple operations, path
+-- interleaving.
+t = box.tuple.new({{1}, {2}})
+ | ---
+ | ...
+t:upsert({{'+', '[2][1]', 1}, {'&', '[1][crash]', 2}, {'=', '[3]', {4}}})
+ | ---
+ | - [[1], [3], [4]]
+ | ...
+
+t = box.tuple.new({ { { { 1 } }, { {a = 2} } }, { 3 } })
+ | ---
+ | ...
+t:upsert({{'=', '[1][1][1]', 4}, {'!', '[1][2][1].a', 5}, {'-', '[2][1]', 4}})
+ | ---
+ | - [[[4], [{'a': 2}]], [-1]]
+ | ...
+
+-- A valid operation on top of invalid, for the
+-- same field.
+t:upsert({{'+', '[1][1][1].a', 10}, {'+', '[1][1][1]', -10}})
+ | ---
+ | - [[[[1]], [{'a': 2}]], [3]]
+ | ...
+
+-- Invalid operand of an arith operation. Also should turn into
+-- nop.
+t:upsert({{'+', '[1][1][1]', 10}})
+ | ---
+ | - [[[[1]], [{'a': 2}]], [3]]
+ | ...
+-- This should be correct.
+t:upsert({{'+', '[1][1][1][1]', 10}})
+ | ---
+ | - [[[[11]], [{'a': 2}]], [3]]
+ | ...
+
+-- Check that invalid insertion can't screw indexes. I.e. next
+-- operations won't see the not added field.
+t = box.tuple.new({{1, 2, 3}})
+ | ---
+ | ...
+-- The second operation should change 2 to 12, not 20 to 30.
+t:upsert({{'!', '[1][2][100]', 20}, {'+', '[1][2]', 10}})
+ | ---
+ | - [[1, 12, 3]]
+ | ...
diff --git a/test/box/gh-5135-invalid-upsert.test.lua b/test/box/gh-5135-invalid-upsert.test.lua
new file mode 100644
index 000000000..8c01c9882
--- /dev/null
+++ b/test/box/gh-5135-invalid-upsert.test.lua
@@ -0,0 +1,72 @@
+--
+-- gh-5135: there was a crash in case upsert operation was
+-- invalid with an error appeared not in the operation, but in
+-- something else. For example, bad json. Or json type mismatching
+-- field type (attempt to do [...] on a scalar field). The
+-- expected behaviour is that such operations are just skipped.
+-- All the crashes were related to a so called 'bar' update. When
+-- an operation has a json path not interleaving with any other
+-- path.
+-- In all tests no crash should happen + not less importantly the
+-- bad operation should be nop, and should not affect other
+-- operations.
+--
+
+ops = {}
+ops[1] = {'!', 1, 1}
+ops[3] = {'!', 3, 2}
+
+-- Duplicate in a map.
+t = box.tuple.new({{a = 100}})
+ops[2] = {'!', '[1].a', 200}
+t:upsert(ops)
+
+-- Bad JSON when do '!'.
+ops[2] = {'!', '[1].a[crash]', 200}
+t:upsert(ops)
+
+-- Bad JSON when do '='.
+t = box.tuple.new({{1}})
+ops[2] = {'!', '[1][crash]', 200}
+t:upsert(ops)
+
+-- Can't delete more than 1 field from map in one
+-- operation.
+t = box.tuple.new({{a = 100}})
+ops[2] = {'#', '[1].a', 2}
+t:upsert(ops)
+
+-- Bad JSON in '#'
+ops[2] = {'#', '[1].a[crash]', 1}
+t:upsert(ops)
+
+-- Bad JSON in scalar operations.
+ops[2] = {'+', '[1].a[crash]', 1}
+t:upsert(ops)
+t = box.tuple.new({{1}})
+ops[2] = {'&', '[1][crash]', 2}
+t:upsert(ops)
+
+-- Several fields, multiple operations, path
+-- interleaving.
+t = box.tuple.new({{1}, {2}})
+t:upsert({{'+', '[2][1]', 1}, {'&', '[1][crash]', 2}, {'=', '[3]', {4}}})
+
+t = box.tuple.new({ { { { 1 } }, { {a = 2} } }, { 3 } })
+t:upsert({{'=', '[1][1][1]', 4}, {'!', '[1][2][1].a', 5}, {'-', '[2][1]', 4}})
+
+-- A valid operation on top of invalid, for the
+-- same field.
+t:upsert({{'+', '[1][1][1].a', 10}, {'+', '[1][1][1]', -10}})
+
+-- Invalid operand of an arith operation. Also should turn into
+-- nop.
+t:upsert({{'+', '[1][1][1]', 10}})
+-- This should be correct.
+t:upsert({{'+', '[1][1][1][1]', 10}})
+
+-- Check that invalid insertion can't screw indexes. I.e. next
+-- operations won't see the not added field.
+t = box.tuple.new({{1, 2, 3}})
+-- The second operation should change 2 to 12, not 20 to 30.
+t:upsert({{'!', '[1][2][100]', 20}, {'+', '[1][2]', 10}})
diff --git a/test/box/update.result b/test/box/update.result
index 6ab1a5bc5..425cb8802 100644
--- a/test/box/update.result
+++ b/test/box/update.result
@@ -1035,53 +1035,150 @@ t4_map:update({{'!', '[4].a', 100}})
         450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}], {'a': 100}]
 ...
 -- Test errors.
-t:update({{'!', 'a', 100}}) -- No such field.
+ops = {{'!', 'a', 100}} -- No such field.
+---
+...
+t:update(ops)
+---
+- error: Field 'a' was not found in the tuple
+...
+t:upsert(ops)
 ---
 - error: Field 'a' was not found in the tuple
 ...
-t:update({{'!', 'f.a', 300}}) -- Key already exists.
+ops = {{'!', 'f.a', 300}} -- Key already exists.
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''f.a'' UPDATE error: the key exists already'
 ...
-t:update({{'!', 'f.c.f[0]', 3.5}}) -- No such index, too small.
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'!', 'f.c.f[0]', 3.5}} -- No such index, too small.
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''f.c.f[0]'' UPDATE error: invalid JSON in position 7'
 ...
-t:update({{'!', 'f.c.f[100]', 100}}) -- No such index, too big.
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'!', 'f.c.f[100]', 100}} -- No such index, too big.
+---
+...
+t:update(ops)
 ---
 - error: Field ''f.c.f[100]'' was not found in the tuple
 ...
-t:update({{'!', 'g[4][100]', 700}}) -- Insert index into map.
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'!', 'g[4][100]', 700}} -- Insert index into map.
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''g[4][100]'' UPDATE error: can not access by index a non-array field'
 ...
-t:update({{'!', 'g[1][1]', 300}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'!', 'g[1][1]', 300}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''g[1][1]'' UPDATE error: can not access by index a non-array field'
 ...
-t:update({{'!', 'f.g.a', 700}}) -- Insert key into array.
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'!', 'f.g.a', 700}} -- Insert key into array.
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''f.g.a'' UPDATE error: can not access by key a non-map field'
 ...
-t:update({{'!', 'f.g[1].a', 700}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'!', 'f.g[1].a', 700}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''f.g[1].a'' UPDATE error: can not access by key a non-map field'
 ...
-t:update({{'!', 'f[*].k', 20}}) -- 'Any' is not considered valid JSON.
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'!', 'f[*].k', 20}} -- 'Any' is not considered valid JSON.
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''f[*].k'' UPDATE error: invalid JSON in position 3'
 ...
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
 -- JSON error after the not existing field to insert.
-t:update({{'!', '[2].e.100000', 100}})
+ops = {{'!', '[2].e.100000', 100}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[2].e.100000'' UPDATE error: invalid JSON in position 7'
 ...
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
 -- Correct JSON, but next to last field does not exist. '!' can't
 -- create the whole path.
-t:update({{'!', '[2].e.f', 100}})
+ops = {{'!', '[2].e.f', 100}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''[2].e.f'' was not found in the tuple
 ...
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
 --
 -- =
 --
@@ -1136,28 +1233,73 @@ t4_map:update({{'=', '[4]["a"]', 100}})
         450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}], {'a': 100}]
 ...
 -- Test errors.
-t:update({{'=', 'f.a[1]', 100}})
+ops = {{'=', 'f.a[1]', 100}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''f.a[1]'' UPDATE error: can not access by index a non-array field'
 ...
-t:update({{'=', 'f.a.k', 100}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'=', 'f.a.k', 100}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''f.a.k'' UPDATE error: can not access by key a non-map field'
 ...
-t:update({{'=', 'f.c.f[1]', 100}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'=', 'f.c.f[1]', 100}}
+---
+...
+t:update(ops)
 ---
 - [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [100, 5, 6, 7, 8], 'e': 500,
       'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
         450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
 ...
-t:update({{'=', 'f.c.f[100]', 100}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [100, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'=', 'f.c.f[100]', 100}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''f.c.f[100]'' was not found in the tuple
 ...
-t:update({{'=', '[2].c.f 1 1 1 1', 100}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'=', '[2].c.f 1 1 1 1', 100}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[2].c.f 1 1 1 1'' UPDATE error: invalid JSON in position 8'
 ...
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
 --
 -- #
 --
@@ -1198,23 +1340,64 @@ t:update({{'#', 'f.c.f[5]', 2}})
         450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
 ...
 -- Test errors.
-t:update({{'#', 'f.h', 1}})
+ops = {{'#', 'f.h', 1}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''f.h'' was not found in the tuple
 ...
-t:update({{'#', 'f.c.f[100]', 1}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'#', 'f.c.f[100]', 1}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''f.c.f[100]'' was not found in the tuple
 ...
-t:update({{'#', 'f.b', 2}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'#', 'f.b', 2}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''f.b'' UPDATE error: can delete only 1 field from a map in a row'
 ...
-t:update({{'#', 'f.b', 0}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'#', 'f.b', 0}}
+---
+...
+t:update(ops)
+---
+- error: 'Field ''f.b'' UPDATE error: cannot delete 0 fields'
+...
+t:upsert(ops)
 ---
 - error: 'Field ''f.b'' UPDATE error: cannot delete 0 fields'
 ...
-t:update({{'#', 'f', 0}})
+ops = {{'#', 'f', 0}}
+---
+...
+t:update(ops)
+---
+- error: 'Field ''f'' UPDATE error: cannot delete 0 fields'
+...
+t:upsert(ops)
 ---
 - error: 'Field ''f'' UPDATE error: cannot delete 0 fields'
 ...
@@ -1249,23 +1432,59 @@ t2:update({{':', '[4].str', 2, 2, 'e'}})
         450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}], {'str': 'aed'}]
 ...
 -- Test errors.
-t:update({{'+', 'g[3]', 50}})
+ops = {{'+', 'g[3]', 50}}
+---
+...
+t:update(ops)
 ---
 - error: 'Argument type in operation ''+'' on field ''g[3]'' does not match field
     type: expected a number'
 ...
-t:update({{'+', '[2].b.......', 100}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'+', '[2].b.......', 100}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[2].b.......'' UPDATE error: invalid JSON in position 7'
 ...
-t:update({{'+', '[2].b.c.d.e', 100}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'+', '[2].b.c.d.e', 100}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''[2].b.c.d.e'' was not found in the tuple
 ...
-t:update({{'-', '[2][*]', 20}})
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+ops = {{'-', '[2][*]', 20}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[2][*]'' UPDATE error: invalid JSON in position 5'
 ...
+t:upsert(ops)
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+      'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+        450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
 -- Vinyl normalizes field numbers. It should not touch paths,
 -- and they should not affect squashing.
 format = {}
@@ -1413,34 +1632,82 @@ t:update({{'=', '[4][4][1]', 4.5}, {'=', '[4][4][2]', 5.5}, {'=', '[4][3]', 3.5}
         17, 18, 19]], 20, {'b': 22, 'a': 21, 'c': {'d': 23, 'e': 24}}, 25]]
 ...
 -- Test errors.
-t:update({{'=', '[4][4][5][1]', 8000}, {'=', '[4][4][5].a', -1}})
+ops = {{'=', '[4][4][5][1]', 8000}, {'=', '[4][4][5].a', -1}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[4][4][5].a'' UPDATE error: can not update array by non-integer
     index'
 ...
-t:update({{'=', '[4][4][5][1]', 8000}, {'=', '[4][4][*].a', -1}})
+t:upsert(ops)
+---
+- [1, {}, [], [1, 2, 3, [4, 5, 6, 7, [8000, 9, [10, 11, 12], 13, 14], 15, 16, [17,
+        18, 19]], 20, {'b': 22, 'a': 21, 'c': {'d': 23, 'e': 24}}, 25]]
+...
+ops = {{'=', '[4][4][5][1]', 8000}, {'=', '[4][4][*].a', -1}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[4][4][*].a'' UPDATE error: can not update array by non-integer
     index'
 ...
-t:update({{'=', '[4][4][*].b', 8000}, {'=', '[4][4][*].a', -1}})
+t:upsert(ops)
+---
+- [1, {}, [], [1, 2, 3, [4, 5, 6, 7, [8000, 9, [10, 11, 12], 13, 14], 15, 16, [17,
+        18, 19]], 20, {'b': 22, 'a': 21, 'c': {'d': 23, 'e': 24}}, 25]]
+...
+ops = {{'=', '[4][4][*].b', 8000}, {'=', '[4][4][*].a', -1}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[4][4][*].b'' UPDATE error: invalid JSON in position 8'
 ...
-t:update({{'=', '[4][4][1]', 4000}, {'=', '[4][4]-badjson', -1}})
+t:upsert(ops)
+---
+- [1, {}, [], [1, 2, 3, [4, 5, 6, 7, [8, 9, [10, 11, 12], 13, 14], 15, 16, [17, 18,
+        19]], 20, {'b': 22, 'a': 21, 'c': {'d': 23, 'e': 24}}, 25]]
+...
+ops = {{'=', '[4][4][1]', 4000}, {'=', '[4][4]-badjson', -1}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[4][4]-badjson'' UPDATE error: invalid JSON in position 7'
 ...
-t:update({{'=', '[4][4][1]', 1}, {'=', '[4][4][1]', 2}})
+t:upsert(ops)
+---
+- [1, {}, [], [1, 2, 3, [4000, 5, 6, 7, [8, 9, [10, 11, 12], 13, 14], 15, 16, [17,
+        18, 19]], 20, {'b': 22, 'a': 21, 'c': {'d': 23, 'e': 24}}, 25]]
+...
+ops = {{'=', '[4][4][1]', 1}, {'=', '[4][4][1]', 2}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[4][4][1]'' UPDATE error: double update of the same field'
 ...
+t:upsert(ops)
+---
+- [1, {}, [], [1, 2, 3, [1, 5, 6, 7, [8, 9, [10, 11, 12], 13, 14], 15, 16, [17, 18,
+        19]], 20, {'b': 22, 'a': 21, 'c': {'d': 23, 'e': 24}}, 25]]
+...
 -- First two operations produce zero length bar update in field
 -- [4][4][5][4]. The third operation should raise an error.
-t:update({{'+', '[4][4][5][4]', 13000}, {'=', '[4][4][5][1]', 8000}, {'+', '[4][4][5][4]', 1}})
+ops = {{'+', '[4][4][5][4]', 13000}, {'=', '[4][4][5][1]', 8000}, {'+', '[4][4][5][4]', 1}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[4][4][5][4]'' UPDATE error: double update of the same field'
 ...
+t:upsert(ops)
+---
+- [1, {}, [], [1, 2, 3, [4, 5, 6, 7, [8000, 9, [10, 11, 12], 13013, 14], 15, 16, [
+        17, 18, 19]], 20, {'b': 22, 'a': 21, 'c': {'d': 23, 'e': 24}}, 25]]
+...
 --
 -- !
 --
@@ -1481,17 +1748,33 @@ t:update({{'!', '[4][3]', 2.5}, {'+', '[4][5][5][5]', 0.75}, {'+', '[4][5][3]',
 ...
 -- Error - an attempt to follow JSON path in a scalar field
 -- during branching.
-t:update({{'+', '[4][3]', 0.5}, {'+', '[4][3][2]', 0.5}})
+ops = {{'+', '[4][3]', 0.5}, {'+', '[4][3][2]', 0.5}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''[4][3][2]'' was not found in the tuple
 ...
+t:upsert(ops)
+---
+- [1, {}, [], [1, 2, 3.5, [4, 5, 6, 7, [8, 9, [10, 11, 12], 13, 14], 15, 16, [17,
+        18, 19]], 20, {'b': 22, 'a': 21, 'c': {'d': 23, 'e': 24}}, 25]]
+...
 -- Error - array update should check that its token is a number.
 -- Parent field won't guarantee that.
-t:update({{'=', '[3]', {0}}, {'=', '[4][3]', 3.5}, {'=', '[4][4][1]', 4.5}, {'=', '[4][4][2]', 5.5}, {'=', '[4][4].key', 6.5}})
+ops = {{'=', '[3]', {0}}, {'=', '[4][3]', 3.5}, {'=', '[4][4][1]', 4.5}, {'=', '[4][4][2]', 5.5}, {'=', '[4][4].key', 6.5}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[4][4].key'' UPDATE error: can''t update an array by a non-numeric
     index'
 ...
+t:upsert(ops)
+---
+- [1, {}, [0], [1, 2, 3.5, [4.5, 5.5, 6, 7, [8, 9, [10, 11, 12], 13, 14], 15, 16,
+      [17, 18, 19]], 20, {'b': 22, 'a': 21, 'c': {'d': 23, 'e': 24}}, 25]]
+...
 --
 -- Intersecting map updates.
 --
@@ -1583,6 +1866,13 @@ t:update(ops)
 ---
 - error: 'Field ''[2].h.i.<badjson>'' UPDATE error: invalid JSON in position 9'
 ...
+t:upsert(ops)
+---
+- [1, {'b': 2000, 'c': [3, 4, 5], 'd': {0: 0, -1: -1, true: false, 'f': 7, 'e': 6},
+    'h': {'i': {'k': 15000, 'j': 14000}, 'm': 16000, -1: [[[[-1], -1], -1], -1]},
+    'a': 1, 'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
 ops[#ops][1] = '!'
 ---
 ...
@@ -1590,6 +1880,13 @@ t:update(ops)
 ---
 - error: 'Field ''[2].h.i.<badjson>'' UPDATE error: invalid JSON in position 9'
 ...
+t:upsert(ops)
+---
+- [1, {'b': 2000, 'c': [3, 4, 5], 'd': {0: 0, -1: -1, true: false, 'f': 7, 'e': 6},
+    'h': {'i': {'k': 15000, 'j': 14000}, 'm': 16000, -1: [[[[-1], -1], -1], -1]},
+    'a': 1, 'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
 ops[#ops][1] = '#'
 ---
 ...
@@ -1600,6 +1897,13 @@ t:update(ops)
 ---
 - error: 'Field ''[2].h.i.<badjson>'' UPDATE error: invalid JSON in position 9'
 ...
+t:upsert(ops)
+---
+- [1, {'b': 2000, 'c': [3, 4, 5], 'd': {0: 0, -1: -1, true: false, 'f': 7, 'e': 6},
+    'h': {'i': {'k': 15000, 'j': 14000}, 'm': 16000, -1: [[[[-1], -1], -1], -1]},
+    'a': 1, 'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
 -- Key extractor should reject any attempt to use non-string keys.
 ops[#ops] = {'-', '[2].h.i[20]', -1}
 ---
@@ -1609,6 +1913,13 @@ t:update(ops)
 - error: 'Field ''[2].h.i[20]'' UPDATE error: can''t update a map by not a string
     key'
 ...
+t:upsert(ops)
+---
+- [1, {'b': 2000, 'c': [3, 4, 5], 'd': {0: 0, -1: -1, true: false, 'f': 7, 'e': 6},
+    'h': {'i': {'k': 15000, 'j': 14000}, 'm': 16000, -1: [[[[-1], -1], -1], -1]},
+    'a': 1, 'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
 ops[#ops][1] = '='
 ---
 ...
@@ -1617,33 +1928,100 @@ t:update(ops)
 - error: 'Field ''[2].h.i[20]'' UPDATE error: can''t update a map by not a string
     key'
 ...
-t:update({{'+', '[2].d.e', 1}, {'+', '[2].d.f', 1}, {'+', '[2].d.k', 1}})
+t:upsert(ops)
+---
+- [1, {'b': 2000, 'c': [3, 4, 5], 'd': {0: 0, -1: -1, true: false, 'f': 7, 'e': 6},
+    'h': {'i': {'k': 15000, 'j': 14000}, 'm': 16000, -1: [[[[-1], -1], -1], -1]},
+    'a': 1, 'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
+ops = {{'+', '[2].d.e', 1}, {'+', '[2].d.f', 1}, {'+', '[2].d.k', 1}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''[2].d.k'' was not found in the tuple
 ...
-t:update({{'=', '[2].d.e', 6000}, {'!', '[2].d.g.h', -1}})
+t:upsert(ops)
+---
+- [1, {'b': 2, 'a': 1, 'd': {0: 0, true: false, 'f': 8, -1: -1, 'e': 7}, 'h': {'i': {
+        'j': 14, 'k': 15}, -1: [[[[-1], -1], -1], -1], 'm': 16}, 'c': [3, 4, 5], 'g': {
+      1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
+ops = {{'=', '[2].d.e', 6000}, {'!', '[2].d.g.h', -1}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''[2].d.g.h'' was not found in the tuple
 ...
-t:update({{'!', '[2].d.g', 6000}, {'!', '[2].d.e', -1}})
+t:upsert(ops)
+---
+- [1, {'b': 2, 'a': 1, 'd': {0: 0, 'f': 7, -1: -1, true: false, 'e': 6000}, 'h': {
+      'i': {'j': 14, 'k': 15}, -1: [[[[-1], -1], -1], -1], 'm': 16}, 'c': [3, 4, 5],
+    'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
+ops = {{'!', '[2].d.g', 6000}, {'!', '[2].d.e', -1}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[2].d.e'' UPDATE error: the key exists already'
 ...
-t:update({{'=', '[2].d.g', 6000}, {'#', '[2].d.old', 10}})
+t:upsert(ops)
+---
+- [1, {'b': 2, 'a': 1, 'd': {0: 0, true: false, 'f': 7, -1: -1, 'e': 6, 'g': 6000},
+    'h': {'i': {'j': 14, 'k': 15}, -1: [[[[-1], -1], -1], -1], 'm': 16}, 'c': [3,
+      4, 5], 'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
+ops = {{'=', '[2].d.g', 6000}, {'#', '[2].d.old', 10}}
+---
+...
+t:update(ops)
 ---
 - error: 'Field ''[2].d.old'' UPDATE error: can delete only 1 field from a map in
     a row'
 ...
+t:upsert(ops)
+---
+- [1, {'b': 2, 'a': 1, 'd': {0: 0, 'f': 7, 'e': 6, -1: -1, true: false, 'g': 6000},
+    'h': {'i': {'j': 14, 'k': 15}, -1: [[[[-1], -1], -1], -1], 'm': 16}, 'c': [3,
+      4, 5], 'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
 -- '!'/'=' can be used to create a field, but only if it is a
 -- tail. These operations can't create the whole path.
-t:update({{'=', '[2].d.g', 6000}, {'=', '[2].d.new.new', -1}})
+ops = {{'=', '[2].d.g', 6000}, {'=', '[2].d.new.new', -1}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''[2].d.new.new'' was not found in the tuple
 ...
-t:update({{'=', '[2].d.g', 6000}, {'#', '[2].d.old.old', 1}})
+t:upsert(ops)
+---
+- [1, {'b': 2, 'a': 1, 'd': {0: 0, 'f': 7, 'e': 6, -1: -1, true: false, 'g': 6000},
+    'h': {'i': {'j': 14, 'k': 15}, -1: [[[[-1], -1], -1], -1], 'm': 16}, 'c': [3,
+      4, 5], 'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
+ops = {{'=', '[2].d.g', 6000}, {'#', '[2].d.old.old', 1}}
+---
+...
+t:update(ops)
 ---
 - error: Field ''[2].d.old.old'' was not found in the tuple
 ...
+t:upsert(ops)
+---
+- [1, {'b': 2, 'a': 1, 'd': {0: 0, 'f': 7, 'e': 6, -1: -1, true: false, 'g': 6000},
+    'h': {'i': {'j': 14, 'k': 15}, -1: [[[[-1], -1], -1], -1], 'm': 16}, 'c': [3,
+      4, 5], 'g': {1: {'k': 8, 'v': 9}, 2: {'k': 10, 'v': 11}, 3: {'k': '12str', 'v': '13str'},
+      null: null}}, []]
+...
 s:drop()
 ---
 ...
diff --git a/test/box/update.test.lua b/test/box/update.test.lua
index 1538cae9c..27cf55467 100644
--- a/test/box/update.test.lua
+++ b/test/box/update.test.lua
@@ -343,20 +343,52 @@ t2:update({{'!', 'g[6][1]', 50}})
 t4_array:update({{'!', '[4][1]', 100}})
 t4_map:update({{'!', '[4].a', 100}})
 -- Test errors.
-t:update({{'!', 'a', 100}}) -- No such field.
-t:update({{'!', 'f.a', 300}}) -- Key already exists.
-t:update({{'!', 'f.c.f[0]', 3.5}}) -- No such index, too small.
-t:update({{'!', 'f.c.f[100]', 100}}) -- No such index, too big.
-t:update({{'!', 'g[4][100]', 700}}) -- Insert index into map.
-t:update({{'!', 'g[1][1]', 300}})
-t:update({{'!', 'f.g.a', 700}}) -- Insert key into array.
-t:update({{'!', 'f.g[1].a', 700}})
-t:update({{'!', 'f[*].k', 20}}) -- 'Any' is not considered valid JSON.
+ops = {{'!', 'a', 100}} -- No such field.
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'!', 'f.a', 300}} -- Key already exists.
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'!', 'f.c.f[0]', 3.5}} -- No such index, too small.
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'!', 'f.c.f[100]', 100}} -- No such index, too big.
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'!', 'g[4][100]', 700}} -- Insert index into map.
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'!', 'g[1][1]', 300}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'!', 'f.g.a', 700}} -- Insert key into array.
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'!', 'f.g[1].a', 700}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'!', 'f[*].k', 20}} -- 'Any' is not considered valid JSON.
+t:update(ops)
+t:upsert(ops)
+
 -- JSON error after the not existing field to insert.
-t:update({{'!', '[2].e.100000', 100}})
+ops = {{'!', '[2].e.100000', 100}}
+t:update(ops)
+t:upsert(ops)
+
 -- Correct JSON, but next to last field does not exist. '!' can't
 -- create the whole path.
-t:update({{'!', '[2].e.f', 100}})
+ops = {{'!', '[2].e.f', 100}}
+t:update(ops)
+t:upsert(ops)
 
 --
 -- =
@@ -373,11 +405,25 @@ t:update({{'=', 'f.g[1]', 0}})
 t4_array:update({{'=', '[4][1]', 100}})
 t4_map:update({{'=', '[4]["a"]', 100}})
 -- Test errors.
-t:update({{'=', 'f.a[1]', 100}})
-t:update({{'=', 'f.a.k', 100}})
-t:update({{'=', 'f.c.f[1]', 100}})
-t:update({{'=', 'f.c.f[100]', 100}})
-t:update({{'=', '[2].c.f 1 1 1 1', 100}})
+ops = {{'=', 'f.a[1]', 100}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', 'f.a.k', 100}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', 'f.c.f[1]', 100}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', 'f.c.f[100]', 100}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', '[2].c.f 1 1 1 1', 100}}
+t:update(ops)
+t:upsert(ops)
 
 --
 -- #
@@ -389,11 +435,25 @@ t:update({{'#', 'f.c.f[1]', 100}})
 t:update({{'#', 'f.c.f[5]', 1}})
 t:update({{'#', 'f.c.f[5]', 2}})
 -- Test errors.
-t:update({{'#', 'f.h', 1}})
-t:update({{'#', 'f.c.f[100]', 1}})
-t:update({{'#', 'f.b', 2}})
-t:update({{'#', 'f.b', 0}})
-t:update({{'#', 'f', 0}})
+ops = {{'#', 'f.h', 1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'#', 'f.c.f[100]', 1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'#', 'f.b', 2}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'#', 'f.b', 0}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'#', 'f', 0}}
+t:update(ops)
+t:upsert(ops)
 
 --
 -- Scalar operations.
@@ -404,10 +464,21 @@ t:update({{'&', 'f.c.f[2]', 4}})
 t2 = t:update({{'=', 4, {str = 'abcd'}}})
 t2:update({{':', '[4].str', 2, 2, 'e'}})
 -- Test errors.
-t:update({{'+', 'g[3]', 50}})
-t:update({{'+', '[2].b.......', 100}})
-t:update({{'+', '[2].b.c.d.e', 100}})
-t:update({{'-', '[2][*]', 20}})
+ops = {{'+', 'g[3]', 50}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'+', '[2].b.......', 100}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'+', '[2].b.c.d.e', 100}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'-', '[2][*]', 20}}
+t:update(ops)
+t:upsert(ops)
 
 -- Vinyl normalizes field numbers. It should not touch paths,
 -- and they should not affect squashing.
@@ -488,14 +559,31 @@ t:update({{'=', '[4][4][5][3][2]', 11000}, {'=', '[4][4][5][3][1]', 10000}, {'='
 -- delete the route and replace it with a full featured array.
 t:update({{'=', '[4][4][1]', 4.5}, {'=', '[4][4][2]', 5.5}, {'=', '[4][3]', 3.5}})
 -- Test errors.
-t:update({{'=', '[4][4][5][1]', 8000}, {'=', '[4][4][5].a', -1}})
-t:update({{'=', '[4][4][5][1]', 8000}, {'=', '[4][4][*].a', -1}})
-t:update({{'=', '[4][4][*].b', 8000}, {'=', '[4][4][*].a', -1}})
-t:update({{'=', '[4][4][1]', 4000}, {'=', '[4][4]-badjson', -1}})
-t:update({{'=', '[4][4][1]', 1}, {'=', '[4][4][1]', 2}})
+ops = {{'=', '[4][4][5][1]', 8000}, {'=', '[4][4][5].a', -1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', '[4][4][5][1]', 8000}, {'=', '[4][4][*].a', -1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', '[4][4][*].b', 8000}, {'=', '[4][4][*].a', -1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', '[4][4][1]', 4000}, {'=', '[4][4]-badjson', -1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', '[4][4][1]', 1}, {'=', '[4][4][1]', 2}}
+t:update(ops)
+t:upsert(ops)
+
 -- First two operations produce zero length bar update in field
 -- [4][4][5][4]. The third operation should raise an error.
-t:update({{'+', '[4][4][5][4]', 13000}, {'=', '[4][4][5][1]', 8000}, {'+', '[4][4][5][4]', 1}})
+ops = {{'+', '[4][4][5][4]', 13000}, {'=', '[4][4][5][1]', 8000}, {'+', '[4][4][5][4]', 1}}
+t:update(ops)
+t:upsert(ops)
 
 --
 -- !
@@ -516,10 +604,14 @@ t:update({{'+', '[4][3]', 0.5}, {'+', '[4][4][5][5]', 0.75}, {'+', '[4][4][3]',
 t:update({{'!', '[4][3]', 2.5}, {'+', '[4][5][5][5]', 0.75}, {'+', '[4][5][3]', 0.25}})
 -- Error - an attempt to follow JSON path in a scalar field
 -- during branching.
-t:update({{'+', '[4][3]', 0.5}, {'+', '[4][3][2]', 0.5}})
+ops = {{'+', '[4][3]', 0.5}, {'+', '[4][3][2]', 0.5}}
+t:update(ops)
+t:upsert(ops)
 -- Error - array update should check that its token is a number.
 -- Parent field won't guarantee that.
-t:update({{'=', '[3]', {0}}, {'=', '[4][3]', 3.5}, {'=', '[4][4][1]', 4.5}, {'=', '[4][4][2]', 5.5}, {'=', '[4][4].key', 6.5}})
+ops = {{'=', '[3]', {0}}, {'=', '[4][3]', 3.5}, {'=', '[4][4][1]', 4.5}, {'=', '[4][4][2]', 5.5}, {'=', '[4][4].key', 6.5}}
+t:update(ops)
+t:upsert(ops)
 
 --
 -- Intersecting map updates.
@@ -576,25 +668,51 @@ ops = {{'=', '[2].h.i.j', 14000}, {'=', '[2].h.i.k', 15000}, {'=', '[2].h.m', 16
 -- implemented separately and tested both.
 ops[#ops] = {'+', '[2].h.i.<badjson>', -1}
 t:update(ops)
+t:upsert(ops)
+
 ops[#ops][1] = '!'
 t:update(ops)
+t:upsert(ops)
+
 ops[#ops][1] = '#'
 ops[#ops][3] = 1
 t:update(ops)
+t:upsert(ops)
+
 -- Key extractor should reject any attempt to use non-string keys.
 ops[#ops] = {'-', '[2].h.i[20]', -1}
 t:update(ops)
+t:upsert(ops)
+
 ops[#ops][1] = '='
 t:update(ops)
+t:upsert(ops)
+
+ops = {{'+', '[2].d.e', 1}, {'+', '[2].d.f', 1}, {'+', '[2].d.k', 1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', '[2].d.e', 6000}, {'!', '[2].d.g.h', -1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'!', '[2].d.g', 6000}, {'!', '[2].d.e', -1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', '[2].d.g', 6000}, {'#', '[2].d.old', 10}}
+t:update(ops)
+t:upsert(ops)
 
-t:update({{'+', '[2].d.e', 1}, {'+', '[2].d.f', 1}, {'+', '[2].d.k', 1}})
-t:update({{'=', '[2].d.e', 6000}, {'!', '[2].d.g.h', -1}})
-t:update({{'!', '[2].d.g', 6000}, {'!', '[2].d.e', -1}})
-t:update({{'=', '[2].d.g', 6000}, {'#', '[2].d.old', 10}})
 -- '!'/'=' can be used to create a field, but only if it is a
 -- tail. These operations can't create the whole path.
-t:update({{'=', '[2].d.g', 6000}, {'=', '[2].d.new.new', -1}})
-t:update({{'=', '[2].d.g', 6000}, {'#', '[2].d.old.old', 1}})
+ops = {{'=', '[2].d.g', 6000}, {'=', '[2].d.new.new', -1}}
+t:update(ops)
+t:upsert(ops)
+
+ops = {{'=', '[2].d.g', 6000}, {'#', '[2].d.old.old', 1}}
+t:update(ops)
+t:upsert(ops)
 
 s:drop()
 
-- 
2.21.1 (Apple Git-122.3)



More information about the Tarantool-patches mailing list