From: Vladislav Shpilevoy <v.shpilevoy@tarantool.org> To: tarantool-patches@dev.tarantool.org, kostja.osipov@gmail.com Subject: [Tarantool-patches] [PATCH 1/1] tuple: enable isolated JSON updates Date: Fri, 22 Nov 2019 00:19:11 +0100 [thread overview] Message-ID: <da5e5278552b062e101f0addfc415f83a9810edd.1574378275.git.v.shpilevoy@tarantool.org> (raw) 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)
next reply other threads:[~2019-11-21 23:12 UTC|newest] Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top 2019-11-21 23:19 Vladislav Shpilevoy [this message] 2019-12-09 20:18 ` Sergey Ostanevich 2019-12-13 23:34 ` Vladislav Shpilevoy 2019-12-14 9:20 ` Sergey Ostanevich 2019-12-19 8:34 ` Kirill Yukhin
Reply instructions: You may reply publicly to this message via plain-text email using any one of the following methods: * Save the following mbox file, import it into your mail client, and reply-to-all from there: mbox Avoid top-posting and favor interleaved quoting: https://en.wikipedia.org/wiki/Posting_style#Interleaved_style * Reply using the --to, --cc, and --in-reply-to switches of git-send-email(1): git send-email \ --in-reply-to=da5e5278552b062e101f0addfc415f83a9810edd.1574378275.git.v.shpilevoy@tarantool.org \ --to=v.shpilevoy@tarantool.org \ --cc=kostja.osipov@gmail.com \ --cc=tarantool-patches@dev.tarantool.org \ --subject='Re: [Tarantool-patches] [PATCH 1/1] tuple: enable isolated JSON updates' \ /path/to/YOUR_REPLY https://kernel.org/pub/software/scm/git/docs/git-send-email.html * If your mail client supports setting the In-Reply-To header via mailto: links, try the mailto: link
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox