[Tarantool-patches] [PATCH 1/1] tuple: enable isolated JSON updates
Vladislav Shpilevoy
v.shpilevoy at tarantool.org
Fri Nov 22 02:19:11 MSK 2019
Isolated tuple update is an update by JSON path, which hasn't a
common prefix with any other JSON update operation in the same
set. For example, these JSON update operations are isolated:
{'=', '[1][2][3]', 100},
{'+', '[2].b.c', 200}
Their JSON paths has no a common prefix. But these operations are
not isolated:
{'=', '[1][2][3]', 100},
{'+', '[1].b.c', 200}
They have a common prefix '[1]'.
Isolated updates are a first part of fully functional JSON
updates. Their feature is that their implementation is relatively
simple and lightweight - an isolated JSON update does not store
each part of the JSON path as a separate object. Isolated update
stores just string with JSON and pointer to the MessagePack
object to update.
Such isolated updates are called 'bar update'. They are a basic
brick of more complex JSON updates.
Part of #1261
---
Branch: https://github.com/tarantool/tarantool/tree/gerold103/gh-1261-json-bar-update
Issue: https://github.com/tarantool/tarantool/issues/1261
src/box/CMakeLists.txt | 1 +
src/box/vinyl.c | 16 +-
src/box/xrow_update.c | 41 +++-
src/box/xrow_update_array.c | 22 +-
src/box/xrow_update_bar.c | 454 +++++++++++++++++++++++++++++++++++
src/box/xrow_update_field.c | 45 +++-
src/box/xrow_update_field.h | 115 +++++++++
test/box/update.result | 410 ++++++++++++++++++++++++++++++-
test/box/update.test.lua | 145 +++++++++++
test/engine/update.result | 5 -
test/engine/update.test.lua | 2 -
test/unit/column_mask.c | 75 +++++-
test/unit/column_mask.result | 8 +-
13 files changed, 1314 insertions(+), 25 deletions(-)
create mode 100644 src/box/xrow_update_bar.c
diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 5cd5cba81..fc9d1a3e8 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -43,6 +43,7 @@ add_library(tuple STATIC
xrow_update.c
xrow_update_field.c
xrow_update_array.c
+ xrow_update_bar.c
tuple_compare.cc
tuple_extract_key.cc
tuple_hash.cc
diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index 767e40006..15a136f81 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -2004,13 +2004,25 @@ request_normalize_ops(struct request *request)
ops_end = mp_encode_str(ops_end, op_name, op_name_len);
int field_no;
- if (mp_typeof(*pos) == MP_INT) {
+ const char *field_name;
+ switch (mp_typeof(*pos)) {
+ case MP_INT:
field_no = mp_decode_int(&pos);
ops_end = mp_encode_int(ops_end, field_no);
- } else {
+ break;
+ case MP_UINT:
field_no = mp_decode_uint(&pos);
field_no -= request->index_base;
ops_end = mp_encode_uint(ops_end, field_no);
+ break;
+ case MP_STR:
+ field_name = pos;
+ mp_next(&pos);
+ memcpy(ops_end, field_name, pos - field_name);
+ ops_end += pos - field_name;
+ break;
+ default:
+ unreachable();
}
if (*op_name == ':') {
diff --git a/src/box/xrow_update.c b/src/box/xrow_update.c
index 123db081a..ecda17544 100644
--- a/src/box/xrow_update.c
+++ b/src/box/xrow_update.c
@@ -116,7 +116,12 @@ struct xrow_update
* re-encode each Lua update with 0-based indexes.
*/
int index_base;
- /** A bitmask of all columns modified by this update. */
+ /**
+ * A bitmask of all columns modified by this update. Only
+ * the first level of a tuple is accounted here. I.e. if
+ * a field [1][2][3] was updated, then only [1] is
+ * reflected.
+ */
uint64_t column_mask;
/** First level of update tree. It is always array. */
struct xrow_update_field root;
@@ -182,9 +187,26 @@ xrow_update_read_ops(struct xrow_update *update, const char *expr,
*/
if (column_mask != COLUMN_MASK_FULL) {
int32_t field_no;
+ char opcode;
+ if (xrow_update_op_is_term(op)) {
+ opcode = op->opcode;
+ } else {
+ /*
+ * When a field is not terminal,
+ * on the first level it for sure
+ * changes only one field and in
+ * terms of column mask is
+ * equivalent to any scalar
+ * operation. Even if it was '!'
+ * or '#'. Zero means, that it
+ * won't match any checks with
+ * non-scalar operations below.
+ */
+ opcode = 0;
+ }
if (op->field_no >= 0)
field_no = op->field_no;
- else if (op->opcode != '!')
+ else if (opcode != '!')
field_no = field_count_hint + op->field_no;
else
/*
@@ -227,12 +249,12 @@ xrow_update_read_ops(struct xrow_update *update, const char *expr,
* hint. It is used to translate negative
* field numbers into positive ones.
*/
- if (op->opcode == '!')
+ if (opcode == '!')
++field_count_hint;
- else if (op->opcode == '#')
+ else if (opcode == '#')
field_count_hint -= (int32_t) op->arg.del.count;
- if (op->opcode == '!' || op->opcode == '#')
+ if (opcode == '!' || opcode == '#')
/*
* If the operation is insertion
* or deletion then it potentially
@@ -412,6 +434,15 @@ xrow_upsert_squash(const char *expr1, const char *expr1_end,
if (op->opcode != '+' && op->opcode != '-' &&
op->opcode != '=')
return NULL;
+ /*
+ * Not terminal operation means, that the
+ * update is not flat, and squash would
+ * need to build a tree of operations to
+ * find matches. That is too complex,
+ * squash is skipped.
+ */
+ if (! xrow_update_op_is_term(op))
+ return NULL;
if (op->field_no <= prev_field_no)
return NULL;
prev_field_no = op->field_no;
diff --git a/src/box/xrow_update_array.c b/src/box/xrow_update_array.c
index 7f198076b..1cc49f861 100644
--- a/src/box/xrow_update_array.c
+++ b/src/box/xrow_update_array.c
@@ -220,12 +220,20 @@ xrow_update_op_do_array_insert(struct xrow_update_op *op,
struct xrow_update_field *field)
{
assert(field->type == XUPDATE_ARRAY);
+ struct xrow_update_array_item *item;
+ if (! xrow_update_op_is_term(op)) {
+ item = xrow_update_array_extract_item(field, op);
+ if (item == NULL)
+ return -1;
+ return xrow_update_op_do_field_insert(op, &item->field);
+ }
+
struct xrow_update_rope *rope = field->array.rope;
uint32_t size = xrow_update_rope_size(rope);
if (xrow_update_op_adjust_field_no(op, size + 1) != 0)
return -1;
- struct xrow_update_array_item *item = (struct xrow_update_array_item *)
+ item = (struct xrow_update_array_item *)
xrow_update_alloc(rope->ctx, sizeof(*item));
if (item == NULL)
return -1;
@@ -248,6 +256,8 @@ xrow_update_op_do_array_set(struct xrow_update_op *op,
xrow_update_array_extract_item(field, op);
if (item == NULL)
return -1;
+ if (! xrow_update_op_is_term(op))
+ return xrow_update_op_do_field_set(op, &item->field);
op->new_field_len = op->arg.set.length;
/*
* Ignore the previous op, if any. It is not correct,
@@ -265,6 +275,14 @@ xrow_update_op_do_array_delete(struct xrow_update_op *op,
struct xrow_update_field *field)
{
assert(field->type == XUPDATE_ARRAY);
+ if (! xrow_update_op_is_term(op)) {
+ struct xrow_update_array_item *item =
+ xrow_update_array_extract_item(field, op);
+ if (item == NULL)
+ return -1;
+ return xrow_update_op_do_field_delete(op, &item->field);
+ }
+
struct xrow_update_rope *rope = field->array.rope;
uint32_t size = xrow_update_rope_size(rope);
if (xrow_update_op_adjust_field_no(op, size) != 0)
@@ -287,6 +305,8 @@ xrow_update_op_do_array_##op_type(struct xrow_update_op *op, \
xrow_update_array_extract_item(field, op); \
if (item == NULL) \
return -1; \
+ if (! xrow_update_op_is_term(op)) \
+ return xrow_update_op_do_field_##op_type(op, &item->field); \
if (item->field.type != XUPDATE_NOP) \
return xrow_update_err_double(op); \
if (xrow_update_op_do_##op_type(op, item->field.data) != 0) \
diff --git a/src/box/xrow_update_bar.c b/src/box/xrow_update_bar.c
new file mode 100644
index 000000000..737673e8a
--- /dev/null
+++ b/src/box/xrow_update_bar.c
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials
+ * provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "xrow_update_field.h"
+#include "tuple.h"
+
+/**
+ * Locate a field to update by @a op's JSON path and initialize
+ * @a field as a bar update.
+ *
+ * @param op Update operation.
+ * @param field Field to locate in.
+ * @param[out] key_len_or_index One parameter for two values,
+ * depending on where the target point is located: in an
+ * array or a map. In case of map it is size of a key
+ * before the found point. It is used to find range of the
+ * both key and value in '#' operation to drop the pair.
+ * In case of array it is index of the array element to be
+ * able to check how many fields are left for deletion.
+ *
+ * @retval 0 Success.
+ * @retval -1 Not found or invalid JSON.
+ */
+static inline int
+xrow_update_bar_locate(struct xrow_update_op *op,
+ struct xrow_update_field *field,
+ int *key_len_or_index)
+{
+ /*
+ * Bar update is not flat by definition. It always has a
+ * non empty path. This is why op is expected to be not
+ * terminal.
+ */
+ assert(! xrow_update_op_is_term(op));
+ 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;
+ const char *pos = field->data;
+ struct json_token token;
+ while ((rc = json_lexer_next_token(&op->lexer, &token)) == 0 &&
+ token.type != JSON_TOKEN_END) {
+
+ switch (token.type) {
+ case JSON_TOKEN_NUM:
+ field->bar.parent = pos;
+ *key_len_or_index = token.num;
+ rc = tuple_field_go_to_index(&pos, token.num);
+ break;
+ case JSON_TOKEN_STR:
+ field->bar.parent = pos;
+ *key_len_or_index = token.len;
+ rc = tuple_field_go_to_key(&pos, token.str, token.len);
+ break;
+ default:
+ assert(token.type == JSON_TOKEN_ANY);
+ rc = op->lexer.symbol_count - 1;
+ return xrow_update_err_bad_json(op, rc);
+ }
+ if (rc != 0)
+ return xrow_update_err_no_such_field(op);
+ }
+ if (rc > 0)
+ return xrow_update_err_bad_json(op, rc);
+
+ field->bar.point = pos;
+ mp_next(&pos);
+ field->bar.point_size = pos - field->bar.point;
+ return 0;
+}
+
+/**
+ * Locate an optional field to update by @a op's JSON path. If
+ * found or only a last path part is not found, initialize @a
+ * field as a bar update. Last path part may not exist and it is
+ * ok, for example, for '!' and '=' operations.
+ */
+static inline int
+xrow_update_bar_locate_opt(struct xrow_update_op *op,
+ struct xrow_update_field *field, bool *is_found,
+ int *key_len_or_index)
+{
+ /*
+ * Bar update is not flat by definition. It always has a
+ * non empty path. This is why op is expected to be not
+ * terminal.
+ */
+ assert(! xrow_update_op_is_term(op));
+ 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;
+ const char *pos = field->data;
+ struct json_token token;
+ do {
+ rc = json_lexer_next_token(&op->lexer, &token);
+ if (rc != 0)
+ return xrow_update_err_bad_json(op, rc);
+
+ switch (token.type) {
+ case JSON_TOKEN_END:
+ *is_found = true;
+ field->bar.point = pos;
+ mp_next(&pos);
+ field->bar.point_size = pos - field->bar.point;
+ return 0;
+ case JSON_TOKEN_NUM:
+ field->bar.parent = pos;
+ *key_len_or_index = token.num;
+ rc = tuple_field_go_to_index(&pos, token.num);
+ break;
+ case JSON_TOKEN_STR:
+ field->bar.parent = pos;
+ *key_len_or_index = token.len;
+ rc = tuple_field_go_to_key(&pos, token.str, token.len);
+ break;
+ default:
+ assert(token.type == JSON_TOKEN_ANY);
+ rc = op->lexer.symbol_count - 1;
+ return xrow_update_err_bad_json(op, rc);
+ }
+ } while (rc == 0);
+ assert(rc == -1);
+ /* Ensure, that 'token' is next to last path part. */
+ struct json_token tmp_token;
+ rc = json_lexer_next_token(&op->lexer, &tmp_token);
+ if (rc != 0)
+ return xrow_update_err_bad_json(op, rc);
+ if (tmp_token.type != JSON_TOKEN_END)
+ return xrow_update_err_no_such_field(op);
+
+ *is_found = false;
+ if (token.type == JSON_TOKEN_NUM) {
+ const char *tmp = field->bar.parent;
+ if (mp_typeof(*tmp) != MP_ARRAY) {
+ return xrow_update_err(op, "can not access by index a "\
+ "non-array field");
+ }
+ uint32_t size = mp_decode_array(&tmp);
+ if ((uint32_t) token.num > size)
+ return xrow_update_err_no_such_field(op);
+ /*
+ * The updated point is in an array, its position
+ * was not found, and its index is <= size. The
+ * only way how can that happen - the update tries
+ * to append a new array element. The following
+ * code tries to find the array's end.
+ */
+ assert((uint32_t) token.num == size);
+ if (field->bar.parent == field->data) {
+ /*
+ * Optimization for the case when the path
+ * is short. So parent of the updated
+ * point is the field itself. It allows
+ * not to decode anything at all. It is
+ * worth doing, since the paths are
+ * usually short.
+ */
+ field->bar.point = field->data + field->size;
+ } else {
+ field->bar.point = field->bar.parent;
+ mp_next(&field->bar.point);
+ }
+ field->bar.point_size = 0;
+ } else {
+ assert(token.type == JSON_TOKEN_STR);
+ field->bar.new_key = token.str;
+ field->bar.new_key_len = token.len;
+ if (mp_typeof(*field->bar.parent) != MP_MAP) {
+ return xrow_update_err(op, "can not access by key a "\
+ "non-map field");
+ }
+ }
+ return 0;
+}
+
+/**
+ * Nop fields are those which are not updated. And when they
+ * receive an update via one of xrow_update_op_do_nop_* functions,
+ * it means, that there is a non terminal path digging inside this
+ * not updated field. It turns nop field into a bar field. How
+ * exactly - depends on a concrete operation.
+ */
+
+int
+xrow_update_op_do_nop_insert(struct xrow_update_op *op,
+ struct xrow_update_field *field)
+{
+ assert(op->opcode == '!');
+ assert(field->type == XUPDATE_NOP);
+ bool is_found = false;
+ int key_len = 0;
+ if (xrow_update_bar_locate_opt(op, field, &is_found, &key_len) != 0)
+ return -1;
+ op->new_field_len = op->arg.set.length;
+ if (mp_typeof(*field->bar.parent) == MP_MAP) {
+ if (is_found)
+ return xrow_update_err_duplicate(op);
+ /*
+ * Don't forget, that map element is a pair. So
+ * key length also should be accounted.
+ */
+ op->new_field_len += mp_sizeof_str(key_len);
+ }
+ return 0;
+}
+
+int
+xrow_update_op_do_nop_set(struct xrow_update_op *op,
+ struct xrow_update_field *field)
+{
+ assert(op->opcode == '=');
+ assert(field->type == XUPDATE_NOP);
+ bool is_found = false;
+ int key_len = 0;
+ if (xrow_update_bar_locate_opt(op, field, &is_found, &key_len) != 0)
+ return -1;
+ op->new_field_len = op->arg.set.length;
+ if (! is_found) {
+ op->opcode = '!';
+ if (mp_typeof(*field->bar.parent) == MP_MAP)
+ op->new_field_len += mp_sizeof_str(key_len);
+ }
+ return 0;
+}
+
+int
+xrow_update_op_do_nop_delete(struct xrow_update_op *op,
+ struct xrow_update_field *field)
+{
+ assert(op->opcode == '#');
+ assert(field->type == XUPDATE_NOP);
+ int key_len_or_index = 0;
+ if (xrow_update_bar_locate(op, field, &key_len_or_index) != 0)
+ return -1;
+ if (mp_typeof(*field->bar.parent) == MP_ARRAY) {
+ const char *tmp = field->bar.parent;
+ uint32_t size = mp_decode_array(&tmp);
+ if (key_len_or_index + op->arg.del.count > size)
+ op->arg.del.count = size - key_len_or_index;
+ const char *end = field->bar.point + field->bar.point_size;
+ for (uint32_t i = 1; i < op->arg.del.count; ++i)
+ mp_next(&end);
+ field->bar.point_size = end - field->bar.point;
+ } else {
+ if (op->arg.del.count != 1)
+ return xrow_update_err_delete1(op);
+ /* Take key size into account to delete it too. */
+ key_len_or_index = mp_sizeof_str(key_len_or_index);
+ field->bar.point -= key_len_or_index;
+ field->bar.point_size += key_len_or_index;
+ }
+ return 0;
+}
+
+#define DO_NOP_OP_GENERIC(op_type) \
+int \
+xrow_update_op_do_nop_##op_type(struct xrow_update_op *op, \
+ struct xrow_update_field *field) \
+{ \
+ assert(field->type == XUPDATE_NOP); \
+ 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); \
+}
+
+DO_NOP_OP_GENERIC(arith)
+
+DO_NOP_OP_GENERIC(bit)
+
+DO_NOP_OP_GENERIC(splice)
+
+#undef DO_NOP_OP_GENERIC
+
+#define DO_BAR_OP_GENERIC(op_type) \
+int \
+xrow_update_op_do_bar_##op_type(struct xrow_update_op *op, \
+ struct xrow_update_field *field) \
+{ \
+ /* \
+ * The only way to update a bar is to make a second update \
+ * with the same prefix as this bar. But it is not \
+ * supported yet. \
+ */ \
+ (void) op; \
+ (void) field; \
+ assert(field->type == XUPDATE_BAR); \
+ diag_set(ClientError, ER_UNSUPPORTED, "update", \
+ "intersected JSON paths"); \
+ return -1; \
+}
+
+DO_BAR_OP_GENERIC(insert)
+
+DO_BAR_OP_GENERIC(set)
+
+DO_BAR_OP_GENERIC(delete)
+
+DO_BAR_OP_GENERIC(arith)
+
+DO_BAR_OP_GENERIC(bit)
+
+DO_BAR_OP_GENERIC(splice)
+
+#undef DO_BAR_OP_GENERIC
+
+uint32_t
+xrow_update_bar_sizeof(struct xrow_update_field *field)
+{
+ assert(field->type == XUPDATE_BAR);
+ switch(field->bar.op->opcode) {
+ case '!': {
+ const char *parent = field->bar.parent;
+ uint32_t size = field->size + field->bar.op->new_field_len;
+ if (mp_typeof(*parent) == MP_ARRAY) {
+ uint32_t array_size = mp_decode_array(&parent);
+ return size + mp_sizeof_array(array_size + 1) -
+ mp_sizeof_array(array_size);
+ } else {
+ uint32_t map_size = mp_decode_map(&parent);
+ return size + mp_sizeof_map(map_size + 1) -
+ mp_sizeof_map(map_size);
+ }
+ }
+ case '#': {
+ const char *parent = field->bar.parent;
+ uint32_t delete_count = field->bar.op->arg.del.count;
+ uint32_t size = field->size - field->bar.point_size;
+ if (mp_typeof(*parent) == MP_ARRAY) {
+ uint32_t array_size = mp_decode_array(&parent);
+ assert(array_size >= delete_count);
+ return size - mp_sizeof_array(array_size) +
+ mp_sizeof_array(array_size - delete_count);
+ } else {
+ uint32_t map_size = mp_decode_map(&parent);
+ assert(delete_count == 1);
+ return size - mp_sizeof_map(map_size) +
+ mp_sizeof_map(map_size - 1);
+ }
+ }
+ default: {
+ return field->size - field->bar.point_size +
+ field->bar.op->new_field_len;
+ }
+ }
+}
+
+uint32_t
+xrow_update_bar_store(struct xrow_update_field *field, char *out, char *out_end)
+{
+ assert(field->type == XUPDATE_BAR);
+ (void) out_end;
+ struct xrow_update_op *op = field->bar.op;
+ char *out_saved = out;
+ switch(op->opcode) {
+ case '!': {
+ const char *pos = field->bar.parent;
+ uint32_t before_parent = pos - field->data;
+ /* Before parent. */
+ memcpy(out, field->data, before_parent);
+ out += before_parent;
+ if (mp_typeof(*pos) == MP_ARRAY) {
+ /* New array header. */
+ uint32_t size = mp_decode_array(&pos);
+ out = mp_encode_array(out, size + 1);
+ /* Before insertion point. */
+ size = field->bar.point - pos;
+ memcpy(out, pos, size);
+ out += size;
+ pos += size;
+ } else {
+ /* New map header. */
+ uint32_t size = mp_decode_map(&pos);
+ out = mp_encode_map(out, size + 1);
+ /* New key. */
+ out = mp_encode_str(out, field->bar.new_key,
+ field->bar.new_key_len);
+ }
+ /* New value. */
+ memcpy(out, op->arg.set.value, op->arg.set.length);
+ out += op->arg.set.length;
+ /* Old values and field tail. */
+ uint32_t after_point = field->data + field->size - pos;
+ memcpy(out, pos, after_point);
+ out += after_point;
+ return out - out_saved;
+ }
+ case '#': {
+ const char *pos = field->bar.parent;
+ uint32_t size, before_parent = pos - field->data;
+ memcpy(out, field->data, before_parent);
+ out += before_parent;
+ if (mp_typeof(*pos) == MP_ARRAY) {
+ size = mp_decode_array(&pos);
+ out = mp_encode_array(out, size - op->arg.del.count);
+ } else {
+ size = mp_decode_map(&pos);
+ out = mp_encode_map(out, size - 1);
+ }
+ size = field->bar.point - pos;
+ memcpy(out, pos, size);
+ out += size;
+ pos = field->bar.point + field->bar.point_size;
+
+ size = field->data + field->size - pos;
+ memcpy(out, pos, size);
+ return out + size - out_saved;
+ }
+ default: {
+ uint32_t before_point = field->bar.point - field->data;
+ const char *field_end = field->data + field->size;
+ const char *point_end =
+ field->bar.point + field->bar.point_size;
+ uint32_t after_point = field_end - point_end;
+
+ memcpy(out, field->data, before_point);
+ out += before_point;
+ op->meta->store(op, field->bar.point, out);
+ out += op->new_field_len;
+ memcpy(out, point_end, after_point);
+ return out + after_point - out_saved;
+ }
+ }
+}
diff --git a/src/box/xrow_update_field.c b/src/box/xrow_update_field.c
index c694e17e9..de865a21d 100644
--- a/src/box/xrow_update_field.c
+++ b/src/box/xrow_update_field.c
@@ -38,7 +38,9 @@
static inline const char *
xrow_update_op_field_str(const struct xrow_update_op *op)
{
- if (op->field_no >= 0)
+ if (op->lexer.src != NULL)
+ return tt_sprintf("'%.*s'", op->lexer.src_len, op->lexer.src);
+ else if (op->field_no >= 0)
return tt_sprintf("%d", op->field_no + TUPLE_INDEX_BASE);
else
return tt_sprintf("%d", op->field_no);
@@ -80,8 +82,13 @@ xrow_update_err_splice_bound(const struct xrow_update_op *op)
int
xrow_update_err_no_such_field(const struct xrow_update_op *op)
{
- diag_set(ClientError, ER_NO_SUCH_FIELD_NO, op->field_no >= 0 ?
- TUPLE_INDEX_BASE + op->field_no : op->field_no);
+ if (op->lexer.src == NULL) {
+ diag_set(ClientError, ER_NO_SUCH_FIELD_NO, op->field_no +
+ (op->field_no >= 0 ? TUPLE_INDEX_BASE : 0));
+ return -1;
+ }
+ diag_set(ClientError, ER_NO_SUCH_FIELD_NAME,
+ xrow_update_op_field_str(op));
return -1;
}
@@ -105,6 +112,8 @@ xrow_update_field_sizeof(struct xrow_update_field *field)
return field->scalar.op->new_field_len;
case XUPDATE_ARRAY:
return xrow_update_array_sizeof(field);
+ case XUPDATE_BAR:
+ return xrow_update_bar_sizeof(field);
default:
unreachable();
}
@@ -130,6 +139,8 @@ xrow_update_field_store(struct xrow_update_field *field, char *out,
return size;
case XUPDATE_ARRAY:
return xrow_update_array_store(field, out, out_end);
+ case XUPDATE_BAR:
+ return xrow_update_bar_store(field, out, out_end);
default:
unreachable();
}
@@ -632,6 +643,7 @@ xrow_update_op_decode(struct xrow_update_op *op, int index_base,
switch(mp_typeof(**expr)) {
case MP_INT:
case MP_UINT: {
+ json_lexer_create(&op->lexer, NULL, 0, 0);
if (xrow_update_mp_read_int32(op, expr, &field_no) != 0)
return -1;
if (field_no - index_base >= 0) {
@@ -647,14 +659,35 @@ xrow_update_op_decode(struct xrow_update_op *op, int index_base,
case MP_STR: {
const char *path = mp_decode_str(expr, &len);
uint32_t field_no, hash = field_name_hash(path, len);
+ json_lexer_create(&op->lexer, path, len, TUPLE_INDEX_BASE);
if (tuple_fieldno_by_name(dict, path, len, hash,
&field_no) == 0) {
op->field_no = (int32_t) field_no;
+ op->lexer.offset = len;
break;
}
- diag_set(ClientError, ER_NO_SUCH_FIELD_NAME,
- tt_cstr(path, len));
- return -1;
+ struct json_token token;
+ int rc = json_lexer_next_token(&op->lexer, &token);
+ if (rc != 0)
+ return xrow_update_err_bad_json(op, rc);
+ switch (token.type) {
+ case JSON_TOKEN_NUM:
+ op->field_no = token.num;
+ break;
+ case JSON_TOKEN_STR:
+ hash = field_name_hash(token.str, token.len);
+ if (tuple_fieldno_by_name(dict, token.str, token.len,
+ hash, &field_no) == 0) {
+ op->field_no = (int32_t) field_no;
+ break;
+ }
+ FALLTHROUGH;
+ default:
+ diag_set(ClientError, ER_NO_SUCH_FIELD_NAME,
+ tt_cstr(path, len));
+ return -1;
+ }
+ break;
}
default:
diag_set(ClientError, ER_ILLEGAL_PARAMS,
diff --git a/src/box/xrow_update_field.h b/src/box/xrow_update_field.h
index e90095b9e..bda9222cc 100644
--- a/src/box/xrow_update_field.h
+++ b/src/box/xrow_update_field.h
@@ -32,6 +32,7 @@
*/
#include "trivia/util.h"
#include "tt_static.h"
+#include "json/json.h"
#include "bit/int96.h"
#include "mp_decimal.h"
#include <stdint.h>
@@ -184,6 +185,12 @@ struct xrow_update_op {
uint32_t new_field_len;
/** Opcode symbol: = + - / ... */
char opcode;
+ /**
+ * Operation target path and its lexer in one. This lexer
+ * is used when the operation is applied down through the
+ * update tree.
+ */
+ struct json_lexer lexer;
};
/**
@@ -200,6 +207,21 @@ int
xrow_update_op_decode(struct xrow_update_op *op, int index_base,
struct tuple_dictionary *dict, const char **expr);
+/**
+ * Check if the operation should be applied on the current path
+ * node, i.e. it is terminal. When an operation is just decoded
+ * and is applied to the top level of a tuple, a head of the JSON
+ * path is cut out. If nothing left, it is applied there.
+ * Otherwise the operation is applied to the next level of the
+ * tuple, according to where the path goes, and so on. In the end
+ * it reaches the target point, where it becomes terminal.
+ */
+static inline bool
+xrow_update_op_is_term(const struct xrow_update_op *op)
+{
+ return json_lexer_is_eof(&op->lexer);
+}
+
/* }}} xrow_update_op */
/* {{{ xrow_update_field */
@@ -224,6 +246,14 @@ enum xrow_update_type {
* of individual fields.
*/
XUPDATE_ARRAY,
+ /**
+ * Field of this type stores such update, that has
+ * non-empty JSON path isolated from all other update
+ * operations. In such optimized case it is possible to do
+ * not allocate neither fields nor ops nor anything for
+ * path nodes. And this is the most common case.
+ */
+ XUPDATE_BAR,
};
/**
@@ -255,6 +285,55 @@ struct xrow_update_field {
struct {
struct xrow_update_rope *rope;
} array;
+ /**
+ * Bar update - by an isolated JSON path not
+ * intersected with any another update operation.
+ */
+ struct {
+ /**
+ * Bar update is a single operation
+ * always, no children, by definition.
+ */
+ struct xrow_update_op *op;
+ /**
+ * Always has a non-empty path leading
+ * inside this field's data. This is used
+ * to find the longest common prefix, when
+ * a new update operation intersects with
+ * this bar.
+ */
+ const char *path;
+ int path_len;
+ /**
+ * For insertion/deletion to change
+ * parent's header.
+ */
+ const char *parent;
+ union {
+ /**
+ * For scalar op; insertion into
+ * array; deletion. This is the
+ * point to delete, change or
+ * insert after.
+ */
+ struct {
+ const char *point;
+ uint32_t point_size;
+ };
+ /*
+ * For insertion into map. New
+ * key. On insertion into a map
+ * there is no strict order as in
+ * array and no point. The field
+ * is inserted just right after
+ * the parent header.
+ */
+ struct {
+ const char *new_key;
+ uint32_t new_key_len;
+ };
+ };
+ } bar;
};
};
@@ -351,6 +430,18 @@ OP_DECL_GENERIC(array)
/* }}} xrow_update_field.array */
+/* {{{ update_field.bar */
+
+OP_DECL_GENERIC(bar)
+
+/* }}} update_field.bar */
+
+/* {{{ update_field.nop */
+
+OP_DECL_GENERIC(nop)
+
+/* }}} update_field.nop */
+
#undef OP_DECL_GENERIC
/* {{{ Common helpers. */
@@ -372,6 +463,10 @@ xrow_update_op_do_field_##op_type(struct xrow_update_op *op, \
switch (field->type) { \
case XUPDATE_ARRAY: \
return xrow_update_op_do_array_##op_type(op, field); \
+ case XUPDATE_NOP: \
+ return xrow_update_op_do_nop_##op_type(op, field); \
+ case XUPDATE_BAR: \
+ return xrow_update_op_do_bar_##op_type(op, field); \
default: \
unreachable(); \
} \
@@ -439,6 +534,26 @@ xrow_update_err_double(const struct xrow_update_op *op)
return xrow_update_err(op, "double update of the same field");
}
+static inline int
+xrow_update_err_bad_json(const struct xrow_update_op *op, int pos)
+{
+ return xrow_update_err(op, tt_sprintf("invalid JSON in position %d",
+ pos));
+}
+
+static inline int
+xrow_update_err_delete1(const struct xrow_update_op *op)
+{
+ return xrow_update_err(op, "can delete only 1 field from a map in a "\
+ "row");
+}
+
+static inline int
+xrow_update_err_duplicate(const struct xrow_update_op *op)
+{
+ return xrow_update_err(op, "the key exists already");
+}
+
/** }}} Error helpers. */
#endif /* TARANTOOL_BOX_TUPLE_UPDATE_FIELD_H */
diff --git a/test/box/update.result b/test/box/update.result
index c6a5a25a7..ee38c100f 100644
--- a/test/box/update.result
+++ b/test/box/update.result
@@ -834,7 +834,7 @@ s:update({0}, {{'+', 0}})
...
s:update({0}, {{'+', '+', '+'}})
---
-- error: Field '+' was not found in the tuple
+- error: 'Field ''+'' UPDATE error: invalid JSON in position 1'
...
s:update({0}, {{0, 0, 0}})
---
@@ -889,3 +889,411 @@ s:update(1, {{'=', 3, map}})
s:drop()
---
...
+--
+-- gh-1261: update by JSON path.
+--
+format = {}
+---
+...
+format[1] = {'field1', 'unsigned'}
+---
+...
+format[2] = {'f', 'map'}
+---
+...
+format[3] = {'g', 'array'}
+---
+...
+s = box.schema.create_space('test', {format = format})
+---
+...
+pk = s:create_index('pk')
+---
+...
+t = {}
+---
+...
+t[1] = 1
+---
+...
+t[2] = { \
+ a = 100, \
+ b = 200, \
+ c = { \
+ d = 400, \
+ e = 500, \
+ f = {4, 5, 6, 7, 8}, \
+ g = {k = 600, l = 700} \
+ }, \
+ m = true, \
+ g = {800, 900} \
+}; \
+t[3] = { \
+ 100, \
+ 200, \
+ { \
+ {300, 350}, \
+ {400, 450} \
+ }, \
+ {a = 500, b = 600}, \
+ {c = 700, d = 800} \
+}
+---
+...
+t = s:insert(t)
+---
+...
+t4_array = t:update({{'!', 4, setmetatable({}, {__serialize = 'array'})}})
+---
+...
+t4_map = t:update({{'!', 4, setmetatable({}, {__serialize = 'map'})}})
+---
+...
+t
+---
+- [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}]]
+...
+--
+-- At first, test simple non-intersected paths.
+--
+--
+-- !
+--
+t:update({{'!', 'f.c.f[1]', 3}, {'!', '[3][1]', {100, 200, 300}}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [3, 4, 5, 6, 7, 8], 'e': 500,
+ 'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [[100, 200, 300], 100, 200, [
+ [300, 350], [400, 450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+t:update({{'!', 'f.g[3]', 1000}})
+---
+- [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, 1000]}, [100, 200, [[300, 350],
+ [400, 450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+t:update({{'!', 'g[6]', 'new element'}})
+---
+- [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}, 'new element']]
+...
+t:update({{'!', 'f.e', 300}, {'!', 'g[4].c', 700}})
+---
+- [1, {'b': 200, 'm': true, 'g': [800, 900], 'a': 100, 'c': {'d': 400, 'f': [4, 5,
+ 6, 7, 8], 'e': 500, 'g': {'k': 600, 'l': 700}}, 'e': 300}, [100, 200, [[300,
+ 350], [400, 450]], {'b': 600, 'c': 700, 'a': 500}, {'c': 700, 'd': 800}]]
+...
+t:update({{'!', 'f.c.f[2]', 4.5}, {'!', 'g[3][2][2]', 425}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 4.5, 5, 6, 7, 8], 'e': 500,
+ 'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+ 425, 450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+t2 = t:update({{'!', 'g[6]', {100}}})
+---
+...
+-- Test single element array update.
+t2:update({{'!', 'g[6][2]', 200}})
+---
+- [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}, [100, 200]]]
+...
+t2:update({{'!', 'g[6][1]', 50}})
+---
+- [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}, [50, 100]]]
+...
+-- Test empty array/map.
+t4_array:update({{'!', '[4][1]', 100}})
+---
+- [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}], [100]]
+...
+t4_map:update({{'!', '[4].a', 100}})
+---
+- [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}], {'a': 100}]
+...
+-- Test errors.
+t:update({{'!', 'a', 100}}) -- No such field.
+---
+- error: Field 'a' was not found in the tuple
+...
+t:update({{'!', 'f.a', 300}}) -- Key already exists.
+---
+- error: 'Field ''f.a'' UPDATE error: the key exists already'
+...
+t:update({{'!', 'f.c.f[0]', 3.5}}) -- No such index, too small.
+---
+- 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.
+---
+- error: Field ''f.c.f[100]'' was not found in the tuple
+...
+t:update({{'!', 'g[4][100]', 700}}) -- Insert index into map.
+---
+- error: 'Field ''g[4][100]'' UPDATE error: can not access by index a non-array field'
+...
+t:update({{'!', 'g[1][1]', 300}})
+---
+- 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.
+---
+- error: 'Field ''f.g.a'' UPDATE error: can not access by key a non-map field'
+...
+t:update({{'!', 'f.g[1].a', 700}})
+---
+- 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.
+---
+- error: 'Field ''f[*].k'' UPDATE error: invalid JSON in position 3'
+...
+-- JSON error after the not existing field to insert.
+t:update({{'!', '[2].e.100000', 100}})
+---
+- error: 'Field ''[2].e.100000'' UPDATE error: invalid JSON in position 7'
+...
+-- Correct JSON, but next to last field does not exist. '!' can't
+-- create the whole path.
+t:update({{'!', '[2].e.f', 100}})
+---
+- error: Field ''[2].e.f'' was not found in the tuple
+...
+--
+-- =
+--
+-- Set existing fields.
+t:update({{'=', 'f.a', 150}, {'=', 'g[3][1][2]', 400}})
+---
+- [1, {'b': 200, 'm': true, 'a': 150, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+ 'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 400], [400,
+ 450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+t:update({{'=', 'f', {a = 100, b = 200}}})
+---
+- [1, {'a': 100, 'b': 200}, [100, 200, [[300, 350], [400, 450]], {'a': 500, 'b': 600},
+ {'c': 700, 'd': 800}]]
+...
+t:update({{'=', 'g[4].b', 700}})
+---
+- [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': 700}, {'c': 700, 'd': 800}]]
+...
+-- Insert via set.
+t:update({{'=', 'f.e', 300}})
+---
+- [1, {'b': 200, 'm': true, 'g': [800, 900], 'a': 100, 'c': {'d': 400, 'f': [4, 5,
+ 6, 7, 8], 'e': 500, 'g': {'k': 600, 'l': 700}}, 'e': 300}, [100, 200, [[300,
+ 350], [400, 450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+t:update({{'=', 'f.g[3]', 1000}})
+---
+- [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, 1000]}, [100, 200, [[300, 350],
+ [400, 450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+t:update({{'=', 'f.g[1]', 0}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7, 8], 'e': 500,
+ 'g': {'k': 600, 'l': 700}}, 'g': [0, 900]}, [100, 200, [[300, 350], [400, 450]],
+ {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+-- Test empty array/map.
+t4_array:update({{'=', '[4][1]', 100}})
+---
+- [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}], [100]]
+...
+t4_map:update({{'=', '[4]["a"]', 100}})
+---
+- [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}], {'a': 100}]
+...
+-- Test errors.
+t:update({{'=', 'f.a[1]', 100}})
+---
+- error: 'Field ''f.a[1]'' UPDATE error: can not access by index a non-array field'
+...
+t:update({{'=', 'f.a.k', 100}})
+---
+- error: 'Field ''f.a.k'' UPDATE error: can not access by key a non-map field'
+...
+t:update({{'=', 'f.c.f[1]', 100}})
+---
+- [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}})
+---
+- error: Field ''f.c.f[100]'' was not found in the tuple
+...
+t:update({{'=', '[2].c.f 1 1 1 1', 100}})
+---
+- error: 'Field ''[2].c.f 1 1 1 1'' UPDATE error: invalid JSON in position 8'
+...
+--
+-- #
+--
+t:update({{'#', '[2].b', 1}})
+---
+- [1, {'a': 100, 'm': true, '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}]]
+...
+t:update({{'#', 'f.c.f[1]', 1}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [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[1]', 2}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [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[1]', 100}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [], '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[5]', 1}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7], '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[5]', 2}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 5, 6, 7], 'e': 500,
+ 'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[300, 350], [400,
+ 450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+-- Test errors.
+t:update({{'#', 'f.h', 1}})
+---
+- error: Field ''f.h'' was not found in the tuple
+...
+t:update({{'#', 'f.c.f[100]', 1}})
+---
+- error: Field ''f.c.f[100]'' was not found in the tuple
+...
+t:update({{'#', 'f.b', 2}})
+---
+- error: 'Field ''f.b'' UPDATE error: can delete only 1 field from a map in a row'
+...
+t:update({{'#', 'f.b', 0}})
+---
+- error: 'Field ''f.b'' UPDATE error: cannot delete 0 fields'
+...
+t:update({{'#', 'f', 0}})
+---
+- error: 'Field ''f'' UPDATE error: cannot delete 0 fields'
+...
+--
+-- Scalar operations.
+--
+t:update({{'+', 'f.a', 50}})
+---
+- [1, {'b': 200, 'm': true, 'a': 150, '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}]]
+...
+t:update({{'-', 'f.c.f[1]', 0.5}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [3.5, 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[2]', 4}})
+---
+- [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [4, 4, 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}]]
+...
+t2 = t:update({{'=', 4, {str = 'abcd'}}})
+---
+...
+t2:update({{':', '[4].str', 2, 2, 'e'}})
+---
+- [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}], {'str': 'aed'}]
+...
+-- Test errors.
+t:update({{'+', 'g[3]', 50}})
+---
+- error: 'Argument type in operation ''+'' on field ''g[3]'' does not match field
+ type: expected a number'
+...
+t:update({{'+', '[2].b.......', 100}})
+---
+- error: 'Field ''[2].b.......'' UPDATE error: invalid JSON in position 7'
+...
+t:update({{'+', '[2].b.c.d.e', 100}})
+---
+- error: Field ''[2].b.c.d.e'' was not found in the tuple
+...
+t:update({{'-', '[2][*]', 20}})
+---
+- error: 'Field ''[2][*]'' UPDATE error: invalid JSON in position 5'
+...
+-- Vinyl normalizes field numbers. It should not touch paths,
+-- and they should not affect squashing.
+format = {}
+---
+...
+format[1] = {'field1', 'unsigned'}
+---
+...
+format[2] = {'field2', 'any'}
+---
+...
+vy_s = box.schema.create_space('test2', {engine = 'vinyl', format = format})
+---
+...
+pk = vy_s:create_index('pk')
+---
+...
+_ = vy_s:replace(t)
+---
+...
+box.begin()
+---
+...
+-- Use a scalar operation, only they can be squashed.
+vy_s:upsert({1, 1}, {{'+', 'field2.c.f[1]', 1}})
+---
+...
+vy_s:upsert({1, 1}, {{'+', '[3][3][1][1]', 1}})
+---
+...
+box.commit()
+---
+...
+vy_s:select()
+---
+- - [1, {'b': 200, 'm': true, 'a': 100, 'c': {'d': 400, 'f': [5, 5, 6, 7, 8], 'e': 500,
+ 'g': {'k': 600, 'l': 700}}, 'g': [800, 900]}, [100, 200, [[301, 350], [400,
+ 450]], {'a': 500, 'b': 600}, {'c': 700, 'd': 800}]]
+...
+vy_s:drop()
+---
+...
+s:drop()
+---
+...
diff --git a/test/box/update.test.lua b/test/box/update.test.lua
index ac7698ce9..60e669d27 100644
--- a/test/box/update.test.lua
+++ b/test/box/update.test.lua
@@ -280,3 +280,148 @@ t:update({{'=', 3, map}})
s:update(1, {{'=', 3, map}})
s:drop()
+
+--
+-- gh-1261: update by JSON path.
+--
+format = {}
+format[1] = {'field1', 'unsigned'}
+format[2] = {'f', 'map'}
+format[3] = {'g', 'array'}
+s = box.schema.create_space('test', {format = format})
+pk = s:create_index('pk')
+t = {}
+t[1] = 1
+t[2] = { \
+ a = 100, \
+ b = 200, \
+ c = { \
+ d = 400, \
+ e = 500, \
+ f = {4, 5, 6, 7, 8}, \
+ g = {k = 600, l = 700} \
+ }, \
+ m = true, \
+ g = {800, 900} \
+}; \
+t[3] = { \
+ 100, \
+ 200, \
+ { \
+ {300, 350}, \
+ {400, 450} \
+ }, \
+ {a = 500, b = 600}, \
+ {c = 700, d = 800} \
+}
+t = s:insert(t)
+
+t4_array = t:update({{'!', 4, setmetatable({}, {__serialize = 'array'})}})
+t4_map = t:update({{'!', 4, setmetatable({}, {__serialize = 'map'})}})
+
+t
+--
+-- At first, test simple non-intersected paths.
+--
+
+--
+-- !
+--
+t:update({{'!', 'f.c.f[1]', 3}, {'!', '[3][1]', {100, 200, 300}}})
+t:update({{'!', 'f.g[3]', 1000}})
+t:update({{'!', 'g[6]', 'new element'}})
+t:update({{'!', 'f.e', 300}, {'!', 'g[4].c', 700}})
+t:update({{'!', 'f.c.f[2]', 4.5}, {'!', 'g[3][2][2]', 425}})
+t2 = t:update({{'!', 'g[6]', {100}}})
+-- Test single element array update.
+t2:update({{'!', 'g[6][2]', 200}})
+t2:update({{'!', 'g[6][1]', 50}})
+-- Test empty array/map.
+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.
+-- JSON error after the not existing field to insert.
+t:update({{'!', '[2].e.100000', 100}})
+-- Correct JSON, but next to last field does not exist. '!' can't
+-- create the whole path.
+t:update({{'!', '[2].e.f', 100}})
+
+--
+-- =
+--
+-- Set existing fields.
+t:update({{'=', 'f.a', 150}, {'=', 'g[3][1][2]', 400}})
+t:update({{'=', 'f', {a = 100, b = 200}}})
+t:update({{'=', 'g[4].b', 700}})
+-- Insert via set.
+t:update({{'=', 'f.e', 300}})
+t:update({{'=', 'f.g[3]', 1000}})
+t:update({{'=', 'f.g[1]', 0}})
+-- Test empty array/map.
+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}})
+
+--
+-- #
+--
+t:update({{'#', '[2].b', 1}})
+t:update({{'#', 'f.c.f[1]', 1}})
+t:update({{'#', 'f.c.f[1]', 2}})
+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}})
+
+--
+-- Scalar operations.
+--
+t:update({{'+', 'f.a', 50}})
+t:update({{'-', 'f.c.f[1]', 0.5}})
+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}})
+
+-- Vinyl normalizes field numbers. It should not touch paths,
+-- and they should not affect squashing.
+format = {}
+format[1] = {'field1', 'unsigned'}
+format[2] = {'field2', 'any'}
+vy_s = box.schema.create_space('test2', {engine = 'vinyl', format = format})
+pk = vy_s:create_index('pk')
+_ = vy_s:replace(t)
+
+box.begin()
+-- Use a scalar operation, only they can be squashed.
+vy_s:upsert({1, 1}, {{'+', 'field2.c.f[1]', 1}})
+vy_s:upsert({1, 1}, {{'+', '[3][3][1][1]', 1}})
+box.commit()
+
+vy_s:select()
+vy_s:drop()
+
+s:drop()
diff --git a/test/engine/update.result b/test/engine/update.result
index f181924f3..ddb13bd5b 100644
--- a/test/engine/update.result
+++ b/test/engine/update.result
@@ -843,11 +843,6 @@ t:update({{'+', '[1]', 50}})
---
- [1, [10, 11, 12], {'b': 21, 'a': 20, 'c': 22}, 'abcdefgh', true, -100, 250]
...
--- JSON paths are not allowed yet.
-t:update({{'=', 'field2[1]', 13}})
----
-- error: Field 'field2[1]' was not found in the tuple
-...
s:update({1}, {{'=', 'field3', {d = 30, e = 31, f = 32}}})
---
- [1, [10, 11, 12], {'d': 30, 'f': 32, 'e': 31}, 'abcdefgh', true, -100, 200]
diff --git a/test/engine/update.test.lua b/test/engine/update.test.lua
index 4ca2589e4..31fca2b7b 100644
--- a/test/engine/update.test.lua
+++ b/test/engine/update.test.lua
@@ -156,8 +156,6 @@ t:update({{':', 'field4', 3, 3, 'bbccdd'}, {'+', 'field6', 50}, {'!', 7, 300}})
-- Any path is interpreted as a field name first. And only then
-- as JSON.
t:update({{'+', '[1]', 50}})
--- JSON paths are not allowed yet.
-t:update({{'=', 'field2[1]', 13}})
s:update({1}, {{'=', 'field3', {d = 30, e = 31, f = 32}}})
diff --git a/test/unit/column_mask.c b/test/unit/column_mask.c
index 3beef5ce0..8401a4f7f 100644
--- a/test/unit/column_mask.c
+++ b/test/unit/column_mask.c
@@ -225,16 +225,87 @@ basic_test()
column_masks[i]);
}
+static void
+test_paths(void)
+{
+ header();
+ plan(2);
+
+ char buffer1[1024];
+ char *pos1 = mp_encode_array(buffer1, 7);
+
+ pos1 = mp_encode_uint(pos1, 1);
+ pos1 = mp_encode_uint(pos1, 2);
+ pos1 = mp_encode_array(pos1, 2);
+ pos1 = mp_encode_uint(pos1, 3);
+ pos1 = mp_encode_uint(pos1, 4);
+ pos1 = mp_encode_uint(pos1, 5);
+ pos1 = mp_encode_array(pos1, 2);
+ pos1 = mp_encode_uint(pos1, 6);
+ pos1 = mp_encode_uint(pos1, 7);
+ pos1 = mp_encode_uint(pos1, 8);
+ pos1 = mp_encode_uint(pos1, 9);
+
+
+ char buffer2[1024];
+ char *pos2 = mp_encode_array(buffer2, 2);
+
+ pos2 = mp_encode_array(pos2, 3);
+ pos2 = mp_encode_str(pos2, "!", 1);
+ pos2 = mp_encode_str(pos2, "[3][1]", 6);
+ pos2 = mp_encode_double(pos2, 2.5);
+
+ pos2 = mp_encode_array(pos2, 3);
+ pos2 = mp_encode_str(pos2, "#", 1);
+ pos2 = mp_encode_str(pos2, "[5][1]", 6);
+ pos2 = mp_encode_uint(pos2, 1);
+
+ struct region *gc = &fiber()->gc;
+ size_t svp = region_used(gc);
+ uint32_t result_size;
+ uint64_t column_mask;
+ const char *result =
+ xrow_update_execute(buffer2, pos2, buffer1, pos1,
+ box_tuple_format_default()->dict,
+ &result_size, 1, &column_mask);
+ isnt(result, NULL, "JSON update works");
+
+ /*
+ * Updates on their first level change fields [3] and [5],
+ * or 2 and 4 if 0-based. If that was the single level,
+ * the operations '!' and '#' would change the all the
+ * fields from 2. But each of these operations are not for
+ * the root and therefore does not affect anything except
+ * [3] and [5] on the first level.
+ */
+ uint64_t expected_mask = 0;
+ column_mask_set_fieldno(&expected_mask, 2);
+ column_mask_set_fieldno(&expected_mask, 4);
+ is(column_mask, expected_mask, "column mask match");
+
+ region_truncate(gc, svp);
+
+ check_plan();
+ footer();
+}
+
+static uint32_t
+simple_hash(const char* str, uint32_t len)
+{
+ return str[0] + len;
+}
+
int
main()
{
memory_init();
fiber_init(fiber_c_invoke);
- tuple_init(NULL);
+ tuple_init(simple_hash);
header();
- plan(27);
+ plan(28);
basic_test();
+ test_paths();
footer();
check_plan();
diff --git a/test/unit/column_mask.result b/test/unit/column_mask.result
index 9309e6cdc..1d87a2f24 100644
--- a/test/unit/column_mask.result
+++ b/test/unit/column_mask.result
@@ -1,5 +1,5 @@
*** main ***
-1..27
+1..28
ok 1 - check result length
ok 2 - tuple update is correct
ok 3 - column_mask is correct
@@ -27,4 +27,10 @@ ok 24 - column_mask is correct
ok 25 - check result length
ok 26 - tuple update is correct
ok 27 - column_mask is correct
+ *** test_paths ***
+ 1..2
+ ok 1 - JSON update works
+ ok 2 - column mask match
+ok 28 - subtests
+ *** test_paths: done ***
*** main: done ***
--
2.21.0 (Apple Git-122.2)
More information about the Tarantool-patches
mailing list